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

namespace ZB.MOM.WW.OtOpcUa.Server.Phase7;

/// <summary>
///     Production <c>ITagUpstreamSource</c> for the Phase 7 engines (implements both the
///     Core.VirtualTags and Core.ScriptedAlarms variants — identical shape, distinct
///     namespaces). Per the interface docstring, reads are synchronous — user scripts
///     call <c>ctx.GetTag</c> inline — so we serve from a last-known-value cache that
///     the driver-bridge populates asynchronously via <see cref="Push"/>.
/// </summary>
/// <remarks>
///     <para>
///         <see cref="Push"/> is called by the driver-bridge (wiring added by task #244)
///         every time a driver's <c>ISubscribable.OnDataChange</c> fires. Subscribers
///         registered via <see cref="SubscribeTag"/> are notified synchronously on the
///         calling thread — the VirtualTagEngine + ScriptedAlarmEngine handle their own
///         async hand-off via <c>SemaphoreSlim</c>.
///     </para>
///     <para>
///         Reads of a path that has never been <see cref="Push"/>-ed return
///         <see cref="UpstreamNotConfigured"/>-quality — which scripts see as
///         <c>ctx.GetTag("...").StatusCode == BadNodeIdUnknown</c> and can branch on.
///     </para>
/// </remarks>
public sealed class CachedTagUpstreamSource
    : Core.VirtualTags.ITagUpstreamSource,
      Core.ScriptedAlarms.ITagUpstreamSource
{
    private readonly ConcurrentDictionary<string, DataValueSnapshot> _values = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, List<Action<string, DataValueSnapshot>>> _observers
        = new(StringComparer.Ordinal);

    public DataValueSnapshot ReadTag(string path)
    {
        if (string.IsNullOrEmpty(path)) throw new ArgumentException("path required", nameof(path));
        return _values.TryGetValue(path, out var snap)
            ? snap
            : new DataValueSnapshot(null, UpstreamNotConfigured, null, DateTime.UtcNow);
    }

    public IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer)
    {
        if (string.IsNullOrEmpty(path)) throw new ArgumentException("path required", nameof(path));
        ArgumentNullException.ThrowIfNull(observer);

        var list = _observers.GetOrAdd(path, _ => []);
        lock (list) list.Add(observer);
        return new Unsub(this, path, observer);
    }

    /// <summary>
    ///     Driver-bridge write path — called when a driver delivers a value change for
    ///     <paramref name="path"/>. Updates the cache + fans out to every observer.
    ///     Safe for concurrent callers; observers fire on the caller's thread.
    /// </summary>
    public void Push(string path, DataValueSnapshot snapshot)
    {
        if (string.IsNullOrEmpty(path)) throw new ArgumentException("path required", nameof(path));
        ArgumentNullException.ThrowIfNull(snapshot);

        _values[path] = snapshot;
        if (!_observers.TryGetValue(path, out var list)) return;
        Action<string, DataValueSnapshot>[] snapshotList;
        lock (list) snapshotList = list.ToArray();
        foreach (var observer in snapshotList) observer(path, snapshot);
    }

    /// <summary>Mirror of OPC UA <c>StatusCodes.BadNodeIdUnknown</c> without pulling the OPC stack dependency.</summary>
    public const uint UpstreamNotConfigured = 0x80340000;

    private sealed class Unsub(CachedTagUpstreamSource owner, string path, Action<string, DataValueSnapshot> observer) : IDisposable
    {
        private bool _disposed;
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            if (owner._observers.TryGetValue(path, out var list))
                lock (list) list.Remove(observer);
        }
    }
}
