using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;

/// <summary>
/// Transitional canonical ⇄ 24-column shim for the two AuditLog storage
/// implementations (site SQLite, central SQL Server). Keeps the existing
/// 24-column tables UNCHANGED; this helper decomposes a canonical
/// <see cref="ZB.MOM.WW.Audit.AuditEvent"/> into the typed domain values the
/// columns expect (Channel/Kind/Status enums + the <see cref="AuditDetails"/>
/// fields) and recomposes a canonical record from those column values.
/// </summary>
/// <remarks>
/// <para>
/// The canonical record
/// only carries Action/Category/Outcome at the top level and stashes every
/// ScadaBridge domain field inside <c>DetailsJson</c>; the legacy storage rows
/// carry the domain fields as typed columns. This shim bridges the two without
/// any schema change.
/// </para>
/// <para>
/// <c>ForwardState</c> is deliberately NOT part of this projection — it is a
/// site-storage-only concern carried alongside the canonical record by the site
/// SQLite writer, never inside <c>DetailsJson</c> and never on a central row.
/// </para>
/// </remarks>
public static class AuditRowProjection
{
    /// <summary>
    /// The decomposed domain view of a canonical <see cref="AuditEvent"/> — the
    /// values the 24 storage columns expect. Built by <see cref="Decompose"/> from
    /// the canonical top-level fields plus the <see cref="AuditDetails"/> bag.
    /// </summary>
    public readonly record struct AuditRowValues(
        Guid EventId,
        DateTime OccurredAtUtc,
        DateTime? IngestedAtUtc,
        AuditChannel Channel,
        AuditKind Kind,
        AuditStatus Status,
        Guid? CorrelationId,
        Guid? ExecutionId,
        Guid? ParentExecutionId,
        string? SourceSiteId,
        string? SourceNode,
        string? SourceInstanceId,
        string? SourceScript,
        string? Actor,
        string? Target,
        int? HttpStatus,
        int? DurationMs,
        string? ErrorMessage,
        string? ErrorDetail,
        string? RequestSummary,
        string? ResponseSummary,
        bool PayloadTruncated,
        string? Extra);

    /// <summary>
    /// Decomposes a canonical record into the typed column values. Channel/Kind/Status
    /// come from <c>DetailsJson</c> (the strings written by
    /// <see cref="ScadaBridgeAuditEventFactory"/>); a missing/unparseable discriminator
    /// falls back to the first enum member (defensive — production rows always carry them).
    /// </summary>
    /// <param name="evt">The canonical audit event to decompose.</param>
    /// <returns>An <see cref="AuditRowValues"/> struct containing the typed column values extracted from the event.</returns>
    public static AuditRowValues Decompose(AuditEvent evt)
    {
        ArgumentNullException.ThrowIfNull(evt);
        var d = AuditDetailsCodec.Deserialize(evt.DetailsJson);

        var channel = ParseEnum(d.Channel, AuditChannel.ApiInbound);
        var kind = ParseEnum(d.Kind, AuditKind.InboundRequest);
        var status = ParseEnum(d.Status, AuditStatus.Submitted);

        // The canonical OccurredAtUtc is UTC by construction; columns store a
        // Kind=Utc DateTime so downstream UTC/local conversions are safe
        // (CLAUDE.md: "All timestamps are UTC throughout the system.").
        var occurred = DateTime.SpecifyKind(evt.OccurredAtUtc.UtcDateTime, DateTimeKind.Utc);
        DateTime? ingested = d.IngestedAtUtc.HasValue
            ? DateTime.SpecifyKind(d.IngestedAtUtc.Value.UtcDateTime, DateTimeKind.Utc)
            : null;

        return new AuditRowValues(
            EventId: evt.EventId,
            OccurredAtUtc: occurred,
            IngestedAtUtc: ingested,
            Channel: channel,
            Kind: kind,
            Status: status,
            CorrelationId: evt.CorrelationId,
            ExecutionId: d.ExecutionId,
            ParentExecutionId: d.ParentExecutionId,
            SourceSiteId: d.SourceSiteId,
            SourceNode: evt.SourceNode,
            SourceInstanceId: d.SourceInstanceId,
            SourceScript: d.SourceScript,
            // Canonical Actor is a required non-null string; an empty Actor maps
            // back to a NULL column (legacy rows stored null for system/anon).
            Actor: string.IsNullOrEmpty(evt.Actor) ? null : evt.Actor,
            Target: evt.Target,
            HttpStatus: d.HttpStatus,
            DurationMs: d.DurationMs,
            ErrorMessage: d.ErrorMessage,
            ErrorDetail: d.ErrorDetail,
            RequestSummary: d.RequestSummary,
            ResponseSummary: d.ResponseSummary,
            PayloadTruncated: d.PayloadTruncated,
            Extra: d.Extra);
    }

