using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Alarms;

/// <summary>
///     Subscribes to the four Galaxy alarm attributes (<c>.InAlarm</c>, <c>.Priority</c>,
///     <c>.DescAttrName</c>, <c>.Acked</c>) per alarm-bearing attribute discovered during
///     <c>DiscoverAsync</c>. Maintains one <see cref="AlarmState"/> per alarm, raises
///     <see cref="AlarmTransition"/> on lifecycle transitions (Active / Unacknowledged /
///     Acknowledged / Inactive). Ack path writes <c>.AckMsg</c>. Pure-logic state machine
///     with delegate-based subscribe/write so it's testable against in-memory fakes.
/// </summary>
/// <remarks>
///     Transitions emitted (OPC UA Part 9 alarm lifecycle, simplified for the Galaxy model):
///     <list type="bullet">
///         <item><c>Active</c> — InAlarm false → true. Default to Unacknowledged.</item>
///         <item><c>Acknowledged</c> — Acked false → true while InAlarm is still true.</item>
///         <item><c>Inactive</c> — InAlarm true → false. If still unacknowledged the alarm
///         is marked latched-inactive-unack; next Ack transitions straight to Inactive.</item>
///     </list>
/// </remarks>
public sealed class GalaxyAlarmTracker : IDisposable
{
    public const string InAlarmAttr = ".InAlarm";
    public const string PriorityAttr = ".Priority";
    public const string DescAttrNameAttr = ".DescAttrName";
    public const string AckedAttr = ".Acked";
    public const string AckMsgAttr = ".AckMsg";

    private readonly Func<string, Action<string, Vtq>, Task> _subscribe;
    private readonly Func<string, Task> _unsubscribe;
    private readonly Func<string, object, Task<bool>> _write;
    private readonly Func<DateTime> _clock;

    // Alarm tag (attribute full ref, e.g. "Tank.Level.HiHi") → state.
    private readonly ConcurrentDictionary<string, AlarmState> _alarms =
        new(StringComparer.OrdinalIgnoreCase);

    // Reverse lookup: probed tag (".InAlarm" etc.) → owning alarm tag.
    private readonly ConcurrentDictionary<string, (string AlarmTag, AlarmField Field)> _probeToAlarm =
        new(StringComparer.OrdinalIgnoreCase);

    private bool _disposed;

    public event EventHandler<AlarmTransition>? TransitionRaised;

    public GalaxyAlarmTracker(
        Func<string, Action<string, Vtq>, Task> subscribe,
        Func<string, Task> unsubscribe,
        Func<string, object, Task<bool>> write)
        : this(subscribe, unsubscribe, write, () => DateTime.UtcNow) { }

    internal GalaxyAlarmTracker(
        Func<string, Action<string, Vtq>, Task> subscribe,
        Func<string, Task> unsubscribe,
        Func<string, object, Task<bool>> write,
        Func<DateTime> clock)
    {
        _subscribe = subscribe ?? throw new ArgumentNullException(nameof(subscribe));
        _unsubscribe = unsubscribe ?? throw new ArgumentNullException(nameof(unsubscribe));
        _write = write ?? throw new ArgumentNullException(nameof(write));
        _clock = clock ?? throw new ArgumentNullException(nameof(clock));
    }

    public int TrackedAlarmCount => _alarms.Count;

    /// <summary>
    ///     Advise the four alarm attributes for <paramref name="alarmTag"/>. Idempotent —
    ///     repeat calls for the same alarm tag are a no-op. Subscribe failure for any of the
    ///     four rolls back the alarm entry so a stale callback cannot promote a phantom.
    /// </summary>
    public async Task TrackAsync(string alarmTag)
    {
        if (_disposed || string.IsNullOrWhiteSpace(alarmTag)) return;
        if (_alarms.ContainsKey(alarmTag)) return;

        var state = new AlarmState { AlarmTag = alarmTag };
        if (!_alarms.TryAdd(alarmTag, state)) return;

        var probes = new[]
        {
            (Tag: alarmTag + InAlarmAttr, Field: AlarmField.InAlarm),
            (Tag: alarmTag + PriorityAttr, Field: AlarmField.Priority),
            (Tag: alarmTag + DescAttrNameAttr, Field: AlarmField.DescAttrName),
            (Tag: alarmTag + AckedAttr, Field: AlarmField.Acked),
        };

        foreach (var p in probes)
        {
            _probeToAlarm[p.Tag] = (alarmTag, p.Field);
        }

        try
        {
            foreach (var p in probes)
            {
                await _subscribe(p.Tag, OnProbeCallback).ConfigureAwait(false);
            }
        }
        catch
        {
            // Rollback so a partial advise doesn't leak state.
            _alarms.TryRemove(alarmTag, out _);
            foreach (var p in probes)
            {
                _probeToAlarm.TryRemove(p.Tag, out _);
                try { await _unsubscribe(p.Tag).ConfigureAwait(false); } catch { }
            }
            throw;
        }
    }

