using System.Globalization;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ProtoConditionState = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmConditionState;
using ProtoTransitionKind = ZB.MOM.WW.MxGateway.Contracts.Proto.AlarmTransitionKind;
// Alias the Commons alarm types so their simple names bind here, unambiguous
// against the colliding gateway proto enums above.
using NativeAlarmTransition = ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms.NativeAlarmTransition;
using AlarmConditionState = ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms.AlarmConditionState;
using AlarmTransitionKind = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmTransitionKind;
using AlarmShelveState = ZB.MOM.WW.ScadaBridge.Commons.Types.Enums.AlarmShelveState;

namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;

/// <summary>
/// Pure mapping from MxAccess Gateway alarm-feed proto messages to the
/// protocol-neutral <see cref="NativeAlarmTransition"/> shape. The gateway proto
/// enums (AlarmConditionState / AlarmTransitionKind) collide with the Commons
/// alarm enums, so they are aliased here. Unit-tested without a live gateway.
/// </summary>
public static class MxGatewayAlarmMapper
{
    /// <summary>Clamps the gateway severity onto the unified 0–1000 scale.</summary>
    /// <param name="severity">Raw gateway severity value to clamp.</param>
    /// <returns>The severity clamped to the range [0, 1000].</returns>
    public static int NormalizeSeverity(int severity) => Math.Clamp(severity, 0, 1000);

    /// <summary>Maps a gateway condition-state + severity to the unified condition.</summary>
    /// <param name="state">Gateway proto condition state to map.</param>
    /// <param name="severity">Raw gateway severity value; normalized to 0–1000.</param>
    /// <returns>The protocol-neutral <see cref="AlarmConditionState"/> equivalent.</returns>
    public static AlarmConditionState MapConditionState(ProtoConditionState state, int severity)
    {
        var (active, acked) = state switch
        {
            ProtoConditionState.Active => (true, false),
            ProtoConditionState.ActiveAcked => (true, true),
            ProtoConditionState.Inactive => (false, true),
            _ => (false, true)
        };
        return new AlarmConditionState(active, acked, Confirmed: null,
            Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: NormalizeSeverity(severity));
    }

    /// <summary>Maps a gateway transition kind to the unified transition kind.</summary>
    /// <param name="kind">Gateway proto transition kind to map.</param>
    /// <returns>The protocol-neutral <see cref="AlarmTransitionKind"/> equivalent.</returns>
    public static AlarmTransitionKind MapKind(ProtoTransitionKind kind) => kind switch
    {
        ProtoTransitionKind.Raise => AlarmTransitionKind.Raise,
        ProtoTransitionKind.Acknowledge => AlarmTransitionKind.Acknowledge,
        ProtoTransitionKind.Clear => AlarmTransitionKind.Clear,
        ProtoTransitionKind.Retrigger => AlarmTransitionKind.Retrigger,
        _ => AlarmTransitionKind.StateChange
    };

    /// <summary>Derives the mirrored condition from a transition kind + severity.</summary>
    private static AlarmConditionState ConditionFromKind(ProtoTransitionKind kind, int severity)
    {
        var (active, acked) = kind switch
        {
            ProtoTransitionKind.Raise => (true, false),
            ProtoTransitionKind.Acknowledge => (true, true),
            ProtoTransitionKind.Retrigger => (true, false),
            ProtoTransitionKind.Clear => (false, true),
            _ => (false, true)
        };
        return new AlarmConditionState(active, acked, Confirmed: null,
            Shelve: AlarmShelveState.Unshelved, Suppressed: false, Severity: NormalizeSeverity(severity));
    }

    /// <summary>
    /// Derives the ack timestamp mirrored onto <c>NativeAlarmTransition.AckTime</c>
    /// (MES alarm-status API §6.4).
    ///
    /// <para>
    /// Unlike OPC UA A&amp;C, the MxAccess Gateway alarm feed carries NO dedicated ack
    /// timestamp — an acknowledgement arrives as an <c>ACTIVE_ACKED</c> condition state on a
    /// transition whose only time is <paramref name="transitionTime"/> (the gateway's
    /// transition timestamp, or the DCL's own receipt time when the gateway omits it). That
    /// observation time is what gets stamped: accurate to when the system saw the ack, never
    /// fabricated, but not necessarily the operator's click.
    /// </para>
    ///
    /// <para>
    /// Returns <c>null</c> unless the condition is active AND acknowledged — matching
    /// <c>OpcUaAlarmMapper.DeriveAckTime</c>, so both native protocols agree on "null while
    /// unacked, cleared on re-raise". This matters especially here because the gateway maps
    /// INACTIVE to <c>acked = true</c>: without the active check, every return-to-normal
    /// would report an ack it never observed.
    /// </para>
    /// </summary>
    /// <param name="condition">The mirrored condition state for the transition.</param>
    /// <param name="transitionTime">The transition's timestamp — the DCL's observation instant for the ack.</param>
    /// <returns>The observed ack timestamp, or <c>null</c> when the condition is not an outstanding acknowledged alarm.</returns>
    public static DateTimeOffset? DeriveAckTime(AlarmConditionState condition, DateTimeOffset transitionTime) =>
        condition is { Active: true, Acknowledged: true } ? transitionTime : null;

