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

namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags;

/// <summary>
///     Implements the driver-agnostic capability surface the
///     <c>DriverNodeManager</c> dispatches to when a node resolves to
///     <c>NodeSource.Virtual</c>. Reads return the engine's last-known
///     evaluation result; subscriptions forward engine-emitted change events as
///     <see cref="ISubscribable.OnDataChange"/> events.
/// </summary>
/// <remarks>
///     <para>
///         <see cref="IWritable"/> is deliberately not implemented — OPC UA client
///         writes to virtual tags are rejected in <c>DriverNodeManager</c> before they
///         reach here. Scripts are the only write path, routed
///         through <c>ctx.SetVirtualTag</c>.
///     </para>
/// </remarks>
public sealed class VirtualTagSource : IReadable, ISubscribable
{
    private readonly VirtualTagEngine _engine;
    private readonly ConcurrentDictionary<string, Subscription> _subs = new(StringComparer.Ordinal);

    /// <summary>
    ///     Initializes a new instance of the <see cref="VirtualTagSource"/> class.
    /// </summary>
    /// <param name="engine">The virtual tag engine.</param>
    public VirtualTagSource(VirtualTagEngine engine)
    {
        _engine = engine ?? throw new ArgumentNullException(nameof(engine));
    }

    /// <summary>
    ///     Occurs when data values change.
    /// </summary>
    public event EventHandler<DataChangeEventArgs>? OnDataChange;

    /// <inheritdoc />
    public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
        IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
    {
        if (fullReferences is null) throw new ArgumentNullException(nameof(fullReferences));
        var results = new DataValueSnapshot[fullReferences.Count];
        for (var i = 0; i < fullReferences.Count; i++)
            results[i] = _engine.Read(fullReferences[i]);
        return Task.FromResult<IReadOnlyList<DataValueSnapshot>>(results);
    }

    /// <inheritdoc />
    public Task<ISubscriptionHandle> SubscribeAsync(
        IReadOnlyList<string> fullReferences,
        TimeSpan publishingInterval,
        CancellationToken cancellationToken)
    {
        if (fullReferences is null) throw new ArgumentNullException(nameof(fullReferences));

        var handle = new SubscriptionHandle(Guid.NewGuid().ToString("N"));
        var observers = new List<IDisposable>(fullReferences.Count);

        // OPC UA convention: for each path, emit the initial-data callback BEFORE
        // registering the change observer. This prevents a race where an upstream change
        // fires the observer between the Subscribe call and the Read call, which would
        // deliver a newer change event before the initial-data event, leaving the client
        // with a stale last-known value.
        foreach (var path in fullReferences)
        {
            var snap = _engine.Read(path);
            OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, path, snap));
            observers.Add(_engine.Subscribe(path, (p, s) =>
                OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, p, s))));
        }
        _subs[handle.DiagnosticId] = new Subscription(handle, observers);

        return Task.FromResult<ISubscriptionHandle>(handle);
    }

    /// <inheritdoc />
    public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
    {
        if (handle is null) throw new ArgumentNullException(nameof(handle));
        if (_subs.TryRemove(handle.DiagnosticId, out var sub))
        {
            foreach (var d in sub.Observers)
            {
                try { d.Dispose(); } catch { }
            }
        }
        return Task.CompletedTask;
    }

    private sealed class SubscriptionHandle : ISubscriptionHandle
    {
        /// <summary>
        ///     Initializes a new instance of the <see cref="SubscriptionHandle"/> class.
        /// </summary>
        /// <param name="id">The subscription identifier.</param>
        public SubscriptionHandle(string id) { DiagnosticId = id; }

        /// <inheritdoc />
        public string DiagnosticId { get; }
    }

    private sealed record Subscription(SubscriptionHandle Handle, IReadOnlyList<IDisposable> Observers);
}