    /// <summary>
    ///     Drop every tracked alarm. Unadvises all 4 probes per alarm as best-effort.
    /// </summary>
    public async Task ClearAsync()
    {
        _alarms.Clear();
        foreach (var kv in _probeToAlarm.ToList())
        {
            _probeToAlarm.TryRemove(kv.Key, out _);
            try { await _unsubscribe(kv.Key).ConfigureAwait(false); } catch { }
        }
    }

    /// <summary>
    ///     Operator ack — write the comment text into <c>&lt;alarmTag&gt;.AckMsg</c>.
    ///     Returns false when the runtime reports the write failed.
    /// </summary>
    public Task<bool> AcknowledgeAsync(string alarmTag, string comment)
    {
        if (_disposed || string.IsNullOrWhiteSpace(alarmTag))
            return Task.FromResult(false);
        return _write(alarmTag + AckMsgAttr, comment ?? string.Empty);
    }

    /// <summary>
    ///     Subscription callback entry point. Exposed for tests and for the Backend to route
    ///     fan-out callbacks through. Runs the state machine and fires TransitionRaised
    ///     outside the lock.
    /// </summary>
    public void OnProbeCallback(string probeTag, Vtq vtq)
    {
        if (_disposed) return;
        if (!_probeToAlarm.TryGetValue(probeTag, out var link)) return;
        if (!_alarms.TryGetValue(link.AlarmTag, out var state)) return;

        AlarmTransition? transition = null;
        var now = _clock();

        lock (state.Lock)
        {
            switch (link.Field)
            {
                case AlarmField.InAlarm:
                {
                    var wasActive = state.InAlarm;
                    var isActive = vtq.Value is bool b && b;
                    state.InAlarm = isActive;
                    state.LastUpdateUtc = now;
                    if (!wasActive && isActive)
                    {
                        state.Acked = false;
                        state.LastTransitionUtc = now;
                        transition = new AlarmTransition(state.AlarmTag, AlarmStateTransition.Active, state.Priority, state.DescAttrName, now);
                    }
                    else if (wasActive && !isActive)
                    {
                        state.LastTransitionUtc = now;
                        transition = new AlarmTransition(state.AlarmTag, AlarmStateTransition.Inactive, state.Priority, state.DescAttrName, now);
                    }
                    break;
                }
                case AlarmField.Priority:
                    if (vtq.Value is int pi) state.Priority = pi;
                    else if (vtq.Value is short ps) state.Priority = ps;
                    else if (vtq.Value is long pl && pl <= int.MaxValue) state.Priority = (int)pl;
                    state.LastUpdateUtc = now;
                    break;
                case AlarmField.DescAttrName:
                    state.DescAttrName = vtq.Value as string;
                    state.LastUpdateUtc = now;
                    break;
                case AlarmField.Acked:
                {
                    var wasAcked = state.Acked;
                    var isAcked = vtq.Value is bool b && b;
                    state.Acked = isAcked;
                    state.LastUpdateUtc = now;
                    // Fire Acknowledged only when transitioning false→true. Don't fire on initial
                    // subscribe callback (wasAcked==isAcked in that case because the state starts
                    // with Acked=false and the initial probe is usually true for an un-active alarm).
                    if (!wasAcked && isAcked && state.InAlarm)
                    {
                        state.LastTransitionUtc = now;
                        transition = new AlarmTransition(state.AlarmTag, AlarmStateTransition.Acknowledged, state.Priority, state.DescAttrName, now);
                    }
                    break;
                }
            }
        }

        if (transition is { } t)
        {
            TransitionRaised?.Invoke(this, t);
        }
    }

    public IReadOnlyList<AlarmSnapshot> SnapshotStates()
    {
        return _alarms.Values.Select(s =>
        {
            lock (s.Lock)
                return new AlarmSnapshot(s.AlarmTag, s.InAlarm, s.Acked, s.Priority, s.DescAttrName);
        }).ToList();
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        _alarms.Clear();
        _probeToAlarm.Clear();
    }

    private sealed class AlarmState
    {
        public readonly object Lock = new();
        public string AlarmTag = "";
        public bool InAlarm;
        public bool Acked = true; // default ack'd so first false→true on subscribe doesn't misfire
        public int Priority;
        public string? DescAttrName;
        public DateTime LastUpdateUtc;
        public DateTime LastTransitionUtc;
    }

    private enum AlarmField { InAlarm, Priority, DescAttrName, Acked }
}

public enum AlarmStateTransition { Active, Acknowledged, Inactive }

public sealed record AlarmTransition(
    string AlarmTag,
    AlarmStateTransition Transition,
    int Priority,
    string? DescAttrName,
    DateTime AtUtc);

public sealed record AlarmSnapshot(
    string AlarmTag,
    bool InAlarm,
    bool Acked,
    int Priority,
    string? DescAttrName);
