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;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;

/// <summary>
///     Central-side projection of a wire <see cref="TelemetryEvent"/> envelope back onto its domain
///     record (per-cluster mesh Phase 5). The exact reverse of <c>TelemetryProtoMapNode</c>: it
///     transcribes every proto field onto the matching domain field and selects the domain record from
///     the envelope's <c>oneof</c> arm.
/// </summary>
/// <remarks>
///     <para>
///         <b>Timestamp presence is honoured.</b> A proto <c>google.protobuf.Timestamp</c> field is a
///         message: when the node left it unset (a null nullable-<c>DateTime</c> on the domain side) the
///         generated property is <see langword="null"/>, so the nullable domain fields map via
///         <c>?.ToDateTime()</c> and an absent Timestamp becomes <see langword="null"/>. The
///         non-nullable domain fields (published / sampled / transition timestamps) are always set by
///         the node mapper, so they map via <see cref="Required"/> (which yields a
///         <see cref="DateTimeKind.Utc"/> value).
///     </para>
///     <para>
///         <b>A required Timestamp is defended, not assumed.</b> Across a mixed-version fleet an
///         out-of-contract sender could leave a required Timestamp unset. <see cref="Required"/> throws
///         a clear <see cref="InvalidOperationException"/> naming the kind + field instead of letting a
///         bare <see cref="NullReferenceException"/> escape — the client's per-event isolation then logs
///         it as a dropped malformed event and continues, because it is a code/version fault, never a
///         transport fault.
///     </para>
///     <para>
///         <b>optional-string presence is honoured.</b> A proto3 <c>optional string</c> that the node
///         left unset reads back as <see langword="null"/> (via the generated <c>Has…</c> presence),
///         distinguishing null from the empty string; likewise the <c>optional bool</c>
///         <c>historize_to_aveva</c> round-trips its three states (null / true / false).
///     </para>
/// </remarks>
public static class TelemetryProtoMapCentral
{
    /// <summary>
    ///     Projects a wire <see cref="TelemetryEvent"/> onto its domain record, selecting the type from
    ///     the populated <c>oneof</c> arm. This switch is the coverage guard: every
    ///     <see cref="TelemetryProtoContract.HandledCases"/> value has an arm here, and an unset (or a
    ///     future unmapped) case throws <see cref="NotSupportedException"/> so adding a fifth oneof case
    ///     without a converter fails the coverage test.
    /// </summary>
    /// <param name="evt">The wire envelope to project.</param>
    /// <returns>The domain record for the populated arm.</returns>
    /// <exception cref="ArgumentNullException"><paramref name="evt"/> is null.</exception>
    /// <exception cref="NotSupportedException">The envelope carries no handled <c>oneof</c> arm.</exception>
    public static object MapEvent(TelemetryEvent evt)
    {
        ArgumentNullException.ThrowIfNull(evt);

        return evt.EventCase switch
        {
            TelemetryEvent.EventOneofCase.AlarmTransition => ToAlarm(evt.AlarmTransition),
            TelemetryEvent.EventOneofCase.ScriptLog => ToScript(evt.ScriptLog),
            TelemetryEvent.EventOneofCase.DriverHealth => ToHealth(evt.DriverHealth),
            TelemetryEvent.EventOneofCase.DriverResilience => ToResilience(evt.DriverResilience),
            _ => throw new NotSupportedException(
                $"TelemetryEvent carries no handled oneof arm (EventCase = {evt.EventCase})."),
        };
    }

    /// <summary>Projects an <see cref="AlarmTransition"/> onto an <see cref="AlarmTransitionEvent"/>.</summary>
    /// <param name="msg">The proto sub-message.</param>
    /// <returns>The domain record.</returns>
    public static AlarmTransitionEvent ToAlarm(AlarmTransition msg)
    {
        ArgumentNullException.ThrowIfNull(msg);

        return new AlarmTransitionEvent(
            AlarmId: msg.AlarmId,
            EquipmentPath: msg.EquipmentPath,
            AlarmName: msg.AlarmName,
            TransitionKind: msg.TransitionKind,
            Severity: msg.Severity,
            Message: msg.Message,
            User: msg.User,
            TimestampUtc: Required(msg.TimestampUtc, "AlarmTransition", "timestamp_utc"),
            AlarmTypeName: msg.AlarmTypeName,
            Comment: msg.HasComment ? msg.Comment : null,
            HistorizeToAveva: msg.HasHistorizeToAveva ? msg.HistorizeToAveva : null,
            ReferencingEquipmentPaths: msg.ReferencingEquipmentPaths.ToList());
    }

