using System.Text.Json;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;

namespace ZB.MOM.WW.MxGateway.Server.Security.Audit;

/// <summary>
/// Adapter that overrides the shared library's <see cref="IApiKeyAuditStore"/> so that
/// library-emitted API-key audit events (CLI / admin verbs from
/// <c>ApiKeyAdminCommands</c>) are canonicalized onto <see cref="AuditEvent"/> and routed
/// through the gateway's <see cref="IAuditWriter"/> into the canonical
/// <c>audit_event</c> store.
/// </summary>
/// <remarks>
/// Overriding the registered <see cref="IApiKeyAuditStore"/> is the ONLY way to
/// canonicalize the library-internal <c>ApiKeyAdminCommands</c> events, since that type
/// cannot be edited. <see cref="ListRecentAsync"/> reads back from the canonical store
/// and maps each <see cref="AuditEvent"/> to an <see cref="ApiKeyAuditEntry"/> so the
/// existing dashboard "recent audit" view (and the CLI/store tests) keep working through
/// this same seam, unchanged.
/// <para>
/// The library's own <c>api_key_audit</c> table is left in place but UNUSED — nothing
/// writes to it once this adapter overrides the library's <c>SqliteApiKeyAuditStore</c>
/// registration.
/// </para>
/// </remarks>
public sealed class CanonicalForwardingApiKeyAuditStore(
    IAuditWriter auditWriter,
    SqliteCanonicalAuditStore store) : IApiKeyAuditStore
{
    /// <summary>The canonical <see cref="AuditEvent.Category"/> assigned to API-key events.</summary>
    public const string ApiKeyCategory = "ApiKey";

    /// <summary>Actor used for the library's keyless <c>init-db</c> event.</summary>
    private const string SystemActor = "system";

    /// <summary>Actor used for any other keyless (CLI-originated) library event.</summary>
    private const string CliActor = "cli";

    /// <summary>The library event type that denotes a constraint denial.</summary>
    private const string ConstraintDeniedEventType = "constraint-denied";

    /// <summary>The library's keyless schema-init event type.</summary>
    private const string InitDbEventType = "init-db";

    /// <summary>Canonicalizes a library-emitted API-key audit entry onto <see cref="AuditEvent"/> and writes it via <see cref="IAuditWriter"/>.</summary>
    /// <param name="entry">The library's API-key audit entry to forward.</param>
    /// <param name="ct">Token to observe for cancellation.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(entry);

        AuditEvent auditEvent = new()
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = entry.CreatedUtc,
            // Keyless library events: init-db is system-originated; any other keyless event
            // is a CLI/admin verb run without an authenticated principal.
            Actor = entry.KeyId
                ?? (entry.EventType == InitDbEventType ? SystemActor : CliActor),
            Action = entry.EventType,
            Outcome = entry.EventType == ConstraintDeniedEventType
                ? AuditOutcome.Denied
                : AuditOutcome.Success,
            Category = ApiKeyCategory,
            Target = entry.KeyId,
            SourceNode = entry.RemoteAddress,
            CorrelationId = null,
            DetailsJson = WrapDetails(entry.Details),
        };

        // Best-effort: IAuditWriter swallows/logs failures, so this never throws.
        await auditWriter.WriteAsync(auditEvent, ct).ConfigureAwait(false);
    }

    /// <summary>Reads back the most recent entries from the canonical audit store and maps each one to an <see cref="ApiKeyAuditEntry"/>.</summary>
    /// <param name="limit">The maximum number of entries to return.</param>
    /// <param name="ct">Token to observe for cancellation.</param>
    /// <returns>Up to <paramref name="limit"/> recent API-key audit entries.</returns>
    public async Task<IReadOnlyList<ApiKeyAuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
    {
        IReadOnlyList<AuditEvent> events = await store.ListRecentAsync(limit, ct).ConfigureAwait(false);

        ApiKeyAuditEntry[] entries = new ApiKeyAuditEntry[events.Count];
        for (int index = 0; index < events.Count; index++)
        {
            AuditEvent auditEvent = events[index];
            entries[index] = new ApiKeyAuditEntry(
                KeyId: auditEvent.Actor switch
                {
                    // Keyless library events were mapped to the system/cli sentinel actors on the
                    // way in; map them back to a null KeyId so the dashboard view is faithful.
                    SystemActor or CliActor => null,
                    string actor => actor,
                },
                EventType: auditEvent.Action,
                RemoteAddress: auditEvent.SourceNode,
                CreatedUtc: auditEvent.OccurredAtUtc,
                Details: UnwrapDetails(auditEvent.DetailsJson));
        }

        return entries;
    }

    /// <summary>
    /// Wraps a free-form library detail string into the canonical
    /// <c>{"detail": "&lt;escaped&gt;"}</c> JSON envelope, or null when there is no detail.
    /// </summary>
    private static string? WrapDetails(string? details)
    {
        if (details is null)
        {
            return null;
        }

        return JsonSerializer.Serialize(new Dictionary<string, string> { ["detail"] = details });
    }

    /// <summary>
    /// Unwraps the canonical detail envelope back to the original free-form string. Falls
    /// back to the raw JSON when it is not a recognised <c>{"detail": ...}</c> envelope, so
    /// directly-emitted canonical events (whose DetailsJson is richer) still surface text.
    /// </summary>
    private static string? UnwrapDetails(string? detailsJson)
    {
        if (string.IsNullOrEmpty(detailsJson))
        {
            return null;
        }

        try
        {
            using JsonDocument document = JsonDocument.Parse(detailsJson);
            if (document.RootElement.ValueKind == JsonValueKind.Object
                && document.RootElement.TryGetProperty("detail", out JsonElement detail)
                && detail.ValueKind == JsonValueKind.String)
            {
                return detail.GetString();
            }
        }
        catch (JsonException)
        {
            // Not JSON we recognise; surface the raw payload below.
        }

        return detailsJson;
    }
}
