using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry;

namespace ZB.MOM.WW.OtOpcUa.Host.Grpc;

/// <summary>
///     Node-side projection of a domain <see cref="TelemetryItem"/> onto its wire
///     <see cref="TelemetryEvent"/> envelope (per-cluster mesh Phase 5). The exact reverse of
///     <c>telemetry.proto</c>: it transcribes every domain field onto the matching proto field and
///     selects the envelope's <c>oneof</c> arm from the <see cref="TelemetryItem"/> subtype.
/// </summary>
/// <remarks>
///     <para>
///         <b>DateTime → Timestamp is Kind-defensive.</b> <see cref="Timestamp.FromDateTime"/> throws
///         unless the source <see cref="DateTime.Kind"/> is <see cref="DateTimeKind.Utc"/>. Producers
///         use <see cref="DateTime.UtcNow"/> today, but a stray <see cref="DateTimeKind.Local"/> or
///         <see cref="DateTimeKind.Unspecified"/> value must never crash the stream — so a Local value
///         is converted and an Unspecified value is assumed already-UTC (matching the producer intent
///         and preserving the instant on any machine).
///     </para>
///     <para>
///         <b>Nullable presence is preserved.</b> A null nullable-<c>DateTime</c> leaves the proto
///         Timestamp unset (absent == null per the contract); a null <c>optional string</c> leaves the
///         proto field unset so the generated <c>Has…</c> presence distinguishes null from "".
///     </para>
/// </remarks>
public static class TelemetryProtoMapNode
{
    /// <summary>Projects <paramref name="item"/> onto a wire <see cref="TelemetryEvent"/>.</summary>
    /// <param name="item">The domain telemetry item to project.</param>
    /// <param name="correlationId">Stream correlation id echoed on every envelope.</param>
    /// <returns>The wire envelope with the matching <c>oneof</c> arm populated.</returns>
    public static TelemetryEvent ToProto(TelemetryItem item, string correlationId)
    {
        ArgumentNullException.ThrowIfNull(item);

        return item switch
        {
            TelemetryItem.Alarm a => new TelemetryEvent
            {
                CorrelationId = correlationId,
                AlarmTransition = MapAlarm(a.E),
            },
            TelemetryItem.Script s => new TelemetryEvent
            {
                CorrelationId = correlationId,
                ScriptLog = MapScript(s.E),
            },
            TelemetryItem.Health h => new TelemetryEvent
            {
                CorrelationId = correlationId,
                DriverHealth = MapHealth(h.E),
            },
            TelemetryItem.Resilience r => new TelemetryEvent
            {
                CorrelationId = correlationId,
                DriverResilience = MapResilience(r.E),
            },
            _ => throw new ArgumentOutOfRangeException(
                nameof(item), item.GetType().Name, "Unknown TelemetryItem subtype"),
        };
    }

    private static AlarmTransition MapAlarm(AlarmTransitionEvent e)
    {
        // Required (non-optional) proto string setters throw ArgumentNullException on null, and the
        // domain records carry no runtime null guard (nullable-ref annotations are compile-time only).
        // Coalesce every required string to "" so a stray null can never throw out of the pump and kill
        // the stream. The `optional` fields below keep their null-vs-"" presence guards.
        var msg = new AlarmTransition
        {
            AlarmId = e.AlarmId ?? "",
            EquipmentPath = e.EquipmentPath ?? "",
            AlarmName = e.AlarmName ?? "",
            TransitionKind = e.TransitionKind ?? "",
            Severity = e.Severity,
            Message = e.Message ?? "",
            User = e.User ?? "",
            TimestampUtc = ToUtcTimestamp(e.TimestampUtc),
            AlarmTypeName = e.AlarmTypeName ?? "",
        };

        if (e.Comment is not null)
            msg.Comment = e.Comment;
        if (e.HistorizeToAveva is not null)
            msg.HistorizeToAveva = e.HistorizeToAveva.Value;
        msg.ReferencingEquipmentPaths.AddRange(e.ReferencingEquipmentPaths ?? Enumerable.Empty<string>());

        return msg;
    }

    private static ScriptLog MapScript(ScriptLogEntry e)
    {
        var msg = new ScriptLog
        {
            ScriptId = e.ScriptId ?? "",
            Level = e.Level ?? "",
            Message = e.Message ?? "",
            TimestampUtc = ToUtcTimestamp(e.TimestampUtc),
        };

        if (e.VirtualTagId is not null)
            msg.VirtualTagId = e.VirtualTagId;
        if (e.AlarmId is not null)
            msg.AlarmId = e.AlarmId;
        if (e.EquipmentId is not null)
            msg.EquipmentId = e.EquipmentId;

        return msg;
    }

    private static DriverHealth MapHealth(DriverHealthChanged e)
    {
        var msg = new DriverHealth
        {
            ClusterId = e.ClusterId ?? "",
            DriverInstanceId = e.DriverInstanceId ?? "",
            State = e.State ?? "",
            ErrorCount5Min = e.ErrorCount5Min,
            PublishedUtc = ToUtcTimestamp(e.PublishedUtc),
        };

        if (e.LastSuccessfulReadUtc is not null)
            msg.LastSuccessfulReadUtc = ToUtcTimestamp(e.LastSuccessfulReadUtc.Value);
        if (e.LastError is not null)
            msg.LastError = e.LastError;
        if (e.RediscoveryNeededUtc is not null)
            msg.RediscoveryNeededUtc = ToUtcTimestamp(e.RediscoveryNeededUtc.Value);
        if (e.RediscoveryReason is not null)
            msg.RediscoveryReason = e.RediscoveryReason;

        return msg;
    }

    private static DriverResilienceStatus MapResilience(DriverResilienceStatusChanged e)
    {
        var msg = new DriverResilienceStatus
        {
            DriverInstanceId = e.DriverInstanceId ?? "",
            HostName = e.HostName ?? "",
            BreakerOpen = e.BreakerOpen,
            ConsecutiveFailures = e.ConsecutiveFailures,
            CurrentInFlight = e.CurrentInFlight,
            LastSampledUtc = ToUtcTimestamp(e.LastSampledUtc),
            PublishedUtc = ToUtcTimestamp(e.PublishedUtc),
        };

        if (e.LastBreakerOpenUtc is not null)
            msg.LastBreakerOpenUtc = ToUtcTimestamp(e.LastBreakerOpenUtc.Value);

        return msg;
    }

    /// <summary>
    ///     Converts a domain <see cref="DateTime"/> to a proto <see cref="Timestamp"/> without ever
    ///     throwing on Kind: Utc is used as-is, Local is converted, and Unspecified is assumed
    ///     already-UTC (the producer contract — <see cref="DateTime.UtcNow"/> — with the wall-clock
    ///     ticks preserved so the instant round-trips).
    /// </summary>
    private static Timestamp ToUtcTimestamp(DateTime dt)
    {
        var utc = dt.Kind switch
        {
            DateTimeKind.Utc => dt,
            DateTimeKind.Local => dt.ToUniversalTime(),
            _ => DateTime.SpecifyKind(dt, DateTimeKind.Utc),
        };

        return Timestamp.FromDateTime(utc);
    }
}
