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

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

/// <summary>
///     Adapter that exposes <see cref="ScriptedAlarmEngine"/> through the driver-agnostic
///     <see cref="IAlarmSource"/> surface. The existing Phase 6.1 <c>AlarmTracker</c>
///     composition fan-out consumes this alongside Galaxy / AB CIP / FOCAS alarm
///     sources — no per-source branching in the fan-out.
/// </summary>
/// <remarks>
///     <para>
///         Per Phase 7 plan Stream C.6, ack / confirm / shelve / unshelve are OPC UA
///         method calls per-condition. This adapter implements <see cref="AcknowledgeAsync"/>
///         from the base interface; the richer Part 9 methods (Confirm / Shelve /
///         Unshelve / AddComment) live directly on the engine, invoked from OPC UA
///         method handlers wired up in Stream G.
///     </para>
///     <para>
///         SubscribeAlarmsAsync takes a list of source-node-id filters (typically an
///         Equipment path prefix). When the list is empty every alarm matches. The
///         adapter doesn't maintain per-subscription state beyond the filter set — it
///         checks each emission against every live subscription.
///     </para>
/// </remarks>
public sealed class ScriptedAlarmSource : IAlarmSource, IDisposable
{
    private readonly ScriptedAlarmEngine _engine;
    private readonly ConcurrentDictionary<string, Subscription> _subscriptions
        = new(StringComparer.Ordinal);
    private bool _disposed;

    /// <summary>Initializes a new instance of the <see cref="ScriptedAlarmSource"/> class.</summary>
    /// <param name="engine">The scripted alarm engine to expose.</param>
    public ScriptedAlarmSource(ScriptedAlarmEngine engine)
    {
        _engine = engine ?? throw new ArgumentNullException(nameof(engine));
        _engine.OnEvent += OnEngineEvent;
    }

    /// <summary>Occurs when an alarm event is raised by the engine.</summary>
    public event EventHandler<AlarmEventArgs>? OnAlarmEvent;

    /// <inheritdoc />
    public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
        IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
    {
        if (sourceNodeIds is null) throw new ArgumentNullException(nameof(sourceNodeIds));
        var handle = new SubscriptionHandle(Guid.NewGuid().ToString("N"));
        _subscriptions[handle.DiagnosticId] = new Subscription(handle,
            new HashSet<string>(sourceNodeIds, StringComparer.Ordinal));
        return Task.FromResult<IAlarmSubscriptionHandle>(handle);
    }

    /// <inheritdoc />
    public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
    {
        if (handle is null) throw new ArgumentNullException(nameof(handle));
        _subscriptions.TryRemove(handle.DiagnosticId, out _);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task AcknowledgeAsync(
        IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
    {
        if (acknowledgements is null) throw new ArgumentNullException(nameof(acknowledgements));
        foreach (var a in acknowledgements)
        {
            // Honor the authenticated principal carried on the request when present
            // (the OPC UA dispatch layer / Stream G threads it through). When absent —
            // the raw/non-OPC-UA path — default to "opcua-client" so the ack still
            // produces an audit entry.
            await _engine.AcknowledgeAsync(a.ConditionId, a.OperatorUser ?? "opcua-client", a.Comment, cancellationToken)
                .ConfigureAwait(false);
        }
    }

    private void OnEngineEvent(object? sender, ScriptedAlarmEvent evt)
    {
        if (_disposed) return;

        // #478 — QualityChanged is a source-quality annotation, not a Part 9 state change. It is delivered
        // out of band via the dedicated WriteAlarmQuality node path; surfacing it here would fabricate a
        // phantom AlarmEventArgs that materializes / historizes a condition. Swallow it.
        if (evt.Emission == EmissionKind.QualityChanged) return;

        foreach (var sub in _subscriptions.Values)
        {
            if (!Matches(sub, evt)) continue;
            var payload = new AlarmEventArgs(
                SubscriptionHandle: sub.Handle,
                SourceNodeId: evt.EquipmentPath,
                ConditionId: evt.AlarmId,
                AlarmType: evt.Kind.ToString(),
                Message: evt.Message,
                Severity: evt.Severity,
                SourceTimestampUtc: evt.TimestampUtc);
            try { OnAlarmEvent?.Invoke(this, payload); }
            catch { /* subscriber exceptions don't crash the adapter */ }
        }
    }

    private static bool Matches(Subscription sub, ScriptedAlarmEvent evt)
    {
        if (sub.Filter.Count == 0) return true;
        // A subscription matches if any filter is a prefix of the alarm's equipment
        // path — typical use is "Enterprise/Site/Area/Line" filtering a whole line.
        foreach (var f in sub.Filter)
        {
            if (evt.EquipmentPath.Equals(f, StringComparison.Ordinal)) return true;
            if (evt.EquipmentPath.StartsWith(f + "/", StringComparison.Ordinal)) return true;
        }
        return false;
    }

    /// <summary>Releases resources used by the <see cref="ScriptedAlarmSource"/>.</summary>
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        _engine.OnEvent -= OnEngineEvent;
        _subscriptions.Clear();
    }

    private sealed class SubscriptionHandle : IAlarmSubscriptionHandle
    {
        /// <summary>Initializes a new instance of the <see cref="SubscriptionHandle"/> class.</summary>
        /// <param name="id">The diagnostic ID for this subscription handle.</param>
        public SubscriptionHandle(string id) { DiagnosticId = id; }
        /// <inheritdoc />
        public string DiagnosticId { get; }
    }

    private sealed record Subscription(SubscriptionHandle Handle, IReadOnlySet<string> Filter);
}
