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

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

/// <summary>
///     Synthesizes alarm transitions from raw subtag value changes when the
///     native alarmmgr (wnwrap) consumer is unavailable. Each configured
///     <see cref="AlarmSubtagTarget"/> contributes an <c>.active</c>,
///     <c>.acked</c>, optional <c>.priority</c>, and optional ack-comment
///     subtag; the machine tracks per-alarm state and emits
///     <see cref="MxAlarmTransitionEvent"/> records mirroring the wnwrap
///     consumer's UNACK_ALM / ACK_ALM / UNACK_RTN / ACK_RTN transitions.
///     This type performs no COM work and is fully unit-testable.
/// </summary>
/// <remarks>
///     This type has STA affinity: it is mutated and read on the worker STA
///     thread only and is not thread-safe. The per-target
///     <see cref="AlarmSubtagTarget.AckCommentSubtag"/> is a write-only ack
///     target and is deliberately not bound for observation. The synthesized
///     <see cref="MxAlarmSnapshotRecord.AlarmGuid"/> is left at its default
///     value: in subtag mode acks are correlated by name/comment rather than
///     by GUID, and a synthetic GUID is assigned by a later component.
/// </remarks>
public sealed class SubtagAlarmStateMachine
{
    private static readonly IReadOnlyList<MxAlarmTransitionEvent> NoEvents = Array.Empty<MxAlarmTransitionEvent>();

    private readonly Dictionary<string, AlarmState> _statesByReference = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, SubtagBinding> _bindingsByAddress = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>Initializes the machine from the configured alarm subtag targets.</summary>
    /// <param name="targets">The alarm targets whose subtags will be observed.</param>
    public SubtagAlarmStateMachine(IEnumerable<AlarmSubtagTarget> targets)
    {
        if (targets is null)
        {
            throw new ArgumentNullException(nameof(targets));
        }

        foreach (AlarmSubtagTarget target in targets)
        {
            // Guard duplicate references symmetrically with the dup-address guard
            // in Bind: two watch-list entries that share an AlarmFullReference but
            // differ in subtag addresses would otherwise silently overwrite the
            // earlier _statesByReference entry while its addresses stay bound to an
            // orphaned (and therefore invisible) AlarmState, producing silently
            // inconsistent synthesized state. Fail fast at subscribe time instead.
            string reference = target.AlarmFullReference ?? string.Empty;
            if (_statesByReference.ContainsKey(reference))
            {
                throw new ArgumentException(
                    $"Duplicate alarm full reference '{reference}' is bound to more than one alarm target.",
                    nameof(targets));
            }

            var state = new AlarmState(target);
            _statesByReference[reference] = state;

            Bind(target.ActiveSubtag, state, SubtagRole.Active);
            Bind(target.AckedSubtag, state, SubtagRole.Acked);
            Bind(target.PrioritySubtag, state, SubtagRole.Priority);
        }
    }

    /// <summary>
    ///     Applies a single subtag value change and returns any synthesized
    ///     transitions. Unknown addresses and no-op changes return an empty list.
    /// </summary>
    /// <param name="itemAddress">The subtag address whose value changed.</param>
    /// <param name="value">The new value (bool, numeric, or string).</param>
    /// <param name="timestampUtc">The UTC timestamp of the change.</param>
    /// <returns>The synthesized alarm transitions, if any.</returns>
    public IReadOnlyList<MxAlarmTransitionEvent> Apply(string itemAddress, object? value, DateTime timestampUtc)
    {
        if (itemAddress is null || !_bindingsByAddress.TryGetValue(itemAddress, out SubtagBinding binding))
        {
            return NoEvents;
        }

        AlarmState state = binding.State;
        switch (binding.Role)
        {
            case SubtagRole.Active:
                return ApplyActive(state, CoerceBool(value), timestampUtc);
            case SubtagRole.Acked:
                return ApplyAcked(state, CoerceBool(value), timestampUtc);
            case SubtagRole.Priority:
                state.Priority = CoerceInt(value, state.Priority);
                return NoEvents;
            default:
                return NoEvents;
        }
    }

    /// <summary>
    ///     Returns one snapshot record per currently-active alarm, with state
    ///     reflecting whether the alarm has been acknowledged.
    /// </summary>
    /// <returns>The active alarm snapshot records.</returns>
    public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActive()
    {
        var records = new List<MxAlarmSnapshotRecord>();
        foreach (AlarmState state in _statesByReference.Values)
        {
            if (!state.Active)
            {
                continue;
            }

            MxAlarmStateKind kind = state.Acked ? MxAlarmStateKind.AckAlm : MxAlarmStateKind.UnackAlm;
            records.Add(state.BuildRecord(kind, state.FirstRaiseUtc));
        }

        return records;
    }

