using System;
using System.Globalization;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.Conversion;

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

/// <summary>Maps MXAccess COM events to protobuf MxEvent messages.</summary>
public sealed class MxAccessEventMapper
{
    private readonly VariantConverter variantConverter;
    private readonly MxStatusProxyConverter statusProxyConverter;

    /// <summary>Initializes a new instance of the MxAccessEventMapper class with default converters.</summary>
    public MxAccessEventMapper()
        : this(new VariantConverter(), new MxStatusProxyConverter())
    {
    }

    /// <summary>Initializes a new instance of the MxAccessEventMapper class with provided converters.</summary>
    /// <param name="variantConverter">Converter for MXAccess variant values to MxValue protobuf messages.</param>
    /// <param name="statusProxyConverter">Converter for MXAccess status arrays to MxStatusProxy protobuf messages.</param>
    public MxAccessEventMapper(
        VariantConverter variantConverter,
        MxStatusProxyConverter statusProxyConverter)
    {
        this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter));
        this.statusProxyConverter = statusProxyConverter ?? throw new ArgumentNullException(nameof(statusProxyConverter));
    }

    /// <summary>Creates an OnDataChange event from MXAccess COM event arguments.</summary>
    /// <param name="sessionId">Identifier of the session.</param>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="value">Item value received from MXAccess.</param>
    /// <param name="quality">Item quality code from MXAccess.</param>
    /// <param name="timestamp">Item timestamp from MXAccess.</param>
    /// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
    public MxEvent CreateOnDataChange(
        string sessionId,
        int serverHandle,
        int itemHandle,
        object? value,
        int quality,
        object? timestamp,
        Array? statuses)
    {
        MxEvent mxEvent = CreateBaseEvent(
            MxEventFamily.OnDataChange,
            sessionId,
            serverHandle,
            itemHandle,
            statuses);
        mxEvent.Value = variantConverter.Convert(value);
        mxEvent.Quality = quality;
        ApplySourceTimestamp(mxEvent, timestamp);
        mxEvent.OnDataChange = new OnDataChangeEvent();

        return mxEvent;
    }

    /// <summary>Creates an OnWriteComplete event from MXAccess COM event arguments.</summary>
    /// <param name="sessionId">Identifier of the session.</param>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
    public MxEvent CreateOnWriteComplete(
        string sessionId,
        int serverHandle,
        int itemHandle,
        Array? statuses)
    {
        MxEvent mxEvent = CreateBaseEvent(
            MxEventFamily.OnWriteComplete,
            sessionId,
            serverHandle,
            itemHandle,
            statuses);
        mxEvent.OnWriteComplete = new OnWriteCompleteEvent();

        return mxEvent;
    }

    /// <summary>Creates an OperationComplete event from MXAccess COM event arguments.</summary>
    /// <param name="sessionId">Identifier of the session.</param>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
    public MxEvent CreateOperationComplete(
        string sessionId,
        int serverHandle,
        int itemHandle,
        Array? statuses)
    {
        MxEvent mxEvent = CreateBaseEvent(
            MxEventFamily.OperationComplete,
            sessionId,
            serverHandle,
            itemHandle,
            statuses);
        mxEvent.OperationComplete = new OperationCompleteEvent();

        return mxEvent;
    }

    /// <summary>
    ///     Creates an OnAlarmTransition event from MXAccess alarm-event arguments.
    ///     The worker's alarm path drives this method from
    ///     <see cref="MxAccessAlarmEventSink.EnqueueTransition"/> once
    ///     <see cref="AlarmDispatcher"/> decodes a transition raised by the
    ///     wnwrap-backed <see cref="WnWrapAlarmConsumer"/>.
    /// </summary>
    /// <param name="sessionId">Identifier of the session.</param>
    /// <param name="alarmFullReference">Fully-qualified MxAccess alarm reference (e.g. "Tank01.Level.HiHi").</param>
    /// <param name="sourceObjectReference">Galaxy-side source object reference; empty when not bound to a Galaxy object.</param>
    /// <param name="alarmTypeName">MxAccess alarm-type qualifier (e.g. "AnalogLimitAlarm.HiHi").</param>
    /// <param name="transitionKind">Discriminator: Raise / Acknowledge / Clear / Retrigger.</param>
    /// <param name="severity">Raw MxAccess severity (kept on the native scale; lmxopcua maps to OPC UA 0-1000).</param>
    /// <param name="originalRaiseTimestampUtc">When the alarm originally entered active; null on retrigger.</param>
    /// <param name="transitionTimestampUtc">When this specific transition occurred.</param>
    /// <param name="operatorUser">Operator principal recorded by MxAccess on Acknowledge transitions; empty on raise/clear.</param>
    /// <param name="operatorComment">Operator-supplied comment recorded by MxAccess on Acknowledge transitions; empty on raise/clear.</param>
    /// <param name="category">Alarm taxonomy bucket from the Galaxy template.</param>
    /// <param name="description">Human-readable alarm description.</param>
    /// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
    public MxEvent CreateOnAlarmTransition(
        string sessionId,
        string alarmFullReference,
        string sourceObjectReference,
        string alarmTypeName,
        AlarmTransitionKind transitionKind,
        int severity,
        DateTime? originalRaiseTimestampUtc,
        DateTime transitionTimestampUtc,
        string operatorUser,
        string operatorComment,
        string category,
        string description,
        Array? statuses)
    {
        MxEvent mxEvent = CreateBaseEvent(
            MxEventFamily.OnAlarmTransition,
            sessionId,
            serverHandle: 0,
            itemHandle: 0,
            statuses);

        OnAlarmTransitionEvent body = new()
        {
            AlarmFullReference = alarmFullReference ?? string.Empty,
            SourceObjectReference = sourceObjectReference ?? string.Empty,
            AlarmTypeName = alarmTypeName ?? string.Empty,
            TransitionKind = transitionKind,
            Severity = severity,
            TransitionTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
                DateTime.SpecifyKind(transitionTimestampUtc, DateTimeKind.Utc)),
            OperatorUser = operatorUser ?? string.Empty,
            OperatorComment = operatorComment ?? string.Empty,
            Category = category ?? string.Empty,
            Description = description ?? string.Empty,
        };
        if (originalRaiseTimestampUtc is { } orts)
        {
            body.OriginalRaiseTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
                DateTime.SpecifyKind(orts, DateTimeKind.Utc));
        }
        mxEvent.OnAlarmTransition = body;
        return mxEvent;
    }

    /// <summary>Creates an OnBufferedDataChange event from MXAccess COM event arguments.</summary>
    /// <param name="sessionId">Identifier of the session.</param>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="rawDataType">Raw MXAccess data type code for the buffered value.</param>
    /// <param name="value">Item value received from MXAccess.</param>
    /// <param name="quality">Array of quality values from MXAccess.</param>
    /// <param name="timestamp">Array of timestamp values from MXAccess.</param>
    /// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
    public MxEvent CreateOnBufferedDataChange(
        string sessionId,
        int serverHandle,
        int itemHandle,
        int rawDataType,
        object? value,
        object? quality,
        object? timestamp,
        Array? statuses)
    {
        MxDataType dataType = MapMxDataType(rawDataType);
        MxEvent mxEvent = CreateBaseEvent(
            MxEventFamily.OnBufferedDataChange,
            sessionId,
            serverHandle,
            itemHandle,
            statuses);
        mxEvent.Value = variantConverter.Convert(value, dataType);
        mxEvent.OnBufferedDataChange = new OnBufferedDataChangeEvent
        {
            DataType = dataType,
            RawDataType = rawDataType,
            QualityValues = ConvertBufferedArray(quality, MxDataType.Integer),
            TimestampValues = ConvertBufferedArray(timestamp, MxDataType.Time),
        };

        return mxEvent;
    }

    /// <summary>Maps a raw MXAccess data type code to the MxDataType enum.</summary>
    /// <param name="rawDataType">Raw MXAccess data type value to map.</param>
    /// <returns>The corresponding MxDataType enum value.</returns>
    public static MxDataType MapMxDataType(int rawDataType)
    {
        return rawDataType switch
        {
            -1 => MxDataType.Unknown,
            0 => MxDataType.NoData,
            1 => MxDataType.Boolean,
            2 => MxDataType.Integer,
            3 => MxDataType.Float,
            4 => MxDataType.Double,
            5 => MxDataType.String,
            6 => MxDataType.Time,
            7 => MxDataType.ElapsedTime,
            8 => MxDataType.ReferenceType,
            9 => MxDataType.StatusType,
            10 => MxDataType.Enum,
            11 => MxDataType.SecurityClassificationEnum,
            12 => MxDataType.DataQualityType,
            13 => MxDataType.QualifiedEnum,
            14 => MxDataType.QualifiedStruct,
            15 => MxDataType.InternationalizedString,
            16 => MxDataType.BigString,
            17 => MxDataType.End,
            _ => MxDataType.Unknown,
        };
    }

    private MxEvent CreateBaseEvent(
        MxEventFamily family,
        string sessionId,
        int serverHandle,
        int itemHandle,
        Array? statuses)
    {
        MxEvent mxEvent = new()
        {
            Family = family,
            SessionId = sessionId ?? string.Empty,
            ServerHandle = serverHandle,
            ItemHandle = itemHandle,
        };
        mxEvent.Statuses.Add(statusProxyConverter.ConvertMany(statuses));

        return mxEvent;
    }

    private void ApplySourceTimestamp(
        MxEvent mxEvent,
        object? timestamp)
    {
        MxValue convertedTimestamp = variantConverter.Convert(timestamp, MxDataType.Time);
        if (convertedTimestamp.KindCase == MxValue.KindOneofCase.TimestampValue)
        {
            mxEvent.SourceTimestamp = convertedTimestamp.TimestampValue;
            return;
        }

        // MXAccess fires OnDataChange with pftItemTimeStamp marshaled as a
        // VT_BSTR string (e.g. "3/26/2026 1:38:22.907 PM"), not a FILETIME or
        // a VT_DATE — so the variant converter classifies it as a plain
        // string and the timestamp would otherwise be dropped. Parse it here
        // so the source timestamp still reaches MxEvent. MXAccess formats the
        // string in the worker host's local time; see TryParseSourceTimestamp.
        if (convertedTimestamp.KindCase == MxValue.KindOneofCase.StringValue
            && TryParseSourceTimestamp(convertedTimestamp.StringValue, out DateTime parsedUtc))
        {
            mxEvent.SourceTimestamp = Timestamp.FromDateTime(parsedUtc);
            return;
        }

        if (!string.IsNullOrWhiteSpace(convertedTimestamp.RawDiagnostic))
        {
            mxEvent.RawStatus = string.IsNullOrWhiteSpace(mxEvent.RawStatus)
                ? convertedTimestamp.RawDiagnostic
                : $"{mxEvent.RawStatus}; {convertedTimestamp.RawDiagnostic}";
        }
    }

    /// <summary>
    ///     Parses an MXAccess <c>OnDataChange</c> timestamp string into a UTC
    ///     <see cref="DateTime"/>. MXAccess delivers the value as a culture-
    ///     formatted string rather than a FILETIME or VT_DATE, and formats it
    ///     in the worker host's <em>local</em> time (verified empirically — a
    ///     fast-changing tag's timestamp lands the host's UTC offset behind
    ///     wall-clock UTC). The parsed value is therefore taken as local time
    ///     and converted to UTC. Tries the worker host's culture first
    ///     (MXAccess formats with the host locale), then the invariant culture.
    /// </summary>
    /// <param name="text">The MXAccess timestamp string.</param>
    /// <param name="utc">The parsed UTC timestamp on success.</param>
    /// <returns><see langword="true"/> when the string parsed successfully.</returns>
    internal static bool TryParseSourceTimestamp(string? text, out DateTime utc)
    {
        utc = default;
        if (string.IsNullOrWhiteSpace(text))
        {
            return false;
        }

        const DateTimeStyles styles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
        if (DateTime.TryParse(text, CultureInfo.CurrentCulture, styles, out DateTime parsed)
            || DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out parsed))
        {
            utc = DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
            return true;
        }

        return false;
    }

    private MxArray ConvertBufferedArray(
        object? value,
        MxDataType expectedElementDataType)
    {
        if (value is Array array)
        {
            return variantConverter.ConvertArray(array, expectedElementDataType);
        }

        MxValue converted = variantConverter.Convert(value, expectedElementDataType);
        if (converted.KindCase == MxValue.KindOneofCase.ArrayValue)
        {
            return converted.ArrayValue;
        }

        MxArray mxArray = new()
        {
            ElementDataType = converted.DataType,
            VariantType = converted.VariantType,
            RawElementDataType = converted.RawDataType,
            RawDiagnostic = string.IsNullOrWhiteSpace(converted.RawDiagnostic)
                ? "Buffered MXAccess event argument was not a SAFEARRAY."
                : converted.RawDiagnostic,
        };

        switch (converted.KindCase)
        {
            case MxValue.KindOneofCase.Int32Value:
                mxArray.Int32Values = new Int32Array();
                mxArray.Int32Values.Values.Add(converted.Int32Value);
                break;

            case MxValue.KindOneofCase.Int64Value:
                mxArray.Int64Values = new Int64Array();
                mxArray.Int64Values.Values.Add(converted.Int64Value);
                break;

            case MxValue.KindOneofCase.TimestampValue:
                mxArray.TimestampValues = new TimestampArray();
                mxArray.TimestampValues.Values.Add(converted.TimestampValue);
                break;
        }

        return mxArray;
    }
}
