using System.Globalization;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

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

/// <summary>
/// Reusable single-<see cref="AuditEvent"/> detail body. Extracted verbatim
/// from <see cref="AuditDrilldownDrawer"/> so
/// the drawer and the execution-tree node-detail modal render a row's detail
/// identically. Renders the read-only field list, the conditional
/// Error/Request/Response/Extra subsections, and the action buttons (Copy as
/// cURL, Show all events for this operation, View this/parent execution, View
/// execution chain). The component is fully presentational apart from the
/// clipboard interop and drill-back navigation it owns; the host owns its
/// surrounding chrome.
///
/// <para>
/// <b>Body rendering.</b> Request/Response/Extra summaries are strings.
/// JSON is pretty-printed when it parses; falls back to verbatim otherwise.
/// DbOutbound payloads carry a <c>{sql, parameters}</c> JSON shape and get a
/// SQL code block plus a parameter definition list. Syntax highlighting is
/// CSS-class-only (<c>language-sql</c>); no JS library is loaded — Blazor
/// Server + Bootstrap only per the project's UI rules.
/// </para>
///
/// <para>
/// <b>Redaction badges.</b> The audit pipeline replaces redacted values
/// with the literal sentinels <c>&lt;redacted&gt;</c> or
/// <c>&lt;redacted: redactor error&gt;</c> (see Component-AuditLog.md
/// §Redaction). A yellow "Redacted" badge surfaces on a body section when
/// its text contains either sentinel — no un-redaction or counting.
/// </para>
///
/// <para>
/// <b>Copy as cURL.</b> Best-effort: the URL comes from <c>Target</c>;
/// when the RequestSummary parses as <c>{headers, body}</c>, headers are
/// folded into <c>-H</c> flags and the body into <c>--data-raw</c>. The
/// command is written to the system clipboard via
/// <see cref="IJSRuntime.InvokeVoidAsync(string, object?[])"/>. The button
/// is only surfaced for API channels (ApiOutbound / ApiInbound).
/// </para>
///
/// <para>
/// <b>Drill-back.</b> When <see cref="AuditEvent.CorrelationId"/> is set,
/// the "Show all events" button navigates to
/// <c>/audit/log?correlationId={id}</c>. Likewise, when
/// <see cref="AuditEvent.ExecutionId"/> is set the "View this execution"
/// button navigates to <c>/audit/log?executionId={id}</c>. Likewise, when
/// <see cref="AuditEvent.ParentExecutionId"/> is set the "View parent
/// execution" button navigates to <c>/audit/log?executionId={parentId}</c>
/// — the spawner's id used as the per-run drill-in target. All are deep
/// links the Audit Log page deserializes on init and auto-loads.
/// </para>
/// </summary>
public partial class AuditEventDetail
{
    [Inject] private IJSRuntime JS { get; set; } = null!;
    [Inject] private NavigationManager Navigation { get; set; } = null!;

    /// <summary>
    /// The row to render. Required and non-null — the host (drawer or modal)
    /// only mounts this component once it has a row to show.
    /// </summary>
    [Parameter, EditorRequired] public AuditEventView Event { get; set; } = null!;

    private const string RedactionSentinel = "<redacted>";
    private const string RedactorErrorSentinel = "<redacted: redactor error>";

    private static bool IsApiChannel(AuditChannel channel)
        => channel is AuditChannel.ApiOutbound or AuditChannel.ApiInbound;

    private static string FormatTimestamp(DateTime utc)
    {
        // Force UTC kind in case the row arrived as Unspecified, then emit
        // round-trip ISO-8601 so audit drilldowns are copy-paste safe.
        var kind = utc.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(utc, DateTimeKind.Utc) : utc;
        return kind.ToString("o", CultureInfo.InvariantCulture);
    }

    private static bool IsRedacted(string? text)
    {
        if (string.IsNullOrEmpty(text)) return false;
        return text.Contains(RedactionSentinel, StringComparison.Ordinal)
            || text.Contains(RedactorErrorSentinel, StringComparison.Ordinal);
    }

