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 IMxAccessComObjectFactory? comFactory;
    private readonly Func<IReadOnlyList<AlarmSubtagTarget>, IMxAccessAlarmConsumer>? standbyFactory;
    private readonly MxAccessEventMapper mapper = new MxAccessEventMapper();
    private readonly object syncRoot = new object();
    private AlarmDispatcher? dispatcher;
    private FailoverAlarmConsumer? failoverConsumer;
    private EventHandler<AlarmProviderModeChange>? providerModeChangedHandler;
    private string subscribeSessionId = string.Empty;
    private bool disposed;

    /// <summary>Initializes a new alarm command handler with the given event queue.</summary>
    /// <param name="eventQueue">The event queue.</param>
    public AlarmCommandHandler(MxAccessEventQueue eventQueue)
        : this(eventQueue, () => new WnWrapAlarmConsumer(), threadAffinityCheck: null)
    {
    }

    /// <summary>Test seam — inject a custom consumer factory.</summary>
    /// <param name="eventQueue">The event queue.</param>
    /// <param name="consumerFactory">The alarm consumer factory.</param>
    public AlarmCommandHandler(
        MxAccessEventQueue eventQueue,
        Func<IMxAccessAlarmConsumer> consumerFactory)
        : this(eventQueue, consumerFactory, threadAffinityCheck: null)
    {
    }

    /// <summary>
    ///     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>
    /// <param name="eventQueue">The event queue.</param>
    /// <param name="consumerFactory">The alarm consumer factory.</param>
    /// <param name="threadAffinityCheck">Optional thread affinity check action.</param>
    public AlarmCommandHandler(
        MxAccessEventQueue eventQueue,
        Func<IMxAccessAlarmConsumer> consumerFactory,
        Action? threadAffinityCheck)
        : this(eventQueue, consumerFactory, threadAffinityCheck, comFactory: null, standbyFactory: null)
    {
    }

    /// <summary>
    ///     Full constructor that also threads the MXAccess COM-object factory and
    ///     an optional standby-consumer seam so the subscribe path can build the
    ///     subtag / failover consumers required by
    ///     <see cref="SubscribeAlarmsCommand.ForcedMode"/> and
    ///     <see cref="SubscribeAlarmsCommand.WatchList"/>.
    /// </summary>
    /// <param name="eventQueue">The event queue.</param>
    /// <param name="consumerFactory">
    ///     Factory for the PRIMARY (alarmmgr) consumer — the existing
    ///     wnwrap-backed source. Used alone in alarmmgr mode and as the primary
    ///     of the failover composite in auto mode.
    /// </param>
    /// <param name="threadAffinityCheck">Optional STA thread-affinity guard.</param>
    /// <param name="comFactory">
    ///     The MXAccess COM-object factory used to build the
    ///     <see cref="LmxSubtagAlarmSource"/> backing the subtag consumer. May be
    ///     <see langword="null"/> when a <paramref name="standbyFactory"/> is
    ///     supplied (tests) or when only the alarmmgr path is ever exercised.
    /// </param>
    /// <param name="standbyFactory">
    ///     Optional seam that builds the STANDBY (subtag) consumer from a watch
    ///     list. Defaults to a <see cref="SubtagAlarmConsumer"/> over an
    ///     <see cref="LmxSubtagAlarmSource"/> built from
    ///     <paramref name="comFactory"/>. Tests inject a fake so they need no
    ///     live COM factory.
    /// </param>
    public AlarmCommandHandler(
        MxAccessEventQueue eventQueue,
        Func<IMxAccessAlarmConsumer> consumerFactory,
        Action? threadAffinityCheck,
        IMxAccessComObjectFactory? comFactory,
        Func<IReadOnlyList<AlarmSubtagTarget>, IMxAccessAlarmConsumer>? standbyFactory)
    {
        this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
        this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory));
        this.threadAffinityCheck = threadAffinityCheck;
        this.comFactory = comFactory;
        this.standbyFactory = standbyFactory;
    }

    /// <summary>Gets a value indicating whether the handler is subscribed.</summary>
    public bool IsSubscribed
    {
        get { lock (syncRoot) return dispatcher is not null; }
    }

    /// <inheritdoc />
    public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler));
        if (command is null) throw new ArgumentNullException(nameof(command));
        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.");
            }

            subscribeSessionId = sessionId ?? string.Empty;
            IMxAccessAlarmConsumer consumer = BuildConsumer(command)
                ?? throw new InvalidOperationException("Alarm consumer factory returned null.");

            // When the selected consumer is a failover composite, surface its
            // provider switches onto the worker's event queue so connected
            // gateway clients can observe degraded/recovered state. The handler
            // is unsubscribed/disposed on Unsubscribe/Dispose below.
            if (consumer is FailoverAlarmConsumer failover)
            {
                failoverConsumer = failover;
                providerModeChangedHandler = OnProviderModeChanged;
                failover.ProviderModeChanged += providerModeChangedHandler;
            }

            MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(eventQueue, mapper);
            dispatcher = new AlarmDispatcher(consumer, sink, subscribeSessionId);
            try
            {
                dispatcher.Subscribe(command.SubscriptionExpression ?? string.Empty);
            }
            catch
            {
                DetachProviderModeChanged();
                try { dispatcher.Dispose(); } catch { /* swallow */ }
                dispatcher = null;
                throw;
            }
        }
    }

    /// <summary>
    ///     Selects and builds the alarm consumer from the command's
    ///     <see cref="SubscribeAlarmsCommand.ForcedMode"/> and
    ///     <see cref="SubscribeAlarmsCommand.WatchList"/>:
    ///     <list type="bullet">
    ///         <item><description>
    ///             <c>Alarmmgr</c>, or <c>Unspecified</c> with an empty watch
    ///             list: the existing primary (alarmmgr) consumer only —
    ///             today's behavior.
    ///         </description></item>
    ///         <item><description>
    ///             <c>Subtag</c>: a <see cref="SubtagAlarmConsumer"/> only.
    ///         </description></item>
    ///         <item><description>
    ///             <c>Unspecified</c> with a non-empty watch list (auto): a
    ///             <see cref="FailoverAlarmConsumer"/> over the primary and a
    ///             subtag standby.
    ///         </description></item>
    ///     </list>
    /// </summary>
    private IMxAccessAlarmConsumer BuildConsumer(SubscribeAlarmsCommand command)
    {
        List<AlarmSubtagTarget> watchList = new List<AlarmSubtagTarget>(command.WatchList);

        if (command.ForcedMode == AlarmProviderMode.Subtag)
        {
            return BuildStandby(watchList);
        }

        if (command.ForcedMode == AlarmProviderMode.Unspecified && watchList.Count > 0)
        {
            IMxAccessAlarmConsumer primary = consumerFactory()
                ?? throw new InvalidOperationException("Alarm consumer factory returned null.");
            IMxAccessAlarmConsumer standby = BuildStandby(watchList);
            AlarmFailoverConfig? failoverConfig = command.Failover;
            FailoverSettings settings = new FailoverSettings(
                failoverConfig?.ConsecutiveFailureThreshold ?? 3,
                failoverConfig?.FailbackProbeIntervalSeconds ?? 30,
                failoverConfig?.FailbackStableProbes ?? 3);
            return new FailoverAlarmConsumer(primary, standby, settings);
        }

        // Alarmmgr, or Unspecified with an empty watch list — primary only.
        return consumerFactory()
            ?? throw new InvalidOperationException("Alarm consumer factory returned null.");
    }

    private IMxAccessAlarmConsumer BuildStandby(IReadOnlyList<AlarmSubtagTarget> watchList)
    {
        if (standbyFactory is not null)
        {
            return standbyFactory(watchList)
                ?? throw new InvalidOperationException("Standby alarm consumer factory returned null.");
        }

        if (comFactory is null)
        {
            throw new InvalidOperationException(
                "Subtag alarm consumer requires an IMxAccessComObjectFactory; the alarm command "
                + "handler was constructed without one and no standby factory was supplied.");
        }

        return new SubtagAlarmConsumer(new LmxSubtagAlarmSource(comFactory), watchList);
    }

    private void OnProviderModeChanged(object? sender, AlarmProviderModeChange change)
    {
        if (change is null) return;
        eventQueue.Enqueue(mapper.CreateOnAlarmProviderModeChanged(
            subscribeSessionId, change.Mode, change.Reason, change.HResult, change.AtUtc));
    }

    private void DetachProviderModeChanged()
    {
        if (failoverConsumer is not null && providerModeChangedHandler is not null)
        {
            try { failoverConsumer.ProviderModeChanged -= providerModeChangedHandler; }
            catch { /* swallow */ }
        }
        failoverConsumer = null;
        providerModeChangedHandler = null;
    }

    /// <inheritdoc />
    public void Unsubscribe()
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? toDispose;
        lock (syncRoot)
        {
            DetachProviderModeChanged();
            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,
        out bool snapshotTruncated)
    {
        threadAffinityCheck?.Invoke();
        AlarmDispatcher? d = GetDispatcherOrThrow();
        // The verdict rides out of the same call that produced the records, so
        // filtering below cannot separate it from the set it describes.
        IReadOnlyList<ActiveAlarmSnapshot> all = d.SnapshotActiveAlarms(out snapshotTruncated);
        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();
    }
}
