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>
    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>
    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>
    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);
    }

    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);
    }

    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);
    }

    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);
    }

    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);
    }

    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);
    }

    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>
    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 IReadOnlyList<AlarmComment> AppendComment(
        IReadOnlyList<AlarmComment> existing, DateTime ts, string user, string kind, string? text)
    {
        var list = new List<AlarmComment>(existing.Count + 1);
        list.AddRange(existing);
        list.Add(new AlarmComment(ts, user, kind, text ?? string.Empty));
        return list;
    }
}

/// <summary>Result of a state-machine operation — new state + what to emit (if anything).</summary>
public sealed record TransitionResult(AlarmConditionState State, EmissionKind Emission)
{
    public static TransitionResult None(AlarmConditionState state) => new(state, EmissionKind.None);
    public static TransitionResult NoOp(AlarmConditionState state, string reason) => new(state, EmissionKind.None);
}

/// <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,
}