    /// <summary>
    /// Channel-aware body renderer. DbOutbound bodies that parse as
    /// <c>{sql, parameters}</c> get a SQL block + parameter list; anything
    /// else falls back to JSON-pretty-print, then plain-text verbatim.
    /// </summary>
    private RenderFragment RenderBody(string body, AuditChannel channel) => builder =>
    {
        // DbOutbound special-case: try to extract {sql, parameters}.
        if (channel == AuditChannel.DbOutbound && TryParseDbBody(body, out var sql, out var parameters))
        {
            builder.OpenElement(0, "pre");
            builder.AddAttribute(1, "class", "bg-light border rounded p-2 mb-2 drawer-pre");
            builder.OpenElement(2, "code");
            // Highlighting is CSS-class-only — no JS library is loaded.
            builder.AddAttribute(3, "class", "language-sql");
            builder.AddContent(4, sql);
            builder.CloseElement();
            builder.CloseElement();

            if (parameters is not null && parameters.Count > 0)
            {
                builder.OpenElement(10, "dl");
                builder.AddAttribute(11, "class", "row mb-0 small");
                builder.AddAttribute(12, "data-test", "sql-parameters");
                // The analyzer (ASP0006) requires literal sequence numbers
                // inside a render fragment. We delegate parameter rendering
                // to a helper fragment that uses a stable @key per entry,
                // so per-row diffing stays correct even though the outer
                // sequence number is fixed.
                builder.AddContent(13, BuildSqlParameterRows(parameters));
                builder.CloseElement();
            }
            return;
        }

        // Generic JSON pretty-print path.
        if (TryPrettyPrintJson(body, out var pretty))
        {
            builder.OpenElement(20, "pre");
            builder.AddAttribute(21, "class", "bg-light border rounded p-2 mb-0 drawer-pre json");
            builder.AddContent(22, pretty);
            builder.CloseElement();
            return;
        }

        // Fallback: verbatim. Wrapping in <pre> preserves whitespace, which
        // is useful when the body is multi-line plain text or a partial JSON.
        builder.OpenElement(30, "pre");
        builder.AddAttribute(31, "class", "bg-light border rounded p-2 mb-0 drawer-pre");
        builder.AddContent(32, body);
        builder.CloseElement();
    };

    private static RenderFragment BuildSqlParameterRows(List<KeyValuePair<string, string>> parameters) => builder =>
    {
        foreach (var kv in parameters)
        {
            // Literal sequence numbers (ASP0006) + per-element SetKey so
            // Blazor's diff is still keyed on parameter name. The "0" base
            // is fine here — each loop iteration produces a disjoint
            // dt/dd pair, and the diff keys on @key, not sequence.
            builder.OpenElement(0, "dt");
            builder.SetKey($"dt-{kv.Key}");
            builder.AddAttribute(1, "class", "col-4 text-muted fw-normal font-monospace");
            builder.AddContent(2, kv.Key);
            builder.CloseElement();

            builder.OpenElement(3, "dd");
            builder.SetKey($"dd-{kv.Key}");
            builder.AddAttribute(4, "class", "col-8 font-monospace");
            builder.AddContent(5, kv.Value);
            builder.CloseElement();
        }
    };

