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

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

/// <summary>
///     In-process dispatcher that owns the lifetime of an
///     <see cref="IMxAccessAlarmConsumer"/> + <see cref="MxAccessAlarmEventSink"/>
///     pair, and wires the consumer's <c>AlarmTransitionEmitted</c> stream
///     onto the sink's <c>EnqueueTransition</c> path so transitions land on
///     the worker's <see cref="MxAccessEventQueue"/> as proto
///     <see cref="OnAlarmTransitionEvent"/> messages ready for IPC dispatch.
/// </summary>
/// <remarks>
///     <para>
///         The dispatcher carries the consumer→sink→queue pipeline. The
///         worker's IPC layer issues <c>SubscribeAlarmsCommand</c> /
///         <c>AcknowledgeAlarmCommand</c> / <c>QueryActiveAlarmsCommand</c>
///         through <see cref="AlarmCommandHandler"/>, which owns one
///         dispatcher per session.
///     </para>
///     <para>
///         Threading: <see cref="WnWrapAlarmConsumer"/> owns no internal
///         timer — the worker's STA drives polling via
///         <c>StaRuntime.InvokeAsync(() =&gt; PollOnce())</c>, so the
///         consumer's <c>AlarmTransitionEmitted</c> event fires on the STA.
///         The dispatcher is purely a pass-through, so it inherits that
///         thread. Fan-out into <c>EnqueueTransition</c> uses the
///         thread-safe <see cref="MxAccessEventQueue.Enqueue"/>.
///     </para>
/// </remarks>
public sealed class AlarmDispatcher : IDisposable
{
    private readonly IMxAccessAlarmConsumer consumer;
    private readonly MxAccessAlarmEventSink sink;
    private readonly string sessionId;
    private readonly EventHandler<MxAlarmTransitionEvent> handler;
    private bool disposed;

    /// <summary>Initializes a new alarm dispatcher for the given consumer, sink, and session ID.</summary>
    /// <param name="consumer">The alarm consumer.</param>
    /// <param name="sink">The alarm event sink.</param>
    /// <param name="sessionId">The session identifier.</param>
    public AlarmDispatcher(
        IMxAccessAlarmConsumer consumer,
        MxAccessAlarmEventSink sink,
        string sessionId)
    {
        this.consumer = consumer ?? throw new ArgumentNullException(nameof(consumer));
        this.sink = sink ?? throw new ArgumentNullException(nameof(sink));
        this.sessionId = sessionId ?? string.Empty;
        // Sink.Attach is the seam that propagates the session id onto the
        // proto SessionId field of every emitted MxEvent. Pass the consumer
        // as the "associated COM object" — sink ignores the object reference
        // for the alarm path, but the existing IMxAccessEventSink contract
        // requires a non-null first arg.
        this.sink.Attach(this.consumer, this.sessionId);
        this.handler = OnTransition;
        consumer.AlarmTransitionEmitted += handler;
    }

