using System.Collections.Immutable;

namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

/// <summary>
///     Pure functions for OPC UA Part 9 alarm-condition state transitions. Input = the
///     current <see cref="AlarmConditionState"/> + the event; output = the new state +
///     optional emission hint. The engine calls these; persistence happens around them.
/// </summary>
/// <remarks>
///     <para>
///         No instance state, no I/O, no mutation of the input record. Every transition
///         returns a fresh record. Makes the state machine trivially unit-testable —
///         tests assert on (input, event) -> (output) without standing anything else up.
///     </para>
///     <para>
///         Two invariants the machine enforces:
///         (1) Disabled alarms never transition ActiveState / AckedState / ConfirmedState
///         — all predicate evaluations while disabled produce a no-op result and a
///         diagnostic log line. Re-enable restores normal flow with ActiveState
///         re-derived from the next predicate evaluation.
///         (2) Shelved alarms (OneShot / Timed) don't fire active transitions to
///         subscribers, but the state record still advances so that when shelving
///         expires the ActiveState reflects current reality. OneShot expires on the
///         next clear; Timed expires at <see cref="ShelvingState.UnshelveAtUtc"/>.
///     </para>
/// </remarks>
public static class Part9StateMachine
{
    /// <summary>
    ///     Apply a predicate re-evaluation result. Handles activation, clearing,
    ///     branch-stack increment when a new active arrives while prior active is
    ///     still un-acked, and shelving suppression.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="predicateTrue">Whether the predicate is true.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyPredicate(
        AlarmConditionState current,
        bool predicateTrue,
        DateTime nowUtc)
    {
        if (current.Enabled == AlarmEnabledState.Disabled)
            return TransitionResult.NoOp(current, "disabled — predicate result ignored");

        // Expire timed shelving if the configured clock has passed.
        var shelving = MaybeExpireShelving(current.Shelving, nowUtc);
        var stateWithShelving = current with { Shelving = shelving };

        // Shelved alarms still update state but skip event emission.
        var shelved = shelving.Kind != ShelvingKind.Unshelved;

        if (predicateTrue && current.Active == AlarmActiveState.Inactive)
        {
            // Inactive -> Active transition.
            // OneShotShelving is consumed on the NEXT clear, not activation — so we
            // still suppress this transition's emission.
            var next = stateWithShelving with
            {
                Active = AlarmActiveState.Active,
                Acked = AlarmAckedState.Unacknowledged,
                Confirmed = AlarmConfirmedState.Unconfirmed,
                LastActiveUtc = nowUtc,
                LastTransitionUtc = nowUtc,
            };
            return new TransitionResult(next, shelved ? EmissionKind.Suppressed : EmissionKind.Activated);
        }

        if (!predicateTrue && current.Active == AlarmActiveState.Active)
        {
            // Active -> Inactive transition.
            var next = stateWithShelving with
            {
                Active = AlarmActiveState.Inactive,
                LastClearedUtc = nowUtc,
                LastTransitionUtc = nowUtc,
                // OneShotShelving expires on clear — resetting here so the next
                // activation fires normally.
                Shelving = shelving.Kind == ShelvingKind.OneShot
                    ? ShelvingState.Unshelved
                    : shelving,
            };
            return new TransitionResult(next, shelved ? EmissionKind.Suppressed : EmissionKind.Cleared);
        }

        // Predicate matches current Active — no state change beyond possible shelving
        // expiry.
        return new TransitionResult(stateWithShelving, EmissionKind.None);
    }

    /// <summary>Operator acknowledges the currently-active transition.</summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user who is acknowledging.</param>
    /// <param name="comment">An optional comment.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyAcknowledge(
        AlarmConditionState current,
        string user,
        string? comment,
        DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user))
            throw new ArgumentException("User identity required for audit.", nameof(user));

        if (current.Acked == AlarmAckedState.Acknowledged)
            return TransitionResult.NoOp(current, "already acknowledged");

        var audit = AppendComment(current.Comments, nowUtc, user, "Acknowledge", comment);
        var next = current with
        {
            Acked = AlarmAckedState.Acknowledged,
            LastAckUtc = nowUtc,
            LastAckUser = user,
            LastAckComment = comment,
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Acknowledged);
    }

    /// <summary>Operator confirms the cleared transition. Part 9 requires confirm after clear for retain-flag alarms.</summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user who is confirming.</param>
    /// <param name="comment">An optional comment.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyConfirm(
        AlarmConditionState current,
        string user,
        string? comment,
        DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user))
            throw new ArgumentException("User identity required for audit.", nameof(user));

        if (current.Confirmed == AlarmConfirmedState.Confirmed)
            return TransitionResult.NoOp(current, "already confirmed");

        var audit = AppendComment(current.Comments, nowUtc, user, "Confirm", comment);
        var next = current with
        {
            Confirmed = AlarmConfirmedState.Confirmed,
            LastConfirmUtc = nowUtc,
            LastConfirmUser = user,
            LastConfirmComment = comment,
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Confirmed);
    }

    /// <summary>
    ///     Applies a one-shot shelving action.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user applying the shelving.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyOneShotShelve(
        AlarmConditionState current, string user, DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("User required.", nameof(user));
        if (current.Shelving.Kind == ShelvingKind.OneShot)
            return TransitionResult.NoOp(current, "already one-shot shelved");

        var audit = AppendComment(current.Comments, nowUtc, user, "ShelveOneShot", null);
        var next = current with
        {
            Shelving = new ShelvingState(ShelvingKind.OneShot, null),
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Shelved);
    }

    /// <summary>
    ///     Applies a timed shelving action.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user applying the shelving.</param>
    /// <param name="unshelveAtUtc">The UTC time at which the alarm should be unshelved.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyTimedShelve(
        AlarmConditionState current, string user, DateTime unshelveAtUtc, DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("User required.", nameof(user));
        if (unshelveAtUtc <= nowUtc)
            throw new ArgumentOutOfRangeException(nameof(unshelveAtUtc), "Unshelve time must be in the future.");

        var audit = AppendComment(current.Comments, nowUtc, user, "ShelveTimed",
            $"UnshelveAtUtc={unshelveAtUtc:O}");
        var next = current with
        {
            Shelving = new ShelvingState(ShelvingKind.Timed, unshelveAtUtc),
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Shelved);
    }

    /// <summary>
    ///     Applies an unshelving action.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user applying the unshelving.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyUnshelve(AlarmConditionState current, string user, DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("User required.", nameof(user));
        if (current.Shelving.Kind == ShelvingKind.Unshelved)
            return TransitionResult.NoOp(current, "not shelved");

        var audit = AppendComment(current.Comments, nowUtc, user, "Unshelve", null);
        var next = current with
        {
            Shelving = ShelvingState.Unshelved,
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Unshelved);
    }

    /// <summary>
    ///     Applies an enable action.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user enabling the alarm.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyEnable(AlarmConditionState current, string user, DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("User required.", nameof(user));
        if (current.Enabled == AlarmEnabledState.Enabled)
            return TransitionResult.NoOp(current, "already enabled");

        var audit = AppendComment(current.Comments, nowUtc, user, "Enable", null);
        var next = current with
        {
            Enabled = AlarmEnabledState.Enabled,
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Enabled);
    }

    /// <summary>
    ///     Applies a disable action.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user disabling the alarm.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyDisable(AlarmConditionState current, string user, DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("User required.", nameof(user));
        if (current.Enabled == AlarmEnabledState.Disabled)
            return TransitionResult.NoOp(current, "already disabled");

        var audit = AppendComment(current.Comments, nowUtc, user, "Disable", null);
        var next = current with
        {
            Enabled = AlarmEnabledState.Disabled,
            LastTransitionUtc = nowUtc,
            Comments = audit,
        };
        return new TransitionResult(next, EmissionKind.Disabled);
    }

    /// <summary>
    ///     Applies an add comment action.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="user">The user adding the comment.</param>
    /// <param name="text">The comment text.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyAddComment(
        AlarmConditionState current, string user, string text, DateTime nowUtc)
    {
        if (string.IsNullOrWhiteSpace(user)) throw new ArgumentException("User required.", nameof(user));
        if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Comment text required.", nameof(text));

        var audit = AppendComment(current.Comments, nowUtc, user, "AddComment", text);
        var next = current with { Comments = audit };
        return new TransitionResult(next, EmissionKind.CommentAdded);
    }

    /// <summary>
    ///     Re-evaluate whether a currently timed-shelved alarm has expired. Returns
    ///     the (possibly unshelved) state + emission hint so the engine knows to
    ///     publish an Unshelved event at the right moment.
    /// </summary>
    /// <param name="current">The current alarm condition state.</param>
    /// <param name="nowUtc">The current UTC time.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult ApplyShelvingCheck(AlarmConditionState current, DateTime nowUtc)
    {
        if (current.Shelving.Kind != ShelvingKind.Timed) return TransitionResult.None(current);
        if (current.Shelving.UnshelveAtUtc is DateTime t && nowUtc >= t)
        {
            var audit = AppendComment(current.Comments, nowUtc, "system", "AutoUnshelve",
                $"Timed shelving expired at {nowUtc:O}");
            var next = current with
            {
                Shelving = ShelvingState.Unshelved,
                LastTransitionUtc = nowUtc,
                Comments = audit,
            };
            return new TransitionResult(next, EmissionKind.Unshelved);
        }
        return TransitionResult.None(current);
    }

    private static ShelvingState MaybeExpireShelving(ShelvingState s, DateTime nowUtc)
    {
        if (s.Kind != ShelvingKind.Timed) return s;
        return s.UnshelveAtUtc is DateTime t && nowUtc >= t ? ShelvingState.Unshelved : s;
    }

    private static ImmutableList<AlarmComment> AppendComment(
        ImmutableList<AlarmComment> existing, DateTime ts, string user, string kind, string? text)
        => existing.Add(new AlarmComment(ts, user, kind, text ?? string.Empty));
}