    private static bool TryPrettyPrintJson(string text, out string formatted)
    {
        formatted = text;
        try
        {
            using var doc = JsonDocument.Parse(text);
            formatted = JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
            return true;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    private static string PrettyPrintJson(string text)
        => TryPrettyPrintJson(text, out var pretty) ? pretty : text;

    /// <summary>
    /// Best-effort parse of a DbOutbound <c>{sql, parameters}</c> body.
    /// Returns true only when the JSON has a string <c>sql</c> property;
    /// <c>parameters</c> is treated as an optional object whose values
    /// stringify to scalar text.
    /// </summary>
    private static bool TryParseDbBody(string text, out string sql, out List<KeyValuePair<string, string>>? parameters)
    {
        sql = string.Empty;
        parameters = null;
        try
        {
            using var doc = JsonDocument.Parse(text);
            if (doc.RootElement.ValueKind != JsonValueKind.Object) return false;
            if (!doc.RootElement.TryGetProperty("sql", out var sqlProp) || sqlProp.ValueKind != JsonValueKind.String)
                return false;
            sql = sqlProp.GetString() ?? string.Empty;

            if (doc.RootElement.TryGetProperty("parameters", out var paramsProp)
                && paramsProp.ValueKind == JsonValueKind.Object)
            {
                parameters = new List<KeyValuePair<string, string>>();
                foreach (var p in paramsProp.EnumerateObject())
                {
                    parameters.Add(new KeyValuePair<string, string>(p.Name, StringifyJsonValue(p.Value)));
                }
            }
            return true;
        }
        catch (JsonException)
        {
            return false;
        }
    }

    private static string StringifyJsonValue(JsonElement value) => value.ValueKind switch
    {
        JsonValueKind.String => value.GetString() ?? string.Empty,
        JsonValueKind.Null => "null",
        JsonValueKind.True => "true",
        JsonValueKind.False => "false",
        JsonValueKind.Number => value.GetRawText(),
        _ => value.GetRawText(),
    };

    private async Task CopyCurl()
    {
        var curl = BuildCurlCommand(Event);
        try
        {
            await JS.InvokeVoidAsync("navigator.clipboard.writeText", curl);
        }
        catch
        {
            // Clipboard interop can fail (denied permission, prerender, etc.).
            // The component stays mounted; the failure surfaces in the dev
            // console only — we deliberately do not toast here because the
            // parent page owns toast state.
        }
    }

    private void ShowAllForOperation()
    {
        if (Event.CorrelationId is not { } corr) return;
        var uri = $"/audit/log?correlationId={corr}";
        Navigation.NavigateTo(uri);
    }

    /// <summary>
    /// Drill-in to every audit row sharing this row's <see cref="AuditEvent.ExecutionId"/>
    /// — the universal per-run correlation value, distinct from the per-operation
    /// CorrelationId drill-back above. Navigates to <c>/audit/log?executionId={id}</c>,
    /// which the page parses on init and auto-loads. The button is only rendered
    /// when <see cref="AuditEvent.ExecutionId"/> is non-null, so this is total.
    /// </summary>
    private void ViewThisExecution()
    {
        if (Event.ExecutionId is not { } exec) return;
        var uri = $"/audit/log?executionId={exec}";
        Navigation.NavigateTo(uri);
    }

    /// <summary>
    /// Drill-in to the spawner execution: a routed (child) row carries a non-null
    /// <see cref="AuditEvent.ParentExecutionId"/>. Navigates to
    /// <c>/audit/log?executionId={ParentExecutionId}</c> so the user sees the
    /// spawner execution's own rows — the parent's id becomes the <c>?executionId=</c>
    /// drill-in target. The button is only rendered when
    /// <see cref="AuditEvent.ParentExecutionId"/> is non-null, so this is total.
    /// </summary>
    private void ViewParentExecution()
    {
        if (Event.ParentExecutionId is not { } parentExec) return;
        var uri = $"/audit/log?executionId={parentExec}";
        Navigation.NavigateTo(uri);
    }

    /// <summary>
    /// Drill-in to the execution-chain TREE view (Audit Log ParentExecutionId
    /// feature). Navigates to
    /// <c>/audit/execution-tree?executionId={ExecutionId}</c> — the tree page
    /// resolves the whole chain rooted at the topmost ancestor and renders it
    /// expandably, with this row's execution highlighted. The button is only
    /// rendered when <see cref="AuditEvent.ExecutionId"/> is non-null, so this
    /// is total.
    /// </summary>
    private void ViewExecutionChain()
    {
        if (Event.ExecutionId is not { } exec) return;
        var uri = $"/audit/execution-tree?executionId={exec}";
        Navigation.NavigateTo(uri);
    }

    /// <summary>
    /// Build a cURL command from an audit event. The URL comes from
    /// <c>Target</c>; when the RequestSummary parses as
    /// <c>{headers, body, method?}</c>, headers fold into <c>-H</c> flags
    /// and the body into <c>--data-raw</c>. Default method is POST for
    /// outbound audit rows — the audit pipeline does not always capture
    /// the verb explicitly.
    /// </summary>
    private static string BuildCurlCommand(AuditEventView ev)
    {
        var sb = new StringBuilder();
        sb.Append("curl");

        string method = "POST";
        List<KeyValuePair<string, string>>? headers = null;
        string? body = null;

        if (!string.IsNullOrEmpty(ev.RequestSummary))
        {
            TryExtractCurlPartsFromJson(ev.RequestSummary!, ref method, ref headers, ref body);
        }

        sb.Append(' ').Append("-X ").Append(method);

        if (headers is not null)
        {
            foreach (var (name, value) in headers)
            {
                sb.Append(' ').Append("-H ");
                sb.Append(QuoteShellArg($"{name}: {value}"));
            }
        }

        if (!string.IsNullOrEmpty(body))
        {
            sb.Append(' ').Append("--data-raw ");
            sb.Append(QuoteShellArg(body!));
        }

        var url = ev.Target ?? string.Empty;
        sb.Append(' ').Append(QuoteShellArg(url));
        return sb.ToString();
    }

    private static void TryExtractCurlPartsFromJson(
        string requestSummary,
        ref string method,
        ref List<KeyValuePair<string, string>>? headers,
        ref string? body)
    {
        try
        {
            using var doc = JsonDocument.Parse(requestSummary);
            if (doc.RootElement.ValueKind != JsonValueKind.Object) return;

            if (doc.RootElement.TryGetProperty("method", out var m) && m.ValueKind == JsonValueKind.String)
            {
                method = m.GetString() ?? method;
            }
            if (doc.RootElement.TryGetProperty("headers", out var hs) && hs.ValueKind == JsonValueKind.Object)
            {
                headers = new List<KeyValuePair<string, string>>();
                foreach (var h in hs.EnumerateObject())
                {
                    var value = h.Value.ValueKind == JsonValueKind.String
                        ? h.Value.GetString() ?? string.Empty
                        : h.Value.GetRawText();
                    headers.Add(new KeyValuePair<string, string>(h.Name, value));
                }
            }
            if (doc.RootElement.TryGetProperty("body", out var b))
            {
                body = b.ValueKind == JsonValueKind.String
                    ? b.GetString()
                    : b.GetRawText();
            }
        }
        catch (JsonException)
        {
            // RequestSummary wasn't the expected {headers, body} shape —
            // just produce a bare cURL with no body/headers.
        }
    }

    /// <summary>
    /// Quote a single shell argument with single quotes, escaping embedded
    /// single quotes via the standard <c>'\''</c> idiom. This is the same
    /// quoting strategy curl examples use across man pages.
    /// </summary>
    private static string QuoteShellArg(string value)
    {
        if (string.IsNullOrEmpty(value)) return "''";
        var escaped = value.Replace("'", "'\\''", StringComparison.Ordinal);
        return $"'{escaped}'";
    }
}
