using System.Globalization;
using System.Text.Json;
using Microsoft.Data.SqlClient;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Audit;

/// <summary>
/// Direct-SQL seeding helper for the Audit Log Playwright E2E tests (#23 M7-T16).
///
/// <para>
/// The Playwright suite runs against the live Docker cluster (the same one that
/// answers <c>http://localhost:9000</c>), which talks to the <c>ScadaBridgeConfig</c>
/// database on <c>localhost:1433</c>. <c>infra/mssql/seed-config.sql</c> is off
/// limits per the task's strict rules, so each test inserts its own
/// <c>AuditLog</c> rows at setup time and best-effort deletes them at teardown.
/// </para>
///
/// <para>
/// Rows are tagged with a unique <c>Target</c> prefix derived from the test
/// name + a GUID so the teardown <c>DELETE</c> never touches rows the cluster
/// itself produced. The <c>OccurredAtUtc</c> is pinned to "now" so the default
/// <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Components.Audit.AuditTimeRangePreset.LastHour"/>
/// time-range filter still sees the row after Apply.
/// </para>
///
/// <para>
/// Connection string mirrors the Docker cluster's <c>scadabridge_app</c> account
/// from <c>docker/central-node-a/appsettings.Central.json</c>, with the host
/// pointed at the host-exposed port (<c>localhost:1433</c>). The
/// <c>SCADABRIDGE_PLAYWRIGHT_DB</c> env var lets CI override the connection
/// without recompiling.
/// </para>
/// </summary>
internal static class AuditDataSeeder
{
    /// <summary>
    /// Connection string for the running cluster's configuration DB.
    /// Delegates to <see cref="PlaywrightDbConnection.ConnectionString"/>.
    /// </summary>
    public static string ConnectionString => PlaywrightDbConnection.ConnectionString;

    /// <summary>
    /// Inserts a single audit row into the canonical <c>AuditLog</c> table. After the
    /// <c>CollapseAuditLogToCanonical</c> migration the typed audit fields live inside
    /// <c>DetailsJson</c> (camelCase), and <c>Kind</c>/<c>Status</c>/<c>SourceSiteId</c>/
    /// <c>ExecutionId</c>/<c>ParentExecutionId</c>/<c>IngestedAtUtc</c> are computed columns
    /// derived from it — so this seeder writes only the 10 stored columns plus a
    /// <c>DetailsJson</c> bag matching the production codec, and lets the computed columns
    /// derive automatically. All optional fields are nullable so individual tests can shape
    /// the row to whatever payload they need for their drawer/grid assertions.
    /// </summary>
    public static async Task InsertAuditEventAsync(
        Guid eventId,
        DateTime occurredAtUtc,
        string channel,
        string kind,
        string status,
        string? sourceSiteId = null,
        string? target = null,
        string? actor = null,
        Guid? correlationId = null,
        Guid? executionId = null,
        Guid? parentExecutionId = null,
        int? httpStatus = null,
        int? durationMs = null,
        string? errorMessage = null,
        string? requestSummary = null,
        string? responseSummary = null,
        string? extra = null,
        CancellationToken ct = default)
    {
        // Typed audit fields ride inside DetailsJson (camelCase, nulls omitted) exactly as
        // the production AuditDetailsCodec writes them; the computed columns read these JSON
        // paths ($.kind, $.status, $.sourceSiteId, $.executionId, $.parentExecutionId,
        // $.ingestedAtUtc). Property order is irrelevant — the readers look up by name.
        var details = new Dictionary<string, object?>
        {
            ["channel"] = channel,
            ["kind"] = kind,
            ["status"] = status,
        };
        if (executionId is { } ex) details["executionId"] = ex;
        if (parentExecutionId is { } pex) details["parentExecutionId"] = pex;
        if (sourceSiteId is not null) details["sourceSiteId"] = sourceSiteId;
        if (httpStatus is { } hs) details["httpStatus"] = hs;
        if (durationMs is { } dm) details["durationMs"] = dm;
        if (errorMessage is not null) details["errorMessage"] = errorMessage;
        if (requestSummary is not null) details["requestSummary"] = requestSummary;
        if (responseSummary is not null) details["responseSummary"] = responseSummary;
        if (extra is not null) details["extra"] = extra;
        details["payloadTruncated"] = false;
        details["ingestedAtUtc"] = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);

        var detailsJson = JsonSerializer.Serialize(details);

        // Action/Outcome/Category derive from (channel, kind, status) exactly as the
        // production canonical factory and the migration's SQL projection do.
        var action = $"{channel}.{kind}";
        var category = channel;
        var outcome =
            kind == "InboundAuthFailure" ? "Denied"
            : status == "Delivered" ? "Success"
            : status is "Failed" or "Parked" or "Discarded" ? "Failure"
            : "Success";

        const string sql = @"
INSERT INTO [AuditLog]
([EventId], [OccurredAtUtc], [Actor], [Action], [Outcome], [Category],
 [Target], [SourceNode], [CorrelationId], [DetailsJson])
VALUES
(@eventId, @occurredAtUtc, @actor, @action, @outcome, @category,
 @target, NULL, @correlationId, @detailsJson);";

        await using var connection = new SqlConnection(ConnectionString);
        await connection.OpenAsync(ct);
        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        cmd.Parameters.AddWithValue("@eventId", eventId);
        cmd.Parameters.AddWithValue("@occurredAtUtc", occurredAtUtc);
        cmd.Parameters.AddWithValue("@actor", (object?)actor ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@action", action);
        cmd.Parameters.AddWithValue("@outcome", outcome);
        cmd.Parameters.AddWithValue("@category", category);
        cmd.Parameters.AddWithValue("@target", (object?)target ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@correlationId", (object?)correlationId ?? DBNull.Value);
        cmd.Parameters.AddWithValue("@detailsJson", detailsJson);

        await cmd.ExecuteNonQueryAsync(ct);
    }

    /// <summary>
    /// Best-effort cleanup. Deletes every <c>AuditLog</c> row whose <c>Target</c>
    /// starts with <paramref name="targetPrefix"/>. Swallows all errors — a
    /// stuck row carrying a random GUID suffix does not collide with future
    /// runs and tests should not fail teardown.
    /// </summary>
    public static async Task DeleteByTargetPrefixAsync(string targetPrefix, CancellationToken ct = default)
    {
        try
        {
            await using var connection = new SqlConnection(ConnectionString);
            await connection.OpenAsync(ct);
            await using var cmd = connection.CreateCommand();
            cmd.CommandText = "DELETE FROM [AuditLog] WHERE [Target] LIKE @prefix";
            cmd.Parameters.AddWithValue("@prefix", targetPrefix + "%");
            await cmd.ExecuteNonQueryAsync(ct);
        }
        catch
        {
            // Best-effort — the prefix carries a GUID so the rows are unique to
            // this test run and won't collide on the next pass.
        }
    }

    /// <summary>
    /// Probe whether the configuration DB is reachable. Tests gate their
    /// per-test setup on this; when the cluster is down the test fails with a
    /// clear "MSSQL unavailable" message instead of an opaque SqlException.
    /// </summary>
    public static async Task<bool> IsAvailableAsync(CancellationToken ct = default)
    {
        try
        {
            await using var connection = new SqlConnection(ConnectionString);
            await connection.OpenAsync(ct);
            return true;
        }
        catch
        {
            return false;
        }
    }
}
