using System.Collections.Concurrent;
using System.Collections.Immutable;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;

/// <summary>
///     Thread-safe <see cref="ITagUpstreamSource"/> the scripted-alarm host actor pushes
///     tag values INTO and the <see cref="ScriptedAlarmEngine"/> reads / subscribes FROM.
///     In the live runtime the host actor translates each Akka
///     <c>DependencyValueChanged</c> message into a <see cref="Push(string, DataValueSnapshot)"/>
///     call; the engine sees those values synchronously through <see cref="ReadTag"/> and
///     reactively through <see cref="SubscribeTag"/>.
/// </summary>
/// <remarks>
///     <para>
///         A <see cref="ConcurrentDictionary{TKey, TValue}"/> caches the latest snapshot
///         per path so <see cref="ReadTag"/> can answer synchronously (the engine's
///         startup-recovery + read-cache-refill paths both call it). Per-path observer
///         lists are held as an immutable list inside a <see cref="ConcurrentDictionary{TKey, TValue}"/>
///         so subscribe / unsubscribe mutate via atomic compare-and-swap and
///         <see cref="Push(string, DataValueSnapshot)"/> can capture-then-invoke a stable
///         snapshot — a concurrent unsubscribe can never corrupt an in-flight fan-out.
///     </para>
/// </remarks>
public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource
{
    // OPC UA Part 4 StatusCode for an outright Bad value (severity 10, bit 31 set). The
    // codebase has no shared Core.Abstractions constant — concrete producers (GalaxyDriver,
    // the Wonderware historian client) inline this same 0x80000000u, and the engine's own
    // AreInputsReady gate tests exactly this bit — so an "unknown path" snapshot uses it too.
    private const uint StatusBad = 0x80000000u;

    // Intentionally never pruned: cold-start semantics depend on retained last values so
    // ReadTag can answer synchronously for any path that has ever been pushed.
    private readonly ConcurrentDictionary<string, DataValueSnapshot> _cache
        = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, ImmutableList<Action<string, DataValueSnapshot>>> _observers
        = new(StringComparer.Ordinal);
    private readonly Func<DateTime> _clock;

    /// <summary>
    ///     Initializes a new <see cref="DependencyMuxTagUpstreamSource"/>.
    /// </summary>
    /// <param name="clock">
    ///     Optional function supplying the current UTC time, used to stamp the Bad-quality
    ///     snapshot returned for an unknown path. Defaults to <see cref="DateTime.UtcNow"/>,
    ///     mirroring how <see cref="ScriptedAlarmEngine"/> takes its clock.
    /// </param>
    public DependencyMuxTagUpstreamSource(Func<DateTime>? clock = null)
        => _clock = clock ?? (() => DateTime.UtcNow);

    /// <summary>
    ///     Update the cached snapshot for <paramref name="path"/> and then notify every
    ///     observer currently subscribed to that path. NOT part of
    ///     <see cref="ITagUpstreamSource"/> — the host actor calls this from its
    ///     <c>DependencyValueChanged</c> handler.
    /// </summary>
    /// <param name="path">The tag path whose value changed.</param>
    /// <param name="snapshot">The new value snapshot.</param>
    public void Push(string path, DataValueSnapshot snapshot)
    {
        ArgumentNullException.ThrowIfNull(path);
        ArgumentNullException.ThrowIfNull(snapshot);

        // Cache first so any observer that re-reads the path inside its callback sees the
        // value it is being notified about, not the prior one.
        _cache[path] = snapshot;

        // Capture the immutable observer snapshot, then invoke outside any lock: a
        // concurrent Subscribe/Dispose swaps the dictionary entry atomically and cannot
        // mutate the list we are iterating.
        if (_observers.TryGetValue(path, out var observers))
        {
            foreach (var observer in observers)
            {
                try { observer(path, snapshot); }
                catch { /* one misbehaving observer must not silence the rest */ }
            }
        }
    }

    /// <inheritdoc/>
    public DataValueSnapshot ReadTag(string path)
    {
        ArgumentNullException.ThrowIfNull(path);
        if (_cache.TryGetValue(path, out var snapshot))
            return snapshot;

        // No value has been pushed for this path yet — return a Bad-quality placeholder so
        // the engine's cold-start guard holds the prior condition rather than NRE-ing on a
        // null value cast.
        var now = _clock();
        return new DataValueSnapshot(Value: null, StatusCode: StatusBad, SourceTimestampUtc: null, ServerTimestampUtc: now);
    }

    // No-replay contract: a new subscriber does NOT receive a synthetic initial
    // notification for the current cached value. Callers that need the current value must
    // call ReadTag immediately after subscribing. The engine's cold-start path
    // (startup-recovery + read-cache-refill) already does this.
    /// <inheritdoc/>
    public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
    {
        ArgumentNullException.ThrowIfNull(path);
        ArgumentNullException.ThrowIfNull(observer);

        _observers.AddOrUpdate(
            path,
            _ => ImmutableList.Create(observer),
            (_, existing) => existing.Add(observer));

        return new Subscription(this, path, observer);
    }

    private void Unsubscribe(string path, Action<string, DataValueSnapshot> observer)
    {
        // Atomically remove exactly this observer. Robust to dispose-after-already-removed
        // (the path entry may be gone, or the observer may already have been pulled).
        while (_observers.TryGetValue(path, out var existing))
        {
            var updated = existing.Remove(observer);
            if (ReferenceEquals(updated, existing))
                return; // observer not present — nothing to do

            if (updated.IsEmpty)
            {
                // Drop the empty entry, but only if it hasn't changed under us. The
                // KeyValuePair overload removes atomically iff both key AND value still match.
                if (_observers.TryRemove(new KeyValuePair<string, ImmutableList<Action<string, DataValueSnapshot>>>(path, existing)))
                    return;
                // Lost the race — another mutation swapped the entry; retry.
                continue;
            }

            if (_observers.TryUpdate(path, updated, existing))
                return;
            // Lost the CAS — another subscribe/unsubscribe won; retry with the fresh list.
        }
    }

    /// <summary>
    ///     Per-observer subscription handle. <see cref="Dispose"/> deregisters exactly the
    ///     observer it was created for and is idempotent — calling it after the observer has
    ///     already been removed is a no-op.
    /// </summary>
    private sealed class Subscription : IDisposable
    {
        private DependencyMuxTagUpstreamSource? _owner;
        private readonly string _path;
        private readonly Action<string, DataValueSnapshot> _observer;

        /// <summary>Creates a handle that deregisters <paramref name="observer"/> from <paramref name="owner"/> for <paramref name="path"/> on dispose.</summary>
        /// <param name="owner">The upstream source to deregister from.</param>
        /// <param name="path">The tag path the observer is subscribed to.</param>
        /// <param name="observer">The observer callback to deregister.</param>
        public Subscription(DependencyMuxTagUpstreamSource owner, string path, Action<string, DataValueSnapshot> observer)
        {
            _owner = owner;
            _path = path;
            _observer = observer;
        }

        /// <summary>Deregisters the observer from the owning source. Idempotent — safe to call more than once.</summary>
        public void Dispose()
        {
            // Swap _owner to null first so a double-dispose can't deregister twice (the
            // second call sees null). Interlocked makes the guard safe under concurrent
            // Dispose calls on the same handle.
            var owner = Interlocked.Exchange(ref _owner, null);
            owner?.Unsubscribe(_path, _observer);
        }
    }
}