    /// <summary>Projects a <see cref="ScriptLog"/> onto a <see cref="ScriptLogEntry"/>.</summary>
    /// <param name="msg">The proto sub-message.</param>
    /// <returns>The domain record.</returns>
    public static ScriptLogEntry ToScript(ScriptLog msg)
    {
        ArgumentNullException.ThrowIfNull(msg);

        return new ScriptLogEntry(
            ScriptId: msg.ScriptId,
            Level: msg.Level,
            Message: msg.Message,
            TimestampUtc: Required(msg.TimestampUtc, "ScriptLog", "timestamp_utc"),
            VirtualTagId: msg.HasVirtualTagId ? msg.VirtualTagId : null,
            AlarmId: msg.HasAlarmId ? msg.AlarmId : null,
            EquipmentId: msg.HasEquipmentId ? msg.EquipmentId : null);
    }

    /// <summary>Projects a <see cref="DriverHealth"/> onto a <see cref="DriverHealthChanged"/>.</summary>
    /// <param name="msg">The proto sub-message.</param>
    /// <returns>The domain record.</returns>
    public static DriverHealthChanged ToHealth(DriverHealth msg)
    {
        ArgumentNullException.ThrowIfNull(msg);

        return new DriverHealthChanged(
            ClusterId: msg.ClusterId,
            DriverInstanceId: msg.DriverInstanceId,
            State: msg.State,
            LastSuccessfulReadUtc: msg.LastSuccessfulReadUtc?.ToDateTime(),
            LastError: msg.HasLastError ? msg.LastError : null,
            ErrorCount5Min: msg.ErrorCount5Min,
            PublishedUtc: Required(msg.PublishedUtc, "DriverHealth", "published_utc"),
            RediscoveryNeededUtc: msg.RediscoveryNeededUtc?.ToDateTime(),
            RediscoveryReason: msg.HasRediscoveryReason ? msg.RediscoveryReason : null);
    }

    /// <summary>Projects a <see cref="DriverResilienceStatus"/> onto a <see cref="DriverResilienceStatusChanged"/>.</summary>
    /// <param name="msg">The proto sub-message.</param>
    /// <returns>The domain record.</returns>
    public static DriverResilienceStatusChanged ToResilience(DriverResilienceStatus msg)
    {
        ArgumentNullException.ThrowIfNull(msg);

        return new DriverResilienceStatusChanged(
            DriverInstanceId: msg.DriverInstanceId,
            HostName: msg.HostName,
            BreakerOpen: msg.BreakerOpen,
            ConsecutiveFailures: msg.ConsecutiveFailures,
            CurrentInFlight: msg.CurrentInFlight,
            LastBreakerOpenUtc: msg.LastBreakerOpenUtc?.ToDateTime(),
            LastSampledUtc: Required(msg.LastSampledUtc, "DriverResilienceStatus", "last_sampled_utc"),
            PublishedUtc: Required(msg.PublishedUtc, "DriverResilienceStatus", "published_utc"));
    }

    /// <summary>
    ///     Converts a <b>required</b> proto <see cref="Timestamp"/> to a UTC <see cref="DateTime"/>,
    ///     throwing a clear <see cref="InvalidOperationException"/> naming the telemetry kind + proto
    ///     field if an out-of-contract sender left it unset — rather than letting a bare
    ///     <see cref="NullReferenceException"/> escape.
    /// </summary>
    /// <param name="ts">The required proto Timestamp (<see langword="null"/> when the sender omitted it).</param>
    /// <param name="kind">The telemetry sub-message name, for the diagnostic.</param>
    /// <param name="field">The proto field name, for the diagnostic.</param>
    /// <returns>The Timestamp as a <see cref="DateTimeKind.Utc"/> value.</returns>
    /// <exception cref="InvalidOperationException"><paramref name="ts"/> is unset.</exception>
    private static DateTime Required(Timestamp? ts, string kind, string field) =>
        ts is null
            ? throw new InvalidOperationException($"telemetry {kind} missing required timestamp {field}")
            : ts.ToDateTime();
}
