use serde_json::Value;

pub(super) fn render_schema_debug_html(payload: &Value) -> String {
    let service = string_field(payload, "service").unwrap_or("athena-auth");
    let debug_endpoint = string_field(payload, "debug_endpoint").unwrap_or("/debug/schema");
    let generated_at = string_field(payload, "generated_at").unwrap_or("(unknown)");
    let version = string_field(payload, "version").unwrap_or("(unknown)");
    let raw_payload = serde_json::to_string_pretty(payload).unwrap_or_else(|_| payload.to_string());
    let raw_payload_lines = raw_payload.lines().count();
    let raw_payload_size = format_bytes(raw_payload.len());
    let runtime = object_field(payload, "runtime");
    let request = object_field(payload, "request");
    let current_probe = object_field(payload, "current_tenant_probe");
    let privileged = object_field(payload, "privileged_debug");

    let summary_badges = [
        render_badge(
            "status",
            string_field(payload, "status").unwrap_or("unknown"),
        ),
        render_badge(
            "request",
            request
                .and_then(|value| string_field(value, "resolution_status"))
                .unwrap_or("unknown"),
        ),
        render_badge(
            "probe",
            current_probe
                .and_then(|value| string_field(value, "status"))
                .unwrap_or("unknown"),
        ),
        render_badge(
            "privileged",
            privileged
                .and_then(|value| bool_field(value, "enabled"))
                .map(|value| if value { "enabled" } else { "locked" })
                .unwrap_or("unknown"),
        ),
        render_badge(
            "multitenancy",
            runtime
                .and_then(|value| bool_field(value, "multitenancy_enabled"))
                .map(|value| if value { "enabled" } else { "single-tenant" })
                .unwrap_or("unknown"),
        ),
        render_badge("payload", raw_payload_size.as_str()),
        render_badge("lines", raw_payload_lines.to_string().as_str()),
    ]
    .join("");

    let overview_rows = [
        ("Service", service.to_string()),
        ("Version", version.to_string()),
        ("Debug endpoint", debug_endpoint.to_string()),
        ("Generated at", generated_at.to_string()),
        (
            "Tenant header",
            runtime
                .and_then(|value| string_field(value, "tenant_header_name"))
                .unwrap_or("(not returned)")
                .to_string(),
        ),
        (
            "Resolved tenant",
            request
                .and_then(|value| string_field(value, "resolved_tenant"))
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Probe tenant",
            current_probe
                .and_then(|value| string_field(value, "tenant_id"))
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Privileged access reason",
            privileged
                .and_then(|value| string_field(value, "access_reason"))
                .unwrap_or("(unknown)")
                .to_string(),
        ),
    ];

    let request_section = render_request_section(request);
    let search_path_section = render_search_path_section(current_probe);
    let table_resolution_section = render_table_resolution_section(current_probe);
    let tenant_configs_section = render_tenant_configs_section(payload.get("tenant_configs"));
    let runtime_features_article = render_runtime_features_article(runtime);
    let origin_debug_article = render_origin_debug_article(payload.get("origin_rejection_debug"));
    let privileged_debug_article = render_privileged_debug_article(privileged);
    let raw_payload_section = render_raw_payload_section(raw_payload.as_str());

    format!(
        r#"<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{title}</title>
  <style>
    :root {{
      color-scheme: light;
      --bg: #f4f5f7;
      --panel: #ffffff;
      --ink: #111827;
      --muted: #6b7280;
      --line: #d1d5db;
      --ok: #166534;
      --warn: #92400e;
      --bad: #991b1b;
      --pill: #e5e7eb;
      --accent: #0f172a;
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: linear-gradient(180deg, #eef2ff 0%, var(--bg) 18%, var(--bg) 100%);
      color: var(--ink);
    }}
    main {{
      width: min(1400px, calc(100% - 32px));
      margin: 0 auto;
      padding: 24px 0 48px;
    }}
    .hero {{
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 20px;
      padding: 24px;
      box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
    }}
    h1, h2, h3 {{ margin: 0; }}
    h1 {{ font-size: 28px; }}
    h2 {{ font-size: 18px; margin-bottom: 12px; }}
    p {{ margin: 8px 0 0; color: var(--muted); }}
    .badges {{
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      margin-top: 16px;
    }}
    .badge {{
      display: inline-flex;
      gap: 6px;
      align-items: center;
      border-radius: 999px;
      padding: 7px 12px;
      background: var(--pill);
      border: 1px solid var(--line);
      font-size: 12px;
      font-weight: 700;
    }}
    .badge.ok {{ color: var(--ok); border-color: #86efac; background: #f0fdf4; }}
    .badge.warn {{ color: var(--warn); border-color: #fcd34d; background: #fffbeb; }}
    .badge.bad {{ color: var(--bad); border-color: #fca5a5; background: #fef2f2; }}
    .grid {{
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
      gap: 16px;
      margin-top: 16px;
    }}
    .panel {{
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 18px;
      padding: 18px;
      box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
    }}
    .kv {{
      display: grid;
      grid-template-columns: minmax(140px, 220px) 1fr;
      gap: 10px 14px;
      margin: 0;
    }}
    .kv dt {{
      font-weight: 700;
      color: var(--accent);
    }}
    .kv dd {{
      margin: 0;
      color: var(--muted);
      word-break: break-word;
    }}
    table {{
      width: 100%;
      border-collapse: collapse;
      font-size: 13px;
    }}
    th, td {{
      border: 1px solid var(--line);
      padding: 10px 12px;
      vertical-align: top;
      text-align: left;
    }}
    th {{
      background: #f8fafc;
      color: var(--accent);
    }}
    code, pre {{
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    }}
    pre {{
      margin: 0;
      overflow: auto;
      padding: 16px;
      border-radius: 14px;
      background: #020617;
      color: #e2e8f0;
      font-size: 12px;
      line-height: 1.5;
      max-height: 420px;
    }}
    details {{
      border: 1px solid var(--line);
      border-radius: 14px;
      background: #f8fafc;
      padding: 12px 14px;
    }}
    details + details,
    .stack > * + * {{
      margin-top: 12px;
    }}
    details[open] > summary {{
      margin-bottom: 10px;
    }}
    summary {{
      cursor: pointer;
      font-weight: 700;
      color: var(--accent);
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
      align-items: center;
      justify-content: space-between;
    }}
    .summary-meta {{
      font-size: 12px;
      color: var(--muted);
      font-weight: 600;
    }}
    .section-note {{
      margin-top: 6px;
      font-size: 12px;
      color: var(--muted);
    }}
    .muted {{ color: var(--muted); }}
    .mono {{ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }}
  </style>
</head>
<body>
  <main>
    <section class="hero">
      <h1>Athena Auth Schema Debug</h1>
      <p>HTML view for the full `{debug_endpoint}` payload, including privileged diagnostics.</p>
      <p class="section-note">Big JSON sections are collapsed by default so the page stays usable when privileged diagnostics get huge.</p>
      <div class="badges">{summary_badges}</div>
    </section>

    <section class="grid">
      <article class="panel">
        <h2>Overview</h2>
        {overview}
      </article>
      {request_section}
    </section>

    {search_path_section}
    {table_resolution_section}
    {tenant_configs_section}

    <section class="grid">
      {runtime_features_article}
      {origin_debug_article}
      {privileged_debug_article}
    </section>

    {raw_payload_section}
  </main>
</body>
</html>"#,
        title = escape_html(&format!("{service} {debug_endpoint}")),
        debug_endpoint = escape_html(debug_endpoint),
        summary_badges = summary_badges,
        overview = render_kv_list(&overview_rows),
        request_section = request_section,
        search_path_section = search_path_section,
        table_resolution_section = table_resolution_section,
        tenant_configs_section = tenant_configs_section,
        runtime_features_article = runtime_features_article,
        origin_debug_article = origin_debug_article,
        privileged_debug_article = privileged_debug_article,
        raw_payload_section = raw_payload_section,
    )
}

pub(super) fn render_forbidden_schema_debug_html(
    access_reason: &str,
    query_param_present: bool,
    unlock_hint: &str,
) -> String {
    format!(
        r#"<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Schema debug locked</title>
  <style>
    body {{
      margin: 0;
      min-height: 100vh;
      display: grid;
      place-items: center;
      background: #111827;
      color: #f9fafb;
      font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      padding: 24px;
    }}
    article {{
      width: min(720px, 100%);
      background: rgba(15, 23, 42, 0.88);
      border: 1px solid rgba(148, 163, 184, 0.35);
      border-radius: 20px;
      padding: 24px;
      box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
    }}
    h1 {{ margin: 0 0 12px; font-size: 28px; }}
    p {{ margin: 8px 0; color: #cbd5e1; }}
    code {{
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      color: #f8fafc;
      background: rgba(15, 23, 42, 0.8);
      padding: 2px 6px;
      border-radius: 6px;
    }}
  </style>
</head>
<body>
  <article>
    <h1>Schema debug HTML is locked</h1>
    <p>This page only renders when the request includes a matching <code>jwt_secret</code>.</p>
    <p><strong>Access reason:</strong> {access_reason}</p>
    <p><strong>Query param present:</strong> {query_param_present}</p>
    <p><strong>Unlock hint:</strong> {unlock_hint}</p>
  </article>
</body>
</html>"#,
        access_reason = escape_html(access_reason),
        query_param_present = if query_param_present { "true" } else { "false" },
        unlock_hint = escape_html(unlock_hint),
    )
}

fn render_request_section(request: Option<&Value>) -> String {
    let Some(request) = request else {
        return r#"<article class="panel"><h2>Request</h2><p class="muted">No request diagnostics returned.</p></article>"#.to_string();
    };

    let rows = [
        (
            "Configured header name",
            string_field(request, "configured_header_name")
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Configured header value",
            string_field(request, "configured_header_value")
                .unwrap_or("(empty)")
                .to_string(),
        ),
        (
            "Resolved tenant",
            string_field(request, "resolved_tenant")
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Resolution status",
            string_field(request, "resolution_status")
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        (
            "Resolution error",
            string_field(request, "resolution_error")
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Fallback headers",
            object_field(request, "fallback_header_values")
                .map(compact_json_value)
                .unwrap_or_else(|| "(none)".to_string()),
        ),
    ];

    format!(
        r#"<article class="panel">
  <h2>Request</h2>
  {rows}
  <div class="stack" style="margin-top: 12px;">
    {full_json}
  </div>
</article>"#,
        rows = render_kv_list(&rows),
        full_json = render_json_details("Full request JSON", Some(request), false),
    )
}

fn render_search_path_section(current_probe: Option<&Value>) -> String {
    let Some(search_path) = current_probe.and_then(|value| object_field(value, "search_path"))
    else {
        return String::new();
    };

    let rows = [
        (
            "Configured SQL fragment",
            string_field(search_path, "configured_sql_fragment")
                .unwrap_or("(default)")
                .to_string(),
        ),
        (
            "Configured schemas",
            array_strings(search_path.get("configured_schemas")).join(", "),
        ),
        (
            "Runtime current schema",
            string_field(search_path, "runtime_current_schema")
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        (
            "Runtime setting search_path",
            string_field(search_path, "runtime_setting_search_path")
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        (
            "Runtime effective schemas",
            array_strings(search_path.get("runtime_effective_schemas")).join(", "),
        ),
    ];

    format!(
        r#"<section class="panel" style="margin-top: 16px;">
  <h2>Search path</h2>
  {rows}
</section>"#,
        rows = render_kv_list(&rows)
    )
}

fn render_runtime_features_article(runtime: Option<&Value>) -> String {
    let Some(runtime) = runtime else {
        return r#"<article class="panel"><h2>Runtime features</h2><p class="muted">No runtime diagnostics returned.</p></article>"#.to_string();
    };

    let features = object_field(runtime, "enabled_features");
    let known_tenant_count = runtime
        .get("known_tenants")
        .and_then(Value::as_array)
        .map(|entries| entries.len().to_string())
        .unwrap_or_else(|| "0".to_string());

    let rows = [
        (
            "Default tenant",
            string_field(runtime, "default_tenant")
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        ("Known tenants", known_tenant_count),
        (
            "Mapping mode",
            string_field(runtime, "mapping_application_mode")
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        (
            "Feature keys",
            features
                .and_then(Value::as_object)
                .map(|entries| entries.len().to_string())
                .unwrap_or_else(|| "0".to_string()),
        ),
    ];

    format!(
        r#"<article class="panel">
  <h2>Runtime features</h2>
  {rows}
  <div class="stack" style="margin-top: 12px;">
    {features_json}
    {alias_json}
  </div>
</article>"#,
        rows = render_kv_list(&rows),
        features_json = render_json_details("Enabled feature matrix", features, false),
        alias_json = render_json_details(
            "Table alias config",
            object_field(runtime, "table_alias_config"),
            false
        ),
    )
}

fn render_table_resolution_section(current_probe: Option<&Value>) -> String {
    let Some(probe) = current_probe else {
        return String::new();
    };
    let rows = probe
        .get("table_resolution")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let summary = probe.get("table_resolution_summary");

    let table_rows = if rows.is_empty() {
        "<tr><td colspan=\"6\" class=\"muted\">No table resolution rows returned.</td></tr>"
            .to_string()
    } else {
        rows.iter()
            .map(render_table_resolution_row)
            .collect::<Vec<_>>()
            .join("")
    };

    format!(
        r#"<section class="panel" style="margin-top: 16px;">
  <h2>Table resolution</h2>
  <p class="muted">Runtime lookup vs configured mapped tables, including secondary singular/plural variants.</p>
  <div class="stack" style="margin-top: 12px;">
    <details open>
      <summary>Summary</summary>
      {summary}
    </details>
    <div style="overflow-x: auto;">
      <table>
        <thead>
          <tr>
            <th>Logical</th>
            <th>Runtime lookup</th>
            <th>Mapped target</th>
            <th>Runtime ready</th>
            <th>Mapped ready</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>{table_rows}</tbody>
      </table>
    </div>
  </div>
</section>"#,
        summary = render_json_block(summary),
        table_rows = table_rows,
    )
}

fn render_table_resolution_row(row: &Value) -> String {
    let runtime_status = object_field(row, "runtime_status");
    let mapped_status = object_field(row, "mapped_status");
    let runtime_ready = runtime_status
        .and_then(|value| bool_field(value, "found_in_effective_search_path_any"))
        .or_else(|| {
            runtime_status.and_then(|value| bool_field(value, "found_in_effective_search_path"))
        });
    let mapped_ready = mapped_status
        .and_then(|value| bool_field(value, "found_in_effective_search_path_any"))
        .or_else(|| {
            mapped_status.and_then(|value| bool_field(value, "found_in_effective_search_path"))
        });
    let overall_status = string_field(row, "overall_status").unwrap_or("unknown");

    format!(
        r#"<tr>
  <td><code>{logical}</code></td>
  <td>
    <div><code>{runtime_lookup}</code></div>
    <div class="muted mono">secondary: {runtime_secondary}</div>
  </td>
  <td>
    <div><code>{mapped_lookup}</code></div>
    <div class="muted mono">secondary: {mapped_secondary}</div>
  </td>
  <td>{runtime_ready}</td>
  <td>{mapped_ready}</td>
  <td>
    <div>{status_badge}</div>
    <div class="muted" style="margin-top: 6px;">{note}</div>
  </td>
</tr>"#,
        logical = escape_html(string_field(row, "logical_table").unwrap_or("-")),
        runtime_lookup = escape_html(string_field(row, "runtime_lookup_table").unwrap_or("-")),
        runtime_secondary =
            escape_html(string_field(row, "runtime_secondary_lookup_table").unwrap_or("(none)")),
        mapped_lookup = escape_html(string_field(row, "configured_mapped_table").unwrap_or("-")),
        mapped_secondary =
            escape_html(string_field(row, "configured_secondary_mapped_table").unwrap_or("(none)")),
        runtime_ready = escape_html(bool_option_label(runtime_ready)),
        mapped_ready = escape_html(bool_option_label(mapped_ready)),
        status_badge = render_badge("status", overall_status),
        note = escape_html(string_field(row, "note").unwrap_or("")),
    )
}

fn render_origin_debug_article(value: Option<&Value>) -> String {
    let Some(origin_debug) = value else {
        return r#"<article class="panel"><h2>Origin rejection debug</h2><p class="muted">No origin trust diagnostics returned.</p></article>"#.to_string();
    };

    let current_request = object_field(origin_debug, "currentRequest");
    let runtime = object_field(origin_debug, "runtime");
    let rows = [
        (
            "Allowed",
            current_request
                .and_then(|value| bool_field(value, "allowed"))
                .map(bool_label)
                .unwrap_or("unknown")
                .to_string(),
        ),
        (
            "Origin present",
            current_request
                .and_then(|value| bool_field(value, "originPresent"))
                .map(bool_label)
                .unwrap_or("unknown")
                .to_string(),
        ),
        (
            "Expected base origin",
            current_request
                .and_then(|value| string_field(value, "expectedBaseOrigin"))
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Rejected attempts",
            origin_debug
                .get("rejectedAttemptCount")
                .map(display_value)
                .unwrap_or_else(|| "0".to_string()),
        ),
        (
            "Trusted origins",
            runtime
                .and_then(|value| value.get("trustedOrigins"))
                .and_then(Value::as_array)
                .map(|entries| entries.len().to_string())
                .unwrap_or_else(|| "0".to_string()),
        ),
    ];

    format!(
        r#"<article class="panel">
  <h2>Origin rejection debug</h2>
  {rows}
  <div class="stack" style="margin-top: 12px;">
    {current_request}
    {runtime}
    {full_json}
  </div>
</article>"#,
        rows = render_kv_list(&rows),
        current_request =
            render_json_details("Current request diagnostics", current_request, false),
        runtime = render_json_details("Origin trust runtime", runtime, false),
        full_json = render_json_details("Full origin debug JSON", Some(origin_debug), false),
    )
}

fn render_privileged_debug_article(value: Option<&Value>) -> String {
    let Some(privileged) = value else {
        return r#"<article class="panel"><h2>Privileged debug</h2><p class="muted">No privileged debug payload returned.</p></article>"#.to_string();
    };

    let details = object_field(privileged, "details");
    let config_file = details.and_then(|value| object_field(value, "config_file"));
    let process = details.and_then(|value| object_field(value, "process"));
    let environment = details.and_then(|value| object_field(value, "environment"));
    let oauth_runtime = details.and_then(|value| object_field(value, "oauth_runtime"));
    let tenant_runtime_details =
        details.and_then(|value| object_field(value, "tenant_runtime_details"));

    let rows = [
        (
            "Enabled",
            bool_field(privileged, "enabled")
                .map(bool_label)
                .unwrap_or("unknown")
                .to_string(),
        ),
        (
            "Query param present",
            bool_field(privileged, "query_param_present")
                .map(bool_label)
                .unwrap_or("unknown")
                .to_string(),
        ),
        (
            "Access reason",
            string_field(privileged, "access_reason")
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        (
            "Config path",
            config_file
                .and_then(|value| string_field(value, "path"))
                .unwrap_or("(none)")
                .to_string(),
        ),
        (
            "Config read status",
            config_file
                .and_then(|value| string_field(value, "read_status"))
                .unwrap_or("(unknown)")
                .to_string(),
        ),
        (
            "Process executable",
            process
                .and_then(|value| string_field(value, "executable"))
                .unwrap_or("(none)")
                .to_string(),
        ),
    ];

    format!(
        r#"<article class="panel">
  <h2>Privileged debug</h2>
  {rows}
  <div class="stack" style="margin-top: 12px;">
    {authentication}
    {config_file}
    {environment}
    {oauth_runtime}
    {tenant_runtime_details}
    {process}
    {full_json}
  </div>
</article>"#,
        rows = render_kv_list(&rows),
        authentication = render_json_details(
            "Authentication diagnostics",
            details.and_then(|value| object_field(value, "authentication")),
            false,
        ),
        config_file = render_json_details("Config file snapshot", config_file, false),
        environment = render_json_details("Environment snapshot", environment, false),
        oauth_runtime = render_json_details("OAuth runtime", oauth_runtime, false),
        tenant_runtime_details =
            render_json_details("Tenant runtime details", tenant_runtime_details, false),
        process = render_json_details("Process diagnostics", process, false),
        full_json = render_json_details("Full privileged debug JSON", Some(privileged), false),
    )
}

fn render_tenant_configs_section(value: Option<&Value>) -> String {
    let rows = value.and_then(Value::as_array).cloned().unwrap_or_default();
    if rows.is_empty() {
        return String::new();
    }

    let cards = rows
        .iter()
        .map(|tenant| {
            let database_source = tenant.get("database_source");
            let search_path = tenant.get("configured_search_path");
            let mapping = tenant.get("configured_mapping");
            let entry_count = mapping
                .and_then(|value| value.get("entry_count"))
                .map(display_value)
                .unwrap_or_else(|| "0".to_string());

            let rows = [
                (
                    "Tenant",
                    string_field(tenant, "tenant_id")
                        .unwrap_or("(unknown)")
                        .to_string(),
                ),
                (
                    "Default",
                    bool_field(tenant, "is_default")
                        .map(|value| if value { "true" } else { "false" })
                        .unwrap_or("unknown")
                        .to_string(),
                ),
                (
                    "Database source kind",
                    database_source
                        .and_then(|value| string_field(value, "kind"))
                        .unwrap_or("(none)")
                        .to_string(),
                ),
                (
                    "Resolved URL",
                    database_source
                        .and_then(|value| string_field(value, "resolved_url_redacted"))
                        .unwrap_or("(none)")
                        .to_string(),
                ),
                (
                    "Configured auth schema",
                    string_field(tenant, "configured_auth_schema")
                        .unwrap_or("(none)")
                        .to_string(),
                ),
                (
                    "Configured search path",
                    search_path
                        .and_then(|value| string_field(value, "sql_fragment"))
                        .unwrap_or("(default)")
                        .to_string(),
                ),
                ("Mapping entries", entry_count),
            ];

            format!(
                r#"<article class="panel">
  <h3>{title}</h3>
  <div class="stack" style="margin-top: 12px;">
    {rows}
    <details>
      <summary>Full tenant config</summary>
      {json}
    </details>
  </div>
</article>"#,
                title = escape_html(string_field(tenant, "tenant_id").unwrap_or("tenant")),
                rows = render_kv_list(&rows),
                json = render_json_block(Some(tenant)),
            )
        })
        .collect::<Vec<_>>()
        .join("");

    format!(
        r#"<section style="margin-top: 16px;">
  <div class="grid">
    {cards}
  </div>
</section>"#,
        cards = cards,
    )
}

fn render_raw_payload_section(raw_payload: &str) -> String {
    let metadata = format!(
        "{} · {} lines",
        format_bytes(raw_payload.len()),
        raw_payload.lines().count()
    );

    format!(
        r#"<section class="panel" style="margin-top: 16px;">
  <h2>Raw JSON</h2>
  <p class="section-note">The full payload is still here, but collapsed by default so it does not overwhelm the page.</p>
  <details>
    <summary><span>Full payload JSON</span><span class="summary-meta">{metadata}</span></summary>
    <pre>{raw_payload}</pre>
  </details>
</section>"#,
        metadata = escape_html(metadata.as_str()),
        raw_payload = escape_html(raw_payload),
    )
}

fn render_badge(label: &str, value: &str) -> String {
    format!(
        r#"<span class="badge {tone}"><span>{label}</span><span>{value}</span></span>"#,
        tone = status_tone(value),
        label = escape_html(label),
        value = escape_html(value),
    )
}

fn render_kv_list(rows: &[(&str, String)]) -> String {
    let items = rows
        .iter()
        .map(|(label, value)| {
            format!(
                "<dt>{}</dt><dd>{}</dd>",
                escape_html(label),
                escape_html(value),
            )
        })
        .collect::<Vec<_>>()
        .join("");

    format!(r#"<dl class="kv">{items}</dl>"#)
}

fn render_json_details(title: &str, value: Option<&Value>, open: bool) -> String {
    let Some(value) = value else {
        return format!(
            r#"<details><summary><span>{title}</span><span class="summary-meta">empty</span></summary><p class="section-note">No data returned for this section.</p></details>"#,
            title = escape_html(title),
        );
    };

    let serialized = serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string());
    let summary_meta = format!(
        "{} · {} lines",
        format_bytes(serialized.len()),
        serialized.lines().count()
    );
    let open_attr = if open { " open" } else { "" };

    format!(
        r#"<details{open_attr}>
  <summary><span>{title}</span><span class="summary-meta">{summary_meta}</span></summary>
  <pre>{serialized}</pre>
</details>"#,
        open_attr = open_attr,
        title = escape_html(title),
        summary_meta = escape_html(summary_meta.as_str()),
        serialized = escape_html(&serialized),
    )
}

fn render_json_block(value: Option<&Value>) -> String {
    let serialized = value
        .map(|entry| serde_json::to_string_pretty(entry).unwrap_or_else(|_| entry.to_string()))
        .unwrap_or_else(|| "null".to_string());
    format!("<pre>{}</pre>", escape_html(&serialized))
}

fn status_tone(value: &str) -> &'static str {
    let normalized = value.to_ascii_lowercase();
    if normalized.contains("ready")
        || normalized == "ok"
        || normalized == "enabled"
        || normalized == "true"
    {
        "ok"
    } else if normalized.contains("missing")
        || normalized.contains("error")
        || normalized.contains("locked")
        || normalized.contains("mismatch")
        || normalized.contains("forbidden")
        || normalized == "false"
    {
        "bad"
    } else if normalized.contains("outside")
        || normalized.contains("secondary")
        || normalized.contains("single-tenant")
    {
        "warn"
    } else {
        ""
    }
}

fn object_field<'a>(value: &'a Value, key: &str) -> Option<&'a Value> {
    value.as_object().and_then(|object| object.get(key))
}

fn string_field<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
    object_field(value, key).and_then(Value::as_str)
}

fn bool_field(value: &Value, key: &str) -> Option<bool> {
    object_field(value, key).and_then(Value::as_bool)
}

fn array_strings(value: Option<&Value>) -> Vec<String> {
    value
        .and_then(Value::as_array)
        .map(|items| {
            items
                .iter()
                .filter_map(|item| item.as_str().map(str::to_string))
                .collect()
        })
        .unwrap_or_default()
}

fn bool_option_label(value: Option<bool>) -> &'static str {
    match value {
        Some(true) => "true",
        Some(false) => "false",
        None => "n/a",
    }
}

fn bool_label(value: bool) -> &'static str {
    if value { "true" } else { "false" }
}

fn compact_json_value(value: &Value) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| value.to_string())
}

fn display_value(value: &Value) -> String {
    match value {
        Value::Null => "null".to_string(),
        Value::Bool(value) => value.to_string(),
        Value::Number(value) => value.to_string(),
        Value::String(value) => value.clone(),
        _ => serde_json::to_string(value).unwrap_or_else(|_| value.to_string()),
    }
}

fn format_bytes(bytes: usize) -> String {
    const KIB: f64 = 1024.0;
    const MIB: f64 = KIB * 1024.0;

    if bytes >= MIB as usize {
        format!("{:.1} MiB", bytes as f64 / MIB)
    } else if bytes >= KIB as usize {
        format!("{:.1} KiB", bytes as f64 / KIB)
    } else {
        format!("{bytes} B")
    }
}

fn escape_html(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn render_schema_debug_html_escapes_html_content() {
        let payload = json!({
            "service": "athena-auth<script>",
            "debug_endpoint": "/debug/schema",
            "status": "ok",
            "generated_at": "2026-06-15T00:00:00Z",
            "version": "1.0.0",
            "request": {
                "resolution_status": "ok",
                "resolved_tenant": "<default>"
            },
            "runtime": {
                "multitenancy_enabled": false,
                "tenant_header_name": "x-athena-tenant"
            },
            "current_tenant_probe": {
                "status": "probe_error",
                "table_resolution": []
            },
            "privileged_debug": {
                "enabled": true,
                "access_reason": "jwt_secret_query_matches_env"
            }
        });

        let html = render_schema_debug_html(&payload);
        assert!(html.contains("athena-auth&lt;script&gt;"));
        assert!(html.contains("&lt;default&gt;"));
        assert!(!html.contains("athena-auth<script>"));
    }
}