    /// <summary>
    ///     Begin polling the configured AVEVA alarm provider for
    ///     transitions. The supplied subscription expression follows the
    ///     canonical <c>\\&lt;machine&gt;\Galaxy!&lt;area&gt;</c> format.
    /// </summary>
    /// <param name="subscription">The subscription expression (e.g., <c>\\HOST\Galaxy!Area</c>).</param>
    public void Subscribe(string subscription)
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
        consumer.Subscribe(subscription);
    }

    /// <summary>
    ///     Forward an <c>AcknowledgeAlarm</c> request to the underlying
    ///     consumer's <c>AlarmAckByGUID</c>. Returns the AVEVA-native
    ///     status code (0 = success).
    /// </summary>
    /// <param name="alarmGuid">The alarm GUID.</param>
    /// <param name="ackComment">The acknowledgment comment.</param>
    /// <param name="ackOperatorName">The operator name.</param>
    /// <param name="ackOperatorNode">The operator node.</param>
    /// <param name="ackOperatorDomain">The operator domain.</param>
    /// <param name="ackOperatorFullName">The operator full name.</param>
    /// <returns>The AVEVA-native status code.</returns>
    public int Acknowledge(
        Guid alarmGuid,
        string ackComment,
        string ackOperatorName,
        string ackOperatorNode,
        string ackOperatorDomain,
        string ackOperatorFullName)
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
        return consumer.AcknowledgeByGuid(
            alarmGuid,
            ackComment,
            ackOperatorName,
            ackOperatorNode,
            ackOperatorDomain,
            ackOperatorFullName);
    }

    /// <summary>
    ///     Acknowledge an alarm by its (name, provider, group) tuple.
    ///     Routes to the consumer's <c>AcknowledgeByName</c> path which
    ///     maps to <c>wwAlarmConsumerClass.AlarmAckByName</c>.
    /// </summary>
    /// <param name="alarmName">The alarm name.</param>
    /// <param name="providerName">The provider name.</param>
    /// <param name="groupName">The group name.</param>
    /// <param name="ackComment">The acknowledgment comment.</param>
    /// <param name="ackOperatorName">The operator name.</param>
    /// <param name="ackOperatorNode">The operator node.</param>
    /// <param name="ackOperatorDomain">The operator domain.</param>
    /// <param name="ackOperatorFullName">The operator full name.</param>
    /// <returns>The AVEVA-native status code.</returns>
    public int AcknowledgeByName(
        string alarmName,
        string providerName,
        string groupName,
        string ackComment,
        string ackOperatorName,
        string ackOperatorNode,
        string ackOperatorDomain,
        string ackOperatorFullName)
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
        return consumer.AcknowledgeByName(
            alarmName,
            providerName,
            groupName,
            ackComment,
            ackOperatorName,
            ackOperatorNode,
            ackOperatorDomain,
            ackOperatorFullName);
    }

    /// <summary>
    ///     Drives a single synchronous poll of the underlying consumer.
    ///     Must be called on the STA thread that owns the wnwrap COM object.
    ///     No-op if the dispatcher has been disposed.
    /// </summary>
    public void PollOnce()
    {
        if (disposed) return;
        consumer.PollOnce();
    }

    /// <summary>
    ///     Snapshot the currently-active alarm set as
    ///     <see cref="ActiveAlarmSnapshot"/> protos for the
    ///     <c>QueryActiveAlarms</c> RPC's ConditionRefresh stream.
    /// </summary>
    /// <param name="truncated">
    ///     Receives whether the fetch behind the snapshot hit the per-fetch cap,
    ///     so the returned set may omit active alarms. Forwarded from the single
    ///     atomic consumer read, and stamped onto every returned record as
    ///     <c>ActiveAlarmSnapshot.FromTruncatedSnapshot</c>. Also returned
    ///     separately because a snapshot of zero records still has to report it.
    /// </param>
    /// <returns>The currently active alarm snapshots.</returns>
    public IReadOnlyList<ActiveAlarmSnapshot> SnapshotActiveAlarms(out bool truncated)
    {
        if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
        // One consumer call yields the records and the verdict together, under a
        // single acquisition of the consumer's snapshot lock. Reading them
        // separately would let a poll interleave and pair a stale not-truncated
        // verdict with a capped snapshot — a set that reads complete while
        // missing actives. The atomicity is structural here, not a consequence of
        // the STA happening to serialize the two calls.
        IReadOnlyList<MxAlarmSnapshotRecord> records = consumer.SnapshotActiveAlarms(out truncated);
        if (records.Count == 0) return Array.Empty<ActiveAlarmSnapshot>();
        List<ActiveAlarmSnapshot> snapshots = new List<ActiveAlarmSnapshot>(records.Count);
        foreach (MxAlarmSnapshotRecord record in records)
        {
            snapshots.Add(MapToSnapshot(record, truncated));
        }
        return snapshots;
    }

    private void OnTransition(object? sender, MxAlarmTransitionEvent transition)
    {
        if (disposed) return;
        if (transition is null) return;

        MxAlarmSnapshotRecord record = transition.Record;
        AlarmTransitionKind kind = AlarmRecordTransitionMapper.MapTransition(
            transition.PreviousState, record.State);
        if (kind == AlarmTransitionKind.Unspecified) return;

        string fullReference = AlarmRecordTransitionMapper.ComposeFullReference(
            record.ProviderName, record.Group, record.TagName);

        sink.EnqueueTransition(
            alarmFullReference: fullReference,
            sourceObjectReference: record.TagName,
            alarmTypeName: record.Type,
            transitionKind: kind,
            severity: record.Priority,
            originalRaiseTimestampUtc: null,
            transitionTimestampUtc: record.TransitionTimestampUtc,
            operatorUser: record.OperatorName,
            operatorComment: record.AlarmComment,
            category: record.Group,
            description: string.Empty,
            degraded: record.Degraded);
    }

    private static ActiveAlarmSnapshot MapToSnapshot(MxAlarmSnapshotRecord record, bool truncated)
    {
        ActiveAlarmSnapshot snapshot = new ActiveAlarmSnapshot
        {
            AlarmFullReference = AlarmRecordTransitionMapper.ComposeFullReference(
                record.ProviderName, record.Group, record.TagName),
            SourceObjectReference = record.TagName,
            AlarmTypeName = record.Type,
            CurrentState = MapConditionState(record.State),
            Severity = record.Priority,
            OperatorUser = record.OperatorName,
            OperatorComment = record.AlarmComment,
            Category = record.Group,
            Description = string.Empty,
            Degraded = record.Degraded,
            SourceProvider = record.Degraded ? AlarmProviderMode.Subtag : AlarmProviderMode.Alarmmgr,
            // Set-level status, stamped identically on every record of the
            // snapshot: QueryActiveAlarms streams bare ActiveAlarmSnapshot
            // messages with no envelope to hang it off. Independent of
            // Degraded above — that is about this record's provider, this is
            // about whether the set it belongs to is complete.
            FromTruncatedSnapshot = truncated,
        };
        if (record.TransitionTimestampUtc != DateTime.MinValue)
        {
            snapshot.LastTransitionTimestamp =
                Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
                    DateTime.SpecifyKind(record.TransitionTimestampUtc, DateTimeKind.Utc));
        }
        return snapshot;
    }

    private static AlarmConditionState MapConditionState(MxAlarmStateKind state)
    {
        // The proto's AlarmConditionState only distinguishes Active /
        // ActiveAcked / Inactive — both Rtn states collapse to Inactive
        // (the ack-vs-unack distinction on a cleared alarm is not exposed
        // through OPC UA's Part 9 condition state model anyway).
        return state switch
        {
            MxAlarmStateKind.UnackAlm => AlarmConditionState.Active,
            MxAlarmStateKind.AckAlm => AlarmConditionState.ActiveAcked,
            MxAlarmStateKind.UnackRtn => AlarmConditionState.Inactive,
            MxAlarmStateKind.AckRtn => AlarmConditionState.Inactive,
            _ => AlarmConditionState.Unspecified,
        };
    }

    /// <summary>Gets the session ID.</summary>
    public string SessionId => sessionId;

    /// <inheritdoc />
    public void Dispose()
    {
        if (disposed) return;
        disposed = true;
        try { consumer.AlarmTransitionEmitted -= handler; } catch { /* swallow */ }
        try { sink.Detach(); } catch { /* swallow */ }
        try { consumer.Dispose(); } catch { /* swallow */ }
    }
}
