using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Communication.Grpc;
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;

namespace ScadaLink.AuditLog.Telemetry;

/// <summary>
/// Bridges Audit Log (#23) rows between the in-process <see cref="AuditEvent"/> record
/// and the wire-format <see cref="AuditEventDto"/> exchanged over the
/// <c>IngestAuditEvents</c> RPC.
/// </summary>
/// <remarks>
/// <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 AuditEventMapper
{
    /// <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>
    public static AuditEventDto ToDto(AuditEvent evt)
    {
        ArgumentNullException.ThrowIfNull(evt);

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

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

        if (evt.DurationMs.HasValue)
        {
            dto.DurationMs = evt.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>
    public static AuditEvent FromDto(AuditEventDto dto)
    {
        ArgumentNullException.ThrowIfNull(dto);

        return new AuditEvent
        {
            EventId = Guid.Parse(dto.EventId),
            OccurredAtUtc = DateTime.SpecifyKind(dto.OccurredAtUtc.ToDateTime(), DateTimeKind.Utc),
            IngestedAtUtc = null,
            Channel = Enum.Parse<AuditChannel>(dto.Channel),
            Kind = Enum.Parse<AuditKind>(dto.Kind),
            CorrelationId = NullIfEmpty(dto.CorrelationId) is { } cid ? Guid.Parse(cid) : null,
            SourceSiteId = NullIfEmpty(dto.SourceSiteId),
            SourceInstanceId = NullIfEmpty(dto.SourceInstanceId),
            SourceScript = NullIfEmpty(dto.SourceScript),
            Actor = NullIfEmpty(dto.Actor),
            Target = NullIfEmpty(dto.Target),
            Status = Enum.Parse<AuditStatus>(dto.Status),
            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),
            ForwardState = null
        };
    }

    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);
}