/// <summary>Result of a state-machine operation — new state + what to emit (if anything).</summary>
/// <remarks>
///     <para>
///         <see cref="NoOpReason"/> carries a short diagnostic string for the
///         <see cref="NoOp(AlarmConditionState, string)"/> case (e.g.
///         "disabled — predicate result ignored", "already acknowledged"). The
///         engine logs this at debug level when a no-op result is observed, so
///         the class-level remarks on <see cref="Part9StateMachine"/> hold:
///         disabled-alarm and idempotent ack/confirm/shelve/unshelve
///         transitions do produce a diagnostic log line. Plain
///         <see cref="None(AlarmConditionState)"/> results (state unchanged,
///         no operator intent recorded — e.g. a predicate re-evaluation that
///         confirms the existing active state) leave <see cref="NoOpReason"/>
///         null because there is nothing to surface to an operator.
///     </para>
/// </remarks>
public sealed record TransitionResult(AlarmConditionState State, EmissionKind Emission, string? NoOpReason = null)
{
    /// <summary>
    ///     Creates a transition result with no change and no emission.
    /// </summary>
    /// <param name="state">The alarm condition state.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult None(AlarmConditionState state) => new(state, EmissionKind.None);

    /// <summary>
    ///     Creates a transition result indicating a no-op operation with a reason.
    /// </summary>
    /// <param name="state">The alarm condition state.</param>
    /// <param name="reason">The reason for the no-op.</param>
    /// <returns>The transition result.</returns>
    public static TransitionResult NoOp(AlarmConditionState state, string reason)
        => new(state, EmissionKind.None, reason);
}

/// <summary>What kind of event, if any, the engine should emit after a transition.</summary>
public enum EmissionKind
{
    /// <summary>State did not change meaningfully — no event to emit.</summary>
    None,
    /// <summary>Predicate transitioned to true while shelving was suppressing events.</summary>
    Suppressed,
    Activated,
    Cleared,
    Acknowledged,
    Confirmed,
    Shelved,
    Unshelved,
    Enabled,
    Disabled,
    CommentAdded,
    /// <summary>#478 — the worst-of-input source quality changed bucket (Good/Uncertain/Bad) with no
    /// accompanying Part 9 state transition. Delivered out of band via the dedicated
    /// <c>WriteAlarmQuality</c> node path, never through the <c>IAlarmSource</c> fan-out (it is not a
    /// state change and must not materialize or historize a condition).</summary>
    QualityChanged,
}
