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

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

/// <summary>
///     Generic, driver-agnostic backbone for populating the OPC UA address space from an
///     <see cref="IDriver"/>. Walks the driver's discovery, wires the alarm + data-change +
///     rediscovery subscription events, and hands each variable to the supplied
///     <see cref="IAddressSpaceBuilder"/>. Concrete OPC UA server implementations provide the
///     builder — see the Server project's <c>OpcUaAddressSpaceBuilder</c> for the materialization
///     against <c>CustomNodeManager2</c>.
/// </summary>
/// <remarks>
///     Per <c>docs/v2/plan.md</c> — Core owns the node tree, drivers stream
///     <c>Folder</c>/<c>Variable</c> calls, alarm-bearing variables are annotated via
///     <see cref="IVariableHandle.MarkAsAlarmCondition"/> and subsequent
///     <see cref="IAlarmSource.OnAlarmEvent"/> payloads route to the sink the builder returned.
/// </remarks>
public class GenericDriverNodeManager(IDriver driver) : IDisposable
{
    /// <summary>Gets the underlying driver instance.</summary>
    protected IDriver Driver { get; } = driver ?? throw new ArgumentNullException(nameof(driver));

    /// <summary>Gets the driver instance identifier.</summary>
    public string DriverInstanceId => Driver.DriverInstanceId;

    // Source tag (DriverAttributeInfo.FullName) → alarm-condition sink. Populated during
    // BuildAddressSpaceAsync by a recording IAddressSpaceBuilder implementation that captures the
    // IVariableHandle per attr.IsAlarm=true variable and calls MarkAsAlarmCondition.
    private readonly ConcurrentDictionary<string, IAlarmConditionSink> _alarmSinks =
        new(StringComparer.OrdinalIgnoreCase);

    private EventHandler<AlarmEventArgs>? _alarmForwarder;
    private bool _disposed;

    /// <summary>
    ///     Populates the address space by streaming nodes from the driver into the supplied builder,
    ///     wraps the builder so alarm-condition sinks are captured, subscribes to the driver's
    ///     alarm event stream, and routes each transition to the matching sink by <c>SourceNodeId</c>.
    ///     If called a second time (e.g. Galaxy redeploy via <c>IRediscoverable.OnRediscoveryNeeded</c>)
    ///     the previous alarm subscription is torn down and the sink registry is cleared before
    ///     re-walking, preventing double delivery of alarm transitions.
    ///     Exception isolation (marking the driver's subtree Faulted while other
    ///     drivers stay available) is the caller's responsibility; exceptions from
    ///     <see cref="ITagDiscovery.DiscoverAsync"/> propagate unhandled to the caller. The Server
    ///     project's <c>OpcUaApplicationHost.PopulateAddressSpaces</c> wraps this call in a per-driver
    ///     try/catch that logs + leaves the driver's subtree empty until a Reinitialize succeeds.
    /// </summary>
    /// <param name="builder">The address space builder to populate.</param>
    /// <param name="ct">The cancellation token.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ObjectDisposedException.ThrowIf(_disposed, this);

        // Tear down any previous alarm subscription before re-walking so a second call (e.g. on
        // Galaxy redeploy) does not leave the old forwarder subscribed and double-fire events.
        if (_alarmForwarder is not null && Driver is IAlarmSource existingSource)
        {
            existingSource.OnAlarmEvent -= _alarmForwarder;
            _alarmForwarder = null;
        }
        _alarmSinks.Clear();

        if (Driver is not ITagDiscovery discovery)
            throw new NotSupportedException($"Driver '{Driver.DriverInstanceId}' does not implement ITagDiscovery.");

        var capturing = new CapturingBuilder(builder, _alarmSinks);
#pragma warning disable OTOPCUA0001 // NOT a production dispatch path: this Core scaffolding node-manager is exercised only by GenericDriverNodeManagerTests — the live server materialises the address space via the Server project's own OpcUaAddressSpaceBuilder path. No CapabilityInvoker by design (verified 07/#10: zero production references). If this class is ever wired into live dispatch, route this call through IDriverCapabilityInvoker.
        await discovery.DiscoverAsync(capturing, ct);
#pragma warning restore OTOPCUA0001

        if (Driver is IAlarmSource alarmSource)
        {
            _alarmForwarder = (_, e) =>
            {
                // Route the alarm to the sink registered for the originating variable, if any.
                // Unknown source ids are dropped silently — they may belong to another driver or
                // to a variable the builder chose not to flag as an alarm condition.
                if (_alarmSinks.TryGetValue(e.SourceNodeId, out var sink))
                    sink.OnTransition(e);
            };
            alarmSource.OnAlarmEvent += _alarmForwarder;
        }
    }

    /// <summary>Disposes the node manager and cleans up alarm subscriptions.</summary>
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        if (_alarmForwarder is not null && Driver is IAlarmSource alarmSource)
        {
            alarmSource.OnAlarmEvent -= _alarmForwarder;
        }
        _alarmSinks.Clear();
    }

    /// <summary>
    ///     Snapshot the current alarm-sink registry by source node id. Diagnostic + test hook;
    ///     not part of the hot path.
    /// </summary>
    internal IReadOnlyCollection<string> TrackedAlarmSources => _alarmSinks.Keys.ToList();

    /// <summary>
    ///     Wraps the caller-supplied <see cref="IAddressSpaceBuilder"/> so every
    ///     <see cref="IVariableHandle.MarkAsAlarmCondition"/> call registers the returned sink in
    ///     the node manager's source-node-id map. The builder itself drives materialization;
    ///     this wrapper only observes.
    /// </summary>
    private sealed class CapturingBuilder(
        IAddressSpaceBuilder inner,
        ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IAddressSpaceBuilder
    {
        /// <inheritdoc />
        public IAddressSpaceBuilder Folder(string browseName, string displayName)
            => new CapturingBuilder(inner.Folder(browseName, displayName), sinks);

        /// <inheritdoc />
        public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
            => new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks);

        /// <inheritdoc />
        public void AddProperty(string browseName, DriverDataType dataType, object? value)
            => inner.AddProperty(browseName, dataType, value);
    }

    private sealed class CapturingHandle(
        IVariableHandle inner,
        ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IVariableHandle
    {
        /// <inheritdoc />
        public string FullReference => inner.FullReference;

        /// <inheritdoc />
        public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
        {
            var sink = inner.MarkAsAlarmCondition(info);
            // Register by the driver-side full reference so the alarm forwarder can look it up
            // using AlarmEventArgs.SourceNodeId (which the driver populates with the same tag).
            sinks[inner.FullReference] = sink;
            return sink;
        }
    }
}