    private IReadOnlyList<MxAlarmTransitionEvent> ApplyActive(AlarmState state, bool newActive, DateTime timestampUtc)
    {
        if (newActive == state.Active)
        {
            return NoEvents;
        }

        if (newActive)
        {
            // false -> true: alarm raised, initially unacknowledged.
            MxAlarmStateKind previous = state.DerivedState();
            state.Active = true;
            state.Acked = false;
            state.AckedDuringEpisode = false;
            state.FirstRaiseUtc = timestampUtc;
            return Single(state, MxAlarmStateKind.UnackAlm, previous, timestampUtc);
        }
        else
        {
            // true -> false: alarm returned to normal. The return kind reflects
            // whether the alarm was acknowledged at any point during the active
            // episode, not just its instantaneous Acked flag (see remarks).
            MxAlarmStateKind previous = state.DerivedState();
            MxAlarmStateKind kind = state.AckedDuringEpisode ? MxAlarmStateKind.AckRtn : MxAlarmStateKind.UnackRtn;
            state.Active = false;
            state.Acked = false;
            state.AckedDuringEpisode = false;
            return Single(state, kind, previous, timestampUtc);
        }
    }

    /// <remarks>
    ///     <see cref="AlarmState.AckedDuringEpisode"/> latches that the alarm was
    ///     acknowledged at some point while active. MXAccess does not guarantee
    ///     that the acked subtag update arrives before the active=false update on
    ///     clear, so an out-of-order un-ack (acked true->false) must not lose the
    ///     fact that the episode was acknowledged; the latch ensures the clear
    ///     still emits ACK_RTN rather than UNACK_RTN.
    /// </remarks>
    private IReadOnlyList<MxAlarmTransitionEvent> ApplyAcked(AlarmState state, bool newAcked, DateTime timestampUtc)
    {
        if (newAcked == state.Acked)
        {
            return NoEvents;
        }

        state.Acked = newAcked;

        // Only a true ack of a currently-active alarm produces an ACK_ALM
        // transition; the latch records that the episode was acked at some point.
        if (newAcked && state.Active)
        {
            state.AckedDuringEpisode = true;
            return Single(state, MxAlarmStateKind.AckAlm, MxAlarmStateKind.UnackAlm, timestampUtc);
        }

        // acked true->false while active: do NOT clear the latch (the episode was
        // already acked); emit nothing.
        return NoEvents;
    }

    private static IReadOnlyList<MxAlarmTransitionEvent> Single(
        AlarmState state,
        MxAlarmStateKind kind,
        MxAlarmStateKind previous,
        DateTime timestampUtc)
    {
        return new[]
        {
            new MxAlarmTransitionEvent
            {
                Record = state.BuildRecord(kind, timestampUtc),
                PreviousState = previous,
            },
        };
    }

    private void Bind(string? address, AlarmState state, SubtagRole role)
    {
        if (!string.IsNullOrEmpty(address))
        {
            if (_bindingsByAddress.ContainsKey(address!))
            {
                throw new ArgumentException(
                    $"Duplicate subtag item address '{address}' is bound to more than one alarm target.",
                    nameof(address));
            }

            _bindingsByAddress[address!] = new SubtagBinding(state, role);
        }
    }

    private static bool CoerceBool(object? value)
    {
        switch (value)
        {
            case null:
                return false;
            case bool b:
                return b;
            case string s:
                if (bool.TryParse(s, out bool parsedBool))
                {
                    return parsedBool;
                }

                if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out double parsedNum))
                {
                    return parsedNum != 0d;
                }

