using System;
using System.Collections.Generic;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;

/// <summary>
///     Per-session owner of the worker's alarm-side state. Lazy-creates an
///     <see cref="AlarmDispatcher"/> (with a wnwrap-backed
///     <see cref="WnWrapAlarmConsumer"/> by default) on the first
///     <see cref="Subscribe"/> call, then routes
///     <see cref="Acknowledge"/> / <see cref="QueryActive"/> /
///     <see cref="Unsubscribe"/> through the same instance for the
///     session's lifetime.
/// </summary>
/// <remarks>
///     <para>
///         Construction is dependency-injectable: the consumer factory
///         (default <c>() =&gt; new WnWrapAlarmConsumer()</c>) lets tests
///         substitute a fake without touching AVEVA COM. The event queue
///         is supplied by the owning <see cref="MxAccessStaSession"/> so
///         the alarm-side proto events land on the same queue the worker
///         already drains for IPC dispatch.
///     </para>
///     <para>
///         Threading: invoked from <see cref="MxAccessCommandExecutor"/>
///         which runs on the STA. The wnwrap consumer owns no internal
///         timer — the worker's STA drives <see cref="PollOnce"/> via
///         <c>StaRuntime.InvokeAsync</c>, so the consumer's transition
///         events fire on the same STA. The
///         <see cref="AlarmDispatcher"/>'s event handler hands transitions
///         into the thread-safe <see cref="MxAccessEventQueue"/>.
///     </para>
/// </remarks>
public sealed class AlarmCommandHandler : IAlarmCommandHandler
{
    private readonly MxAccessEventQueue eventQueue;
    private readonly Func<IMxAccessAlarmConsumer> consumerFactory;
    private readonly Action? threadAffinityCheck;
    private readonly object syncRoot = new object();
    private AlarmDispatcher? dispatcher;
    private bool disposed;

    public AlarmCommandHandler(MxAccessEventQueue eventQueue)
        : this(eventQueue, () => new WnWrapAlarmConsumer(), threadAffinityCheck: null)
    {
    }

    /// <summary>Test seam — inject a custom consumer factory.</summary>
    public AlarmCommandHandler(
        MxAccessEventQueue eventQueue,
        Func<IMxAccessAlarmConsumer> consumerFactory)
        : this(eventQueue, consumerFactory, threadAffinityCheck: null)
    {
    }

    /// <summary>
    ///     Worker-024: production constructor that also injects an
    ///     STA-affinity guard. <paramref name="threadAffinityCheck"/> is
    ///     invoked at the entry of every method that touches the underlying
    ///     <see cref="IMxAccessAlarmConsumer"/> (or the wnwrap COM object
    ///     through it) — <see cref="Subscribe"/>, <see cref="Unsubscribe"/>,
    ///     <see cref="Acknowledge"/>, <see cref="AcknowledgeByName"/>,
    ///     <see cref="QueryActive"/>, <see cref="PollOnce"/> — so an
    ///     off-STA call raises a programming-error diagnostic instead of
    ///     deadlocking on cross-apartment marshaling to the
    ///     <c>ThreadingModel=Apartment</c> wnwrap CLSID. The guard is
    ///     optional: tests that already drive the handler on a single
    ///     thread can pass <c>null</c>.
    /// </summary>
    public AlarmCommandHandler(
        MxAccessEventQueue eventQueue,
        Func<IMxAccessAlarmConsumer> consumerFactory,
        Action? threadAffinityCheck)
    {
        this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
        this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory));
        this.threadAffinityCheck = threadAffinityCheck;
    }

    public bool IsSubscribed
    {
        get { lock (syncRoot) return dispatcher is not null; }
    }

    /// <inheritdoc />
    public void Subscribe(string subscription, string sessionId)
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
        if (subscription is null) throw new ArgumentNullException(nameof(subscription));
        threadAffinityCheck?.Invoke();

        lock (syncRoot)
        {
            if (dispatcher is not null)
            {
                throw new InvalidOperationException(
                    "AlarmCommandHandler already has an active subscription; " +
                    "call Unsubscribe before issuing another SubscribeAlarms command.");
            }
            IMxAccessAlarmConsumer consumer = consumerFactory()
                ?? throw new InvalidOperationException("Alarm consumer factory returned null.");
            MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(
                eventQueue, new MxAccessEventMapper());
            dispatcher = new AlarmDispatcher(consumer, sink, sessionId ?? string.Empty);
            try
            {
                dispatcher.Subscribe(subscription);
            }
            catch
            {
                try { dispatcher.Dispose(); } catch { /* swallow */ }
                dispatcher = null;
                throw;
            }
        }
    }

    /// <inheritdoc />
    public void Unsubscribe()
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? toDispose;
        lock (syncRoot)
        {
            toDispose = dispatcher;
            dispatcher = null;
        }
        toDispose?.Dispose();
    }

    /// <inheritdoc />
    public int Acknowledge(
        Guid alarmGuid,
        string comment,
        string operatorUser,
        string operatorNode,
        string operatorDomain,
        string operatorFullName)
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? d = GetDispatcherOrThrow();
        return d.Acknowledge(
            alarmGuid,
            comment ?? string.Empty,
            operatorUser ?? string.Empty,
            operatorNode ?? string.Empty,
            operatorDomain ?? string.Empty,
            operatorFullName ?? string.Empty);
    }

    /// <inheritdoc />
    public int AcknowledgeByName(
        string alarmName,
        string providerName,
        string groupName,
        string comment,
        string operatorUser,
        string operatorNode,
        string operatorDomain,
        string operatorFullName)
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? d = GetDispatcherOrThrow();
        return d.AcknowledgeByName(
            alarmName ?? string.Empty,
            providerName ?? string.Empty,
            groupName ?? string.Empty,
            comment ?? string.Empty,
            operatorUser ?? string.Empty,
            operatorNode ?? string.Empty,
            operatorDomain ?? string.Empty,
            operatorFullName ?? string.Empty);
    }

    /// <inheritdoc />
    public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? d = GetDispatcherOrThrow();
        IReadOnlyList<ActiveAlarmSnapshot> all = d.SnapshotActiveAlarms();
        if (string.IsNullOrEmpty(alarmFilterPrefix)) return all;
        List<ActiveAlarmSnapshot> filtered = new List<ActiveAlarmSnapshot>(all.Count);
        foreach (ActiveAlarmSnapshot snap in all)
        {
            if (snap.AlarmFullReference.StartsWith(alarmFilterPrefix!, StringComparison.Ordinal))
            {
                filtered.Add(snap);
            }
        }
        return filtered;
    }

    /// <inheritdoc />
    public void PollOnce()
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? d;
        lock (syncRoot) d = dispatcher;
        // No-op when not yet subscribed or already disposed.
        d?.PollOnce();
    }

    private AlarmDispatcher GetDispatcherOrThrow()
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
        AlarmDispatcher? d;
        lock (syncRoot) d = dispatcher;
        if (d is null)
        {
            throw new InvalidOperationException(
                "AlarmCommandHandler has no active subscription; " +
                "call SubscribeAlarms before issuing alarm-related commands.");
        }
        return d;
    }

    /// <inheritdoc />
    public void Dispose()
    {
        if (disposed) return;
        disposed = true;
        Unsubscribe();
    }
}