    /// <summary>
    /// Recomposes a canonical <see cref="AuditEvent"/> from the typed column values read
    /// back from storage. The inverse of <see cref="Decompose"/>: Action/Category/Outcome
    /// are rebuilt via the field builders / outcome projector, and every domain field is
    /// re-serialized into <c>DetailsJson</c> via <see cref="AuditDetailsCodec"/>.
    /// </summary>
    /// <param name="v">The typed column values to recompose into a canonical event.</param>
    /// <returns>A reconstructed canonical <see cref="AuditEvent"/> with domain fields re-serialized into <c>DetailsJson</c>.</returns>
    public static AuditEvent Recompose(in AuditRowValues v)
    {
        var details = new AuditDetails
        {
            Channel = v.Channel.ToString(),
            Kind = v.Kind.ToString(),
            Status = v.Status.ToString(),
            ExecutionId = v.ExecutionId,
            ParentExecutionId = v.ParentExecutionId,
            SourceSiteId = v.SourceSiteId,
            SourceInstanceId = v.SourceInstanceId,
            SourceScript = v.SourceScript,
            HttpStatus = v.HttpStatus,
            DurationMs = v.DurationMs,
            ErrorMessage = v.ErrorMessage,
            ErrorDetail = v.ErrorDetail,
            RequestSummary = v.RequestSummary,
            ResponseSummary = v.ResponseSummary,
            PayloadTruncated = v.PayloadTruncated,
            Extra = v.Extra,
            IngestedAtUtc = v.IngestedAtUtc.HasValue
                ? new DateTimeOffset(DateTime.SpecifyKind(v.IngestedAtUtc.Value, DateTimeKind.Utc))
                : null,
        };

        return new AuditEvent
        {
            EventId = v.EventId,
            OccurredAtUtc = new DateTimeOffset(
                DateTime.SpecifyKind(v.OccurredAtUtc, DateTimeKind.Utc)),
            Actor = v.Actor ?? string.Empty,
            Action = AuditFieldBuilders.BuildAction(v.Channel, v.Kind),
            Category = AuditFieldBuilders.BuildCategory(v.Channel),
            Outcome = AuditOutcomeProjector.Project(v.Status, v.Kind),
            Target = v.Target,
            SourceNode = v.SourceNode,
            CorrelationId = v.CorrelationId,
            DetailsJson = AuditDetailsCodec.Serialize(details),
        };
    }

    /// <summary>
    /// Returns a copy of <paramref name="evt"/> with the central-side ingest timestamp
    /// stamped into its <c>DetailsJson</c> (<see cref="AuditDetails.IngestedAtUtc"/>).
    /// <c>IngestedAtUtc</c> is a DetailsJson field on the canonical
    /// record, so the central ingest paths stamp it here rather than on a top-level
    /// property as the legacy bespoke record allowed.
    /// </summary>
    /// <param name="evt">The canonical audit event to update.</param>
    /// <param name="ingestedAtUtc">The central-side ingest timestamp to stamp into the event.</param>
    /// <returns>A new <see cref="AuditEvent"/> with <c>IngestedAtUtc</c> set inside <c>DetailsJson</c>.</returns>
    public static AuditEvent WithIngestedAtUtc(AuditEvent evt, DateTimeOffset ingestedAtUtc)
    {
        ArgumentNullException.ThrowIfNull(evt);
        var d = AuditDetailsCodec.Deserialize(evt.DetailsJson) with
        {
            IngestedAtUtc = ingestedAtUtc.ToUniversalTime(),
        };
        return evt with { DetailsJson = AuditDetailsCodec.Serialize(d) };
    }

    /// <summary>
    /// Case-sensitive <see cref="Enum.TryParse{TEnum}"/> with a caller-supplied fallback.
    /// Returns <paramref name="fallback"/> when <paramref name="value"/> is null, empty,
    /// or does not match any declared member name — so callers never throw on an
    /// unknown/renamed enum string (legacy or corrupt rows degrade gracefully).
    /// </summary>
    /// <typeparam name="TEnum">The enum type to parse into.</typeparam>
    /// <param name="value">The string to parse; null or empty triggers the fallback.</param>
    /// <param name="fallback">Value returned when <paramref name="value"/> is null, empty, or unrecognised.</param>
    /// <returns>The parsed <typeparamref name="TEnum"/> value, or <paramref name="fallback"/> when the input is null, empty, or unrecognised.</returns>
    public static TEnum ParseEnum<TEnum>(string? value, TEnum fallback) where TEnum : struct, Enum
        => !string.IsNullOrEmpty(value) && Enum.TryParse<TEnum>(value, ignoreCase: false, out var parsed)
            ? parsed
            : fallback;
}

/// <summary>
/// Convenience extension that decomposes a canonical <see cref="AuditEvent"/> into its
/// typed 24-field <see cref="AuditRowProjection.AuditRowValues"/> view. Lets callers
/// (and tests) read the ScadaBridge domain fields — Channel/Kind/Status + the DetailsJson
/// fields — as typed properties off a canonical row.
/// </summary>
public static class AuditEventRowExtensions
{
    /// <summary>Decomposes this canonical record into its typed 24-field view.</summary>
    /// <param name="evt">The canonical audit event to decompose.</param>
    /// <returns>An <see cref="AuditRowProjection.AuditRowValues"/> struct with all domain fields extracted from the event.</returns>
    public static AuditRowProjection.AuditRowValues AsRow(this AuditEvent evt)
        => AuditRowProjection.Decompose(evt);
}
