using System;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

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

/// <summary>
///     Translation helpers between the wnwrapConsumer XML payload and the
///     proto-friendly <see cref="AlarmTransitionKind"/> wire format, plus
///     alarm-reference composition.
/// </summary>
/// <remarks>
///     <para>
///         These mappings stay pure and library-agnostic so they're unit
///         testable without an AVEVA install. The COM-side I/O lives on
///         <see cref="WnWrapAlarmConsumer"/>.
///     </para>
/// </remarks>
public static class AlarmRecordTransitionMapper
{
    /// <summary>
    ///     Decode AVEVA's STATE string (one of <c>UNACK_ALM</c>, <c>ACK_ALM</c>,
    ///     <c>UNACK_RTN</c>, <c>ACK_RTN</c>) into the worker's library-agnostic
    ///     <see cref="MxAlarmStateKind"/>. Unknown values map to
    ///     <see cref="MxAlarmStateKind.Unspecified"/>.
    /// </summary>
    /// <param name="stateXml">The state XML string from AVEVA (e.g., UNACK_ALM, ACK_RTN).</param>
    /// <returns>The parsed <see cref="MxAlarmStateKind"/>, or <see cref="MxAlarmStateKind.Unspecified"/> when unrecognized.</returns>
    public static MxAlarmStateKind ParseStateKind(string? stateXml)
    {
        if (string.IsNullOrWhiteSpace(stateXml)) return MxAlarmStateKind.Unspecified;
        return stateXml!.Trim().ToUpperInvariant() switch
        {
            "UNACK_ALM" => MxAlarmStateKind.UnackAlm,
            "ACK_ALM" => MxAlarmStateKind.AckAlm,
            "UNACK_RTN" => MxAlarmStateKind.UnackRtn,
            "ACK_RTN" => MxAlarmStateKind.AckRtn,
            _ => MxAlarmStateKind.Unspecified,
        };
    }

    /// <summary>
    ///     Decide which proto transition kind a state change represents.
    ///     The decision table:
    ///     <list type="bullet">
    ///         <item><description><c>previous=Unspecified</c> + <c>current=*Alm</c> → Raise (new alarm).</description></item>
    ///         <item><description><c>previous=Unspecified</c> + <c>current=*Rtn</c> → Clear (alarm appeared in cleared state — rare; missed the raise).</description></item>
    ///         <item><description><c>previous=Unack*</c> + <c>current=Ack*</c> → Acknowledge.</description></item>
    ///         <item><description><c>previous=*Alm</c> + <c>current=*Rtn</c> → Clear.</description></item>
    ///         <item><description><c>previous=*Rtn</c> + <c>current=*Alm</c> → Raise (re-trigger after clear).</description></item>
    ///         <item><description>Anything else → Unspecified (no proto kind to emit).</description></item>
    ///     </list>
    /// </summary>
    /// <param name="previous">The previous alarm state kind.</param>
    /// <param name="current">The current alarm state kind.</param>
    /// <returns>The proto <see cref="AlarmTransitionKind"/> that the state change represents.</returns>
    public static AlarmTransitionKind MapTransition(
        MxAlarmStateKind previous,
        MxAlarmStateKind current)
    {
        if (current == MxAlarmStateKind.Unspecified) return AlarmTransitionKind.Unspecified;

        bool currentIsAlm = current is MxAlarmStateKind.UnackAlm or MxAlarmStateKind.AckAlm;
        bool currentIsRtn = current is MxAlarmStateKind.UnackRtn or MxAlarmStateKind.AckRtn;
        bool currentIsAcked = current is MxAlarmStateKind.AckAlm or MxAlarmStateKind.AckRtn;

        if (previous == MxAlarmStateKind.Unspecified)
        {
            return currentIsAlm ? AlarmTransitionKind.Raise : AlarmTransitionKind.Clear;
        }

        bool previousIsAlm = previous is MxAlarmStateKind.UnackAlm or MxAlarmStateKind.AckAlm;
        bool previousIsRtn = previous is MxAlarmStateKind.UnackRtn or MxAlarmStateKind.AckRtn;
        bool previousIsAcked = previous is MxAlarmStateKind.AckAlm or MxAlarmStateKind.AckRtn;

        if (previousIsAlm && currentIsRtn) return AlarmTransitionKind.Clear;
        if (previousIsRtn && currentIsAlm) return AlarmTransitionKind.Raise;
        if (!previousIsAcked && currentIsAcked) return AlarmTransitionKind.Acknowledge;

        return AlarmTransitionKind.Unspecified;
    }

