using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;

namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc;

/// <summary>
/// Canonical bridge for Site Call Audit operational rows between the
/// wire-format <see cref="SiteCallOperationalDto"/> exchanged on the
/// <c>CachedCallTelemetry</c> packet and the in-process <see cref="SiteCall"/>
/// persistence entity central writes into the <c>SiteCalls</c> table.
/// </summary>
/// <remarks>
/// <para>
/// This mapper lives in <c>ZB.MOM.WW.ScadaBridge.Communication</c> (which owns the generated
/// <see cref="SiteCallOperationalDto"/> and references <c>Commons</c> for
/// <see cref="SiteCall"/>) so both <c>SiteStreamGrpcServer</c> and
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> can share one implementation without the
/// project-reference cycle that would result from hosting it in
/// <c>ZB.MOM.WW.ScadaBridge.AuditLog</c> (AuditLog → Communication, never the reverse).
/// Mirrors the sibling <see cref="AuditEventDtoMapper"/>.
/// </para>
/// <para>
/// Three directions are provided. <see cref="FromDto"/> rehydrates the central
/// <see cref="SiteCall"/> entity central writes into the <c>SiteCalls</c> table.
/// <see cref="ToDto(SiteCallOperational)"/> projects a site-local
/// <see cref="SiteCallOperational"/> onto the wire — used by the Site Call Audit
/// <c>PullSiteCalls</c> reconciliation handler (the central→site self-heal pull).
/// <see cref="ToDto(SiteCall)"/> projects the entity form back onto the wire; it
/// exists for the gRPC central control plane, whose site-side transport receives
/// an already-decoded <c>IngestCachedTelemetryCommand</c> (which carries
/// <see cref="SiteCall"/>, not <see cref="SiteCallOperational"/>) and must
/// re-encode it. It was previously documented here as necessarily dead code —
/// true only while ClusterClient was the sole path from that command to central.
/// </para>
/// <para>
/// String nullability convention: proto3 scalar strings cannot be absent, so the
/// optional <see cref="SiteCall.LastError"/> rehydrates from an empty string back
/// to null. The optional <c>HttpStatus</c> and <c>TerminalAtUtc</c> use proto
/// wrappers so they preserve true null semantics.
/// </para>
/// </remarks>
public static class SiteCallDtoMapper
{
    /// <summary>
    /// Reconstructs a <see cref="SiteCall"/> persistence entity from its
    /// wire-format DTO. An empty <c>LastError</c> rehydrates as null; absent
    /// <c>HttpStatus</c>/<c>TerminalAtUtc</c> wrappers stay null.
    /// </summary>
    /// <remarks>
    /// <see cref="SiteCall.IngestedAtUtc"/> is stamped here as a placeholder
    /// (<see cref="DateTime.UtcNow"/>); the central ingest actor overwrites it
    /// inside the dual-write transaction so the AuditLog and SiteCalls rows
    /// share one instant. The value sent on the wire is informational only.
    /// </remarks>
    /// <param name="dto">The wire-format site call DTO to map.</param>
    /// <returns>A <see cref="SiteCall"/> entity populated from the DTO fields.</returns>
    public static SiteCall FromDto(SiteCallOperationalDto dto)
    {
        ArgumentNullException.ThrowIfNull(dto);

        return new SiteCall
        {
            TrackedOperationId = TrackedOperationId.Parse(dto.TrackedOperationId),
            Channel = dto.Channel,
            Target = dto.Target,
            SourceSite = dto.SourceSite,
            SourceNode = string.IsNullOrEmpty(dto.SourceNode) ? null : dto.SourceNode,
            Status = dto.Status,
            RetryCount = dto.RetryCount,
            LastError = string.IsNullOrEmpty(dto.LastError) ? null : dto.LastError,
            HttpStatus = dto.HttpStatus,
            CreatedAtUtc = DateTime.SpecifyKind(dto.CreatedAtUtc.ToDateTime(), DateTimeKind.Utc),
            UpdatedAtUtc = DateTime.SpecifyKind(dto.UpdatedAtUtc.ToDateTime(), DateTimeKind.Utc),
            TerminalAtUtc = dto.TerminalAtUtc is null
                ? null
                : DateTime.SpecifyKind(dto.TerminalAtUtc.ToDateTime(), DateTimeKind.Utc),
            IngestedAtUtc = DateTime.UtcNow, // overwritten by AuditLogIngestActor
        };
    }