    /// <summary>
    /// Converts an <see cref="MxValue"/> union to a display-only string using
    /// <see cref="MxValueExtensions.ToClrValue"/> and invariant culture formatting,
    /// so numeric values always use '.' as the decimal separator.  Null or unset
    /// values produce an empty string.
    /// </summary>
    /// <param name="mxVal">The gateway value union to convert, or null.</param>
    /// <returns>The value formatted as an invariant-culture string, or an empty string if null or unset.</returns>
    internal static string MxValueToString(MxValue? mxVal)
    {
        if (mxVal is null) return "";
        var clr = mxVal.ToClrValue();
        return clr is null ? "" : Convert.ToString(clr, CultureInfo.InvariantCulture) ?? "";
    }

    /// <summary>Maps a live <see cref="OnAlarmTransitionEvent"/> to a transition.</summary>
    /// <param name="body">The gateway alarm transition event proto message to map.</param>
    /// <returns>The protocol-neutral <see cref="NativeAlarmTransition"/>.</returns>
    public static NativeAlarmTransition MapTransition(OnAlarmTransitionEvent body)
    {
        var condition = ConditionFromKind(body.TransitionKind, body.Severity);
        var transitionTime = body.TransitionTimestamp?.ToDateTimeOffset() ?? DateTimeOffset.UtcNow;

        return new NativeAlarmTransition(
            // Identify the condition by the object-relative reference (e.g.
            // "Z28061.HeartbeatTimeoutAlarm") rather than the gateway's full provider
            // reference ("Galaxy!<area>.<object>.<alarm>"). The area is preserved in
            // Category; the object reference is globally unique within the galaxy and
            // is the form operators expect. Falls back to the full reference only if
            // the gateway omits the object reference.
            SourceReference: string.IsNullOrEmpty(body.SourceObjectReference)
                ? body.AlarmFullReference : body.SourceObjectReference,
            SourceObjectReference: body.SourceObjectReference,
            AlarmTypeName: body.AlarmTypeName,
            Kind: MapKind(body.TransitionKind),
            Condition: condition,
            Category: body.Category,
            Description: body.Description,
            Message: body.Description,
            OperatorUser: body.OperatorUser,
            OperatorComment: body.OperatorComment,
            OriginalRaiseTime: body.OriginalRaiseTimestamp?.ToDateTimeOffset(),
            TransitionTime: transitionTime,
            CurrentValue: MxValueToString(body.CurrentValue),
            LimitValue: MxValueToString(body.LimitValue),
            // MES alarm-status API §6.4: the gateway supplies no ack timestamp, so an ack
            // is stamped with the transition time it was observed at.
            AckTime: DeriveAckTime(condition, transitionTime));
    }

    /// <summary>The end-of-snapshot sentinel transition (no condition payload).</summary>
    /// <returns>A <see cref="NativeAlarmTransition"/> with <c>AlarmTransitionKind.SnapshotComplete</c>.</returns>
    public static NativeAlarmTransition SnapshotComplete() => new(
        "", "", "", AlarmTransitionKind.SnapshotComplete,
        new AlarmConditionState(false, true, null, AlarmShelveState.Unshelved, false, 0),
        "", "", "", "", "", null, DateTimeOffset.UtcNow, "", "");

    /// <summary>Maps one initial-snapshot <see cref="ActiveAlarmSnapshot"/> entry to a Snapshot transition.</summary>
    /// <param name="snapshot">The active alarm snapshot proto message to map.</param>
    /// <returns>A <see cref="NativeAlarmTransition"/> with <c>AlarmTransitionKind.Snapshot</c>.</returns>
    public static NativeAlarmTransition MapSnapshot(ActiveAlarmSnapshot snapshot)
    {
        var condition = MapConditionState(snapshot.CurrentState, snapshot.Severity);
        var transitionTime = snapshot.LastTransitionTimestamp?.ToDateTimeOffset() ?? DateTimeOffset.UtcNow;

        return new NativeAlarmTransition(
            // See MapTransition: identify by the object-relative reference, not the
            // full "Galaxy!<area>.<object>.<alarm>" provider reference.
            SourceReference: string.IsNullOrEmpty(snapshot.SourceObjectReference)
                ? snapshot.AlarmFullReference : snapshot.SourceObjectReference,
            SourceObjectReference: snapshot.SourceObjectReference,
            AlarmTypeName: snapshot.AlarmTypeName,
            Kind: AlarmTransitionKind.Snapshot,
            Condition: condition,
            Category: snapshot.Category,
            Description: snapshot.Description,
            Message: snapshot.Description,
            OperatorUser: snapshot.OperatorUser,
            OperatorComment: snapshot.OperatorComment,
            OriginalRaiseTime: snapshot.OriginalRaiseTimestamp?.ToDateTimeOffset(),
            TransitionTime: transitionTime,
            CurrentValue: MxValueToString(snapshot.CurrentValue),
            LimitValue: MxValueToString(snapshot.LimitValue),
            // MES alarm-status API §6.4: an ACTIVE_ACKED snapshot entry reports the last
            // transition time as its ack instant — the best the gateway feed supplies. A
            // (re)subscribe snapshot therefore restores an ack time rather than losing it.
            AckTime: DeriveAckTime(condition, transitionTime));
    }
}