                return false;
            default:
                try
                {
                    return Convert.ToDouble(value, CultureInfo.InvariantCulture) != 0d;
                }
                catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
                {
                    return false;
                }
        }
    }

    private static int CoerceInt(object? value, int fallback)
    {
        switch (value)
        {
            case null:
                return fallback;
            case int i:
                return i;
            case string s when int.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out int parsed):
                return parsed;
            case string:
                return fallback;
            default:
                try
                {
                    return Convert.ToInt32(value, CultureInfo.InvariantCulture);
                }
                catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
                {
                    return fallback;
                }
        }
    }

    private enum SubtagRole
    {
        Active,
        Acked,
        Priority,
    }

    private readonly struct SubtagBinding
    {
        /// <summary>Pairs a bound subtag address with the alarm state it feeds and the role it plays.</summary>
        /// <param name="state">The alarm state that this subtag's value changes update.</param>
        /// <param name="role">The role the bound subtag plays (active, acked, or priority).</param>
        public SubtagBinding(AlarmState state, SubtagRole role)
        {
            State = state;
            Role = role;
        }

        /// <summary>Gets the alarm state that this subtag's value changes update.</summary>
        public AlarmState State { get; }

        /// <summary>Gets the role the bound subtag plays (active, acked, or priority).</summary>
        public SubtagRole Role { get; }
    }

    private sealed class AlarmState
    {
        private readonly string _providerName;
        private readonly string _group;
        private readonly string _tagName;

        /// <summary>Initializes the tracked state for one alarm target, deriving its provider/group/tag name.</summary>
        /// <param name="target">The configured alarm subtag target this state tracks.</param>
        public AlarmState(AlarmSubtagTarget target)
        {
            (_providerName, _group, _tagName) = DeriveNameParts(target);
        }

        /// <summary>Gets or sets whether the alarm is currently active (raised).</summary>
        public bool Active { get; set; }

        /// <summary>Gets or sets whether the alarm is currently acknowledged.</summary>
        public bool Acked { get; set; }

        /// <summary>
        ///     Latches that the alarm was acknowledged at some point during the
        ///     current active episode, so a clear emits ACK_RTN even when the
        ///     un-ack subtag arrives before the active=false update.
        /// </summary>
        public bool AckedDuringEpisode { get; set; }

        /// <summary>Gets or sets the alarm's priority, as last reported by the priority subtag.</summary>
        public int Priority { get; set; }

        /// <summary>Gets or sets the UTC timestamp at which the alarm was most recently raised.</summary>
        public DateTime FirstRaiseUtc { get; set; }

        /// <summary>Derives the current alarm state from the tracked flags, before a transition is applied.</summary>
        /// <returns>The alarm state kind (unspecified, unacknowledged, or acknowledged) implied by the tracked flags.</returns>
        public MxAlarmStateKind DerivedState()
        {
            if (!Active)
            {
                return MxAlarmStateKind.Unspecified;
            }

            return Acked ? MxAlarmStateKind.AckAlm : MxAlarmStateKind.UnackAlm;
        }

        /// <summary>Builds a snapshot record for this alarm at the given state kind and timestamp.</summary>
        /// <param name="kind">The alarm state kind to record.</param>
        /// <param name="timestampUtc">The UTC timestamp of the transition being recorded.</param>
        /// <returns>The snapshot record describing this alarm's provider, name, priority, state, and timestamp.</returns>
        public MxAlarmSnapshotRecord BuildRecord(MxAlarmStateKind kind, DateTime timestampUtc)
        {
            return new MxAlarmSnapshotRecord
            {
                ProviderName = _providerName,
                Group = _group,
                TagName = _tagName,
                Priority = Priority,
                State = kind,
                TransitionTimestampUtc = timestampUtc,
            };
        }

        /// <summary>
        ///     Splits an alarm full reference such as <c>Galaxy!Area.Tank01.Level.HiHi</c>
        ///     into provider (<c>Galaxy</c>), group/area (<c>Area</c>), and the object-rooted
        ///     tag name (<c>Tank01.Level.HiHi</c>). The tag name is anchored on the target's
        ///     <see cref="AlarmSubtagTarget.SourceObjectReference"/> when present, so any
        ///     leading provider and area segments are stripped.
        /// </summary>
        private static (string ProviderName, string Group, string TagName) DeriveNameParts(AlarmSubtagTarget target)
        {
            string reference = target.AlarmFullReference ?? string.Empty;

            string providerName = string.Empty;
            string afterBang = reference;
            int bang = reference.IndexOf('!');
            if (bang >= 0)
            {
                providerName = reference.Substring(0, bang);
                afterBang = reference.Substring(bang + 1);
            }

            string tagName = afterBang;
            string source = target.SourceObjectReference ?? string.Empty;
            string group = string.Empty;
            if (source.Length > 0)
            {
                // Match the source on dot-delimited segment boundaries so a source
                // such as "Tank" does not match mid-token inside "Tank01" and a
                // source "Area" does not match inside "TankArea".
                string[] segments = afterBang.Split('.');
                int segmentIndex = -1;
                for (int i = 0; i < segments.Length; i++)
                {
                    if (string.Equals(segments[i], source, StringComparison.OrdinalIgnoreCase))
                    {
                        segmentIndex = i;
                        break;
                    }
                }

                if (segmentIndex > 0)
                {
                    // Everything before the object is the area/group; the object
                    // and everything after it is the object-rooted tag name.
                    group = string.Join(".", segments, 0, segmentIndex);
                    tagName = string.Join(".", segments, segmentIndex, segments.Length - segmentIndex);
                }
                else
                {
                    // segmentIndex == 0 (source is the first segment) or not found:
                    // no leading area to strip, so the tag name is the whole afterBang.
                    tagName = afterBang;
                }
            }

            return (providerName, group, tagName);
        }
    }
}