    /// <summary>
    ///     Compose <c>alarm_full_reference</c> as <c>Provider!Group.AlarmName</c>.
    ///     The format mirrors AVEVA's standard alarm-reference syntax so
    ///     downstream consumers that already speak it (e.g. the gateway's
    ///     AcknowledgeAlarm RPC echoing a reference back as a GUID lookup)
    ///     don't need translation.
    /// </summary>
    /// <param name="providerName">The provider name, or null.</param>
    /// <param name="groupName">The group name, or null.</param>
    /// <param name="alarmName">The alarm name, or null.</param>
    /// <returns>The composed <c>Provider!Group.AlarmName</c> reference string.</returns>
    public static string ComposeFullReference(string? providerName, string? groupName, string? alarmName)
    {
        string provider = providerName ?? string.Empty;
        string group = groupName ?? string.Empty;
        string name = alarmName ?? string.Empty;
        if (string.IsNullOrEmpty(provider))
        {
            return string.IsNullOrEmpty(group) ? name : $"{group}.{name}";
        }
        return string.IsNullOrEmpty(group)
            ? $"{provider}!{name}"
            : $"{provider}!{group}.{name}";
    }

    /// <summary>
    ///     Reassemble a UTC <see cref="DateTime"/> from the wnwrap XML's
    ///     <c>DATE</c> + <c>TIME</c> + <c>GMTOFFSET</c> + <c>DSTADJUST</c>
    ///     fields. Returns <see cref="DateTime.MinValue"/> when DATE / TIME
    ///     can't be parsed (best-effort — failure is non-fatal; the proto
    ///     will carry the epoch and the EventQueue's fault counter records
    ///     the parse miss).
    /// </summary>
    /// <param name="xmlDate">e.g. <c>"2026/5/1"</c> (no zero-padding).</param>
    /// <param name="xmlTime">e.g. <c>"13:26:14.709"</c>.</param>
    /// <param name="gmtOffsetMinutes">Offset of the producer's local time vs UTC, in minutes.</param>
    /// <param name="dstAdjustMinutes">DST adjustment already applied to local time, in minutes.</param>
    /// <returns>The reassembled UTC timestamp, or <see cref="DateTime.MinValue"/> when parsing fails.</returns>
    public static DateTime ParseTransitionTimestampUtc(
        string? xmlDate,
        string? xmlTime,
        int gmtOffsetMinutes,
        int dstAdjustMinutes)
    {
        if (string.IsNullOrWhiteSpace(xmlDate) || string.IsNullOrWhiteSpace(xmlTime))
        {
            return DateTime.MinValue;
        }

        // Parse DATE: yyyy/M/d (no zero padding observed). Use ParseExact with
        // multiple format candidates — AVEVA's locale may format differently
        // on non-en-US hosts.
        string[] dateFormats =
        {
            "yyyy/M/d", "yyyy/MM/dd", "M/d/yyyy", "MM/dd/yyyy",
            "d/M/yyyy", "dd/MM/yyyy",
        };
        string dateTrim = xmlDate!.Trim();
        if (!DateTime.TryParseExact(
                dateTrim,
                dateFormats,
                System.Globalization.CultureInfo.InvariantCulture,
                System.Globalization.DateTimeStyles.None,
                out DateTime date))
        {
            if (!DateTime.TryParse(
                    dateTrim,
                    System.Globalization.CultureInfo.InvariantCulture,
                    System.Globalization.DateTimeStyles.None,
                    out date))
            {
                return DateTime.MinValue;
            }
        }

        // Parse TIME: H:m:s.fff (variable precision).
        string[] timeFormats =
        {
            "H:m:s.fff", "H:m:s.ff", "H:m:s.f", "H:m:s",
            "HH:mm:ss.fff", "HH:mm:ss.ff", "HH:mm:ss.f", "HH:mm:ss",
        };
        string timeTrim = xmlTime!.Trim();
        if (!DateTime.TryParseExact(
                timeTrim,
                timeFormats,
                System.Globalization.CultureInfo.InvariantCulture,
                System.Globalization.DateTimeStyles.None,
                out DateTime time))
        {
            if (!DateTime.TryParse(
                    timeTrim,
                    System.Globalization.CultureInfo.InvariantCulture,
                    System.Globalization.DateTimeStyles.None,
                    out time))
            {
                return DateTime.MinValue;
            }
        }

        DateTime localProducerTime = new DateTime(
            date.Year, date.Month, date.Day,
            time.Hour, time.Minute, time.Second, time.Millisecond,
            DateTimeKind.Unspecified);

        // GMTOFFSET = minutes east of UTC (or behind, depending on convention).
        // The wnwrap convention observed: GMTOFFSET=240, DSTADJUST=0 for
        // EDT (4 hours behind UTC) — so the field is "minutes from local to UTC". To get
        // UTC, ADD the offset.
        DateTime utc = localProducerTime.AddMinutes(gmtOffsetMinutes - dstAdjustMinutes);
        return DateTime.SpecifyKind(utc, DateTimeKind.Utc);
    }
}