    /// <summary>
    /// Projects a site-local <see cref="SiteCallOperational"/> onto its
    /// wire-format DTO for the Site Call Audit <c>PullSiteCalls</c>
    /// reconciliation RPC. The inverse of <see cref="FromDto"/>; null
    /// <see cref="SiteCallOperational.LastError"/> / <see cref="SiteCallOperational.SourceNode"/>
    /// collapse to empty strings (proto3 scalar strings cannot be absent), while
    /// the nullable <c>HttpStatus</c> and <c>TerminalAtUtc</c> stay unset on the
    /// wire so true-null semantics survive the round-trip back through
    /// <see cref="FromDto"/>.
    /// </summary>
    /// <param name="operational">The site-local operational state to project to wire format.</param>
    /// <returns>A populated <see cref="SiteCallOperationalDto"/> ready for transmission.</returns>
    public static SiteCallOperationalDto ToDto(SiteCallOperational operational)
    {
        ArgumentNullException.ThrowIfNull(operational);

        var dto = new SiteCallOperationalDto
        {
            TrackedOperationId = operational.TrackedOperationId.ToString(),
            Channel = operational.Channel,
            Target = operational.Target,
            SourceSite = operational.SourceSite,
            SourceNode = operational.SourceNode ?? string.Empty,
            Status = operational.Status,
            RetryCount = operational.RetryCount,
            LastError = operational.LastError ?? string.Empty,
            CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.CreatedAtUtc)),
            UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.UpdatedAtUtc)),
        };

        if (operational.HttpStatus.HasValue)
        {
            dto.HttpStatus = operational.HttpStatus.Value;
        }

        if (operational.TerminalAtUtc.HasValue)
        {
            dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.TerminalAtUtc.Value));
        }

        return dto;
    }

    /// <summary>
    /// Projects a <see cref="SiteCall"/> entity onto its wire-format DTO — the
    /// inverse of <see cref="FromDto"/>, so the pair round-trips exactly.
    /// </summary>
    /// <remarks>
    /// <see cref="SiteCall.IngestedAtUtc"/> is deliberately NOT written: it is
    /// central-set inside the dual-write transaction and the value carried on the
    /// wire is informational only (<see cref="FromDto"/> stamps a placeholder that
    /// the ingest actor overwrites). Every other field survives; null
    /// <c>SourceNode</c>/<c>LastError</c> collapse to empty strings while the
    /// nullable <c>HttpStatus</c>/<c>TerminalAtUtc</c> stay unset on the wire.
    /// </remarks>
    /// <param name="siteCall">The central operational-state entity to project.</param>
    /// <returns>A populated <see cref="SiteCallOperationalDto"/> ready for transmission.</returns>
    public static SiteCallOperationalDto ToDto(SiteCall siteCall)
    {
        ArgumentNullException.ThrowIfNull(siteCall);

        var dto = new SiteCallOperationalDto
        {
            TrackedOperationId = siteCall.TrackedOperationId.ToString(),
            Channel = siteCall.Channel,
            Target = siteCall.Target,
            SourceSite = siteCall.SourceSite,
            SourceNode = siteCall.SourceNode ?? string.Empty,
            Status = siteCall.Status,
            RetryCount = siteCall.RetryCount,
            LastError = siteCall.LastError ?? string.Empty,
            CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(siteCall.CreatedAtUtc)),
            UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(siteCall.UpdatedAtUtc)),
        };

        if (siteCall.HttpStatus.HasValue)
        {
            dto.HttpStatus = siteCall.HttpStatus.Value;
        }

        if (siteCall.TerminalAtUtc.HasValue)
        {
            dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(siteCall.TerminalAtUtc.Value));
        }

        return dto;
    }

    // All ScadaBridge timestamps are UTC by invariant; Timestamp.FromDateTime
    // requires UTC kind. Specify (never convert) so a row read back from SQLite
    // with Kind=Utc passes through and a defensively-unspecified value is
    // treated as the UTC it already is. Mirrors AuditEventDtoMapper.EnsureUtc.
    private static DateTime EnsureUtc(DateTime value) =>
        value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value, DateTimeKind.Utc);
}
