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

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

/// <summary>
/// Canonical bridge for Audit Log rows between the in-process
/// <see cref="AuditEvent"/> record and the wire-format <see cref="AuditEventDto"/>
/// exchanged over the <c>IngestAuditEvents</c>, <c>IngestCachedTelemetry</c> and
/// <c>PullAuditEvents</c> RPCs.
/// </summary>
/// <remarks>
/// <para>
/// This mapper lives in <c>ZB.MOM.WW.ScadaBridge.Communication</c> (which owns the generated
/// <see cref="AuditEventDto"/> and references <c>Commons</c> for
/// <see cref="AuditEvent"/>) 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).
/// </para>
/// <para><b>Lossy by design:</b> the proto contract intentionally omits two fields.</para>
/// <list type="bullet">
///   <item><see cref="AuditEvent.ForwardState"/> — site-local SQLite state, never travels.</item>
///   <item><see cref="AuditEvent.IngestedAtUtc"/> — central-set at ingest time, not at the site.</item>
/// </list>
/// <para>
/// String nullability convention: proto3 scalar strings cannot be absent, so nullable
/// .NET strings round-trip as empty strings on the wire. Nullable integers use the
/// <c>Int32Value</c> wrapper so they preserve true null semantics.
/// </para>
/// </remarks>
public static class AuditEventDtoMapper
{
    /// <summary>
    /// Projects an <see cref="AuditEvent"/> into its wire-format DTO. Null reference
    /// fields collapse to empty strings; null integer fields leave the wrapper unset.
    /// </summary>
    /// <param name="evt">The audit event to project to wire format.</param>
    /// <returns>A populated <see cref="AuditEventDto"/> ready for transmission; null strings collapse to empty.</returns>
    public static AuditEventDto ToDto(AuditEvent evt)
    {
        ArgumentNullException.ThrowIfNull(evt);

        // The proto contract is the UNCHANGED 24-field wire. The
        // canonical record carries the ScadaBridge domain fields inside
        // DetailsJson — decompose them so the DTO's typed domain fields are
        // populated exactly as before.
        var r = AuditRowProjection.Decompose(evt);

        var dto = new AuditEventDto
        {
            EventId = r.EventId.ToString(),
            OccurredAtUtc = Timestamp.FromDateTime(EnsureUtc(r.OccurredAtUtc)),
            Channel = r.Channel.ToString(),
            Kind = r.Kind.ToString(),
            CorrelationId = r.CorrelationId?.ToString() ?? string.Empty,
            ExecutionId = r.ExecutionId?.ToString() ?? string.Empty,
            ParentExecutionId = r.ParentExecutionId?.ToString() ?? string.Empty,
            SourceSiteId = r.SourceSiteId ?? string.Empty,
            SourceNode = r.SourceNode ?? string.Empty,
            SourceInstanceId = r.SourceInstanceId ?? string.Empty,
            SourceScript = r.SourceScript ?? string.Empty,
            Actor = r.Actor ?? string.Empty,
            Target = r.Target ?? string.Empty,
            Status = r.Status.ToString(),
            ErrorMessage = r.ErrorMessage ?? string.Empty,
            ErrorDetail = r.ErrorDetail ?? string.Empty,
            RequestSummary = r.RequestSummary ?? string.Empty,
            ResponseSummary = r.ResponseSummary ?? string.Empty,
            PayloadTruncated = r.PayloadTruncated,
            Extra = r.Extra ?? string.Empty
        };

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

        if (r.DurationMs.HasValue)
        {
            dto.DurationMs = r.DurationMs.Value;
        }

        return dto;
    }

    /// <summary>
    /// Reconstructs an <see cref="AuditEvent"/> from its wire-format DTO. Empty strings
    /// rehydrate as null reference values; absent integer wrappers stay null.
    /// <see cref="AuditEvent.ForwardState"/> and <see cref="AuditEvent.IngestedAtUtc"/>
    /// are intentionally left null — the central ingest actor sets the latter.
    /// </summary>
    /// <param name="dto">The wire-format DTO to reconstruct into an <see cref="AuditEvent"/>.</param>
    /// <returns>A canonical <see cref="AuditEvent"/> with domain fields recomposed into <c>DetailsJson</c>; <c>ForwardState</c> and <c>IngestedAtUtc</c> are left null.</returns>
    public static AuditEvent FromDto(AuditEventDto dto)
    {
        ArgumentNullException.ThrowIfNull(dto);

        // Recompose the canonical record from the 24-field wire
        // DTO. The domain fields are re-serialized into DetailsJson via the
        // projection helper; IngestedAtUtc is left null (central sets it at
        // ingest) and ForwardState is dropped (site-storage-only, never on the
        // wire).
        return AuditRowProjection.Recompose(new AuditRowProjection.AuditRowValues(
            EventId: Guid.Parse(dto.EventId),
            OccurredAtUtc: DateTime.SpecifyKind(dto.OccurredAtUtc.ToDateTime(), DateTimeKind.Utc),
            IngestedAtUtc: null,
            Channel: AuditRowProjection.ParseEnum<AuditChannel>(dto.Channel, AuditChannel.ApiInbound),
            Kind: AuditRowProjection.ParseEnum<AuditKind>(dto.Kind, AuditKind.InboundRequest),
            Status: AuditRowProjection.ParseEnum<AuditStatus>(dto.Status, AuditStatus.Submitted),
            CorrelationId: NullIfEmpty(dto.CorrelationId) is { } cid ? Guid.Parse(cid) : null,
            ExecutionId: NullIfEmpty(dto.ExecutionId) is { } eid ? Guid.Parse(eid) : null,
            ParentExecutionId: NullIfEmpty(dto.ParentExecutionId) is { } pid ? Guid.Parse(pid) : null,
            SourceSiteId: NullIfEmpty(dto.SourceSiteId),
            SourceNode: NullIfEmpty(dto.SourceNode),
            SourceInstanceId: NullIfEmpty(dto.SourceInstanceId),
            SourceScript: NullIfEmpty(dto.SourceScript),
            Actor: NullIfEmpty(dto.Actor),
            Target: NullIfEmpty(dto.Target),
            HttpStatus: dto.HttpStatus,
            DurationMs: dto.DurationMs,
            ErrorMessage: NullIfEmpty(dto.ErrorMessage),
            ErrorDetail: NullIfEmpty(dto.ErrorDetail),
            RequestSummary: NullIfEmpty(dto.RequestSummary),
            ResponseSummary: NullIfEmpty(dto.ResponseSummary),
            PayloadTruncated: dto.PayloadTruncated,
            Extra: NullIfEmpty(dto.Extra)));
    }

    private static string? NullIfEmpty(string? value) =>
        string.IsNullOrEmpty(value) ? null : value;

    private static DateTime EnsureUtc(DateTime value) =>
        value.Kind == DateTimeKind.Utc
            ? value
            : DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc);
}
