"use client";

import { useState, useEffect, useCallback, useRef } from "react";
import { useBrand } from "@/context/brand-context";
import { ShieldCheck, Loader2, AlertCircle, ChevronRight, CheckCircle2, XCircle, AlertTriangle } from "lucide-react";

/** Core Web Vitals official thresholds (good/needs-improvement/poor).
 *  Kept as a const so the row badges + pass/fail computation stay
 *  honest against Google's published numbers instead of drifting
 *  with arbitrary per-card boundaries. */
const CWV_THRESHOLDS = {
  lcpMs:  { good: 2500, poor: 4000 },   // Largest Contentful Paint
  inpMs:  { good: 200,  poor: 500  },   // Interaction to Next Paint (replaces FID in CrUX)
  clsVal: { good: 0.1,  poor: 0.25 },   // Cumulative Layout Shift (lower is better)
};

/** Classify a metric value against its CWV threshold band. Returns one
 *  of "good" | "warn" | "poor" — the row renderer maps these to the
 *  standard green / amber / red palette. Centralised so LCP/INP/CLS
 *  all follow the same "below good → green, above poor → red"
 *  semantics without each cell reinventing the comparison. */
function cwvStatus(metric: keyof typeof CWV_THRESHOLDS, value: number): "good" | "warn" | "poor" {
  const t = CWV_THRESHOLDS[metric];
  if (value <= t.good) return "good";
  if (value <= t.poor) return "warn";
  return "poor";
}

/** Colour band for an overall Lighthouse score (0-100). Matches
 *  Google's own gauge: 90+ green, 50-89 amber, <50 red. */
function scoreColor(score: number): string {
  if (score >= 90) return "text-green-600";
  if (score >= 50) return "text-amber-600";
  return "text-red-600";
}

/** Total number of pages the runner will attempt — mirrors MAX_PAGES in
 *  performance-audit-runner.ts. Only used to render "Auditing page N
 *  of M" while the runner reports fewer completions than it eventually
 *  will. Kept in sync manually; a mismatch is cosmetic (progress bar
 *  shows wrong denominator) not functional. Tightened 5 → 3 → 2 as
 *  the per-PageSpeed-call timeout ratcheted up to 45s — heavy sites
 *  (healthcare with lots of images + third-party scripts) need the
 *  bigger timeout to land real results, and 2 pages × sequential
 *  mobile+desktop fits the 55s wall budget reliably. */
const EXPECTED_PAGE_COUNT = 2;

/** How long the client will wait on "running" state before showing the
 *  long-running notice. The runner self-caps at 55s, so 90s gives
 *  generous headroom for DB writes, network latency, and the response
 *  round-trip before we nudge the user that something might be wrong. */
const RUNNING_NOTICE_MS = 90_000;

/** Polling cadence while an audit is running. 3s is fast enough that
 *  the "Auditing page N of M" message feels live, and cheap enough
 *  that a failed poll doesn't pile up — GET is read-only and each
 *  response is small. */
const POLL_INTERVAL_MS = 3_000;

export default function SitePerformanceAuditPage() {
  const { brand } = useBrand();
  const [auditStatus, setAuditStatus] = useState<"none" | "running" | "completed" | "failed">("none");
  const [auditError, setAuditError] = useState<string | null>(null);
  const [runningAudit, setRunningAudit] = useState(false);
  // Progress surfaced by GET /api/performance-audit when the latest
  // audit is in the "running" state. `completedPages` is how many
  // URLs have at least one PerformancePageScore row written so far.
  const [progress, setProgress] = useState<{ completedPages: number } | null>(null);
  const [showLongRunningNotice, setShowLongRunningNotice] = useState(false);
  const [scores, setScores] = useState<Array<{
    pageUrl: string; device: string; performance: number; accessibility: number;
    bestPractices: number; seo: number; lcpMs: number; inpMs: number; fcpMs: number;
    clsVal: number; ttfbMs: number; status: string;
  }>>([]);
  const [auditDate, setAuditDate] = useState<string | null>(null);
  // Partial-audit note. Set when the runner finished with fewer than
  // the expected page count (wall budget hit, or some pages returned
  // zero scores). Rendered above the score cards as a subtle yellow
  // banner so users know the numbers are real but not exhaustive. A
  // full successful re-run clears it (setPartialNote(null) in the
  // completed branch with a full page count).
  const [partialNote, setPartialNote] = useState<string | null>(null);
  // URL of the currently-expanded table row, or null when every row is
  // collapsed. Accordion semantics: clicking a row closes any other
  // open row and opens this one; clicking an already-open row closes
  // it. Matches the Content Intel recommendations pattern so users get
  // consistent expand behaviour across the app.
  const [expandedUrl, setExpandedUrl] = useState<string | null>(null);
  // Tab mode. "lab" shows the PageSpeed / Lighthouse scores (legacy
  // view); "rum" shows real-user Core Web Vitals captured by t.js.
  // Lab is the default to preserve the existing landing experience;
  // /site-performance-audit?tab=rum or the tab switcher flips to
  // the RUM panel which hits /api/site-tag/analytics/rum.
  const [mode, setMode] = useState<"lab" | "rum">(() => {
    if (typeof window === "undefined") return "lab";
    return new URLSearchParams(window.location.search).get("tab") === "rum" ? "rum" : "lab";
  });
  // Track when we started the current run so the long-running notice
  // kicks in on wall-clock elapsed, not on poll count. Ref so it
  // doesn't trigger re-renders on every update.
  const runStartRef = useRef<number | null>(null);
  // Competitor CWV comparison state. Only loaded on user click so a
  // brand without competitors or a user who never opens the panel
  // doesn't trigger the 45s PageSpeed call cascade on every page
  // visit. Null = never requested, empty arrays = "no competitors
  // tracked" (surfaced as inline message), populated = render table.
  interface CompetitorRow {
    label: string; url: string; scored: boolean;
    performance: number; accessibility: number; bestPractices: number; seo: number;
    lcpMs: number; inpMs: number; clsVal: number; fcpMs: number; ttfbMs: number;
  }
  const [compareLoading, setCompareLoading] = useState(false);
  const [compareData, setCompareData] = useState<{
    brand: CompetitorRow | null;
    competitors: CompetitorRow[];
    message: string;
    partial: boolean;
  } | null>(null);
  const [compareError, setCompareError] = useState<string | null>(null);
  // Historical audit trend — chronological list of the brand's
  // completed audits with per-run average scores. Fetched lazily
  // alongside the main audit load so the sparkline can render as
  // soon as the completed view paints. Empty array = first ever
  // audit (no trend yet), null = still loading or not yet fetched.
  interface HistoryPoint {
    auditId: string;
    completedAt: string;
    pagesScored: number;
    mobilePerf: number;
    desktopPerf: number;
    mobileLcpMs: number;
    mobileClsMs: number;
  }
  const [history, setHistory] = useState<HistoryPoint[] | null>(null);

  const loadAudit = useCallback(async (): Promise<"none" | "running" | "completed" | "failed"> => {
    try {
      const res = await fetch(`/api/performance-audit?brandId=${brand.id}`, { cache: "no-store" });
      if (!res.ok) { setAuditStatus("none"); return "none"; }
      const data = await res.json();
      if (!data) { setAuditStatus("none"); return "none"; }

      if (data.status === "running") {
        setAuditStatus("running");
        setRunningAudit(true);
        setProgress(data.progress ? { completedPages: data.progress.completedPages ?? 0 } : { completedPages: 0 });
        return "running";
      }
      if (data.status === "failed") {
        setAuditStatus("failed");
        setAuditError(data.errorMessage || "Audit failed");
        setProgress(null);
        setPartialNote(null);
        return "failed";
      }
      if (data.status === "completed" && data.pages?.length > 0) {
        setAuditStatus("completed");
        setScores(data.pages);
        setProgress(null);
        if (data.completedAt) {
          setAuditDate(new Date(data.completedAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }));
        }
        // Partial-audit detection. The runner stores status="completed"
        // for both full and partial runs (DB-level), with the errorMessage
        // field distinguishing them — full runs have errorMessage=null,
        // partials carry the "Scored N of M …" copy. Surface that copy
        // to the user above the score cards so "why are there only 2
        // pages here?" is a one-look answer rather than a support
        // ticket. The scores that DID come back are still real, so
        // we treat this as success (render the cards), just with a
        // caveat banner.
        setPartialNote(data.errorMessage ?? null);
        return "completed";
      }
      setAuditStatus("none");
      return "none";
    } catch {
      setAuditStatus("none");
      return "none";
    }
  }, [brand.id]);

  // Initial fetch on mount / brand change.
  useEffect(() => { loadAudit(); }, [loadAudit]);

  // Fetch audit history for the sparkline chart. Kicks off on every
  // brand change + re-fires after a successful run so new audits
  // land on the chart without requiring a manual refresh. Failure
  // is silent — a missing sparkline is a visual absence, not a
  // blocking error.
  const loadHistory = useCallback(async () => {
    try {
      const res = await fetch(`/api/performance-audit/history?brandId=${brand.id}`, { cache: "no-store" });
      if (!res.ok) { setHistory([]); return; }
      const data = await res.json() as { points?: HistoryPoint[] };
      setHistory(data.points ?? []);
    } catch {
      setHistory([]);
    }
  }, [brand.id]);
  useEffect(() => { loadHistory(); }, [loadHistory]);
  // Refresh history after the main audit flips to "completed" so the
  // brand-new run appears on the sparkline without a page reload.
  useEffect(() => {
    if (auditStatus === "completed") loadHistory();
  }, [auditStatus, loadHistory]);

  // Poll while running. Runs the moment we transition to "running"
  // (either because the user just clicked Run, or because the initial
  // fetch found an in-flight audit). Stops on non-running status.
  useEffect(() => {
    if (auditStatus !== "running") {
      setShowLongRunningNotice(false);
      return;
    }
    if (!runStartRef.current) runStartRef.current = Date.now();

    const interval = setInterval(async () => {
      const next = await loadAudit();
      if (next !== "running") {
        // loadAudit already updated state; clean up the start ref so
        // a subsequent run starts fresh.
        runStartRef.current = null;
        setRunningAudit(false);
      }
      // Nudge the user if we're still running past the notice window.
      const started = runStartRef.current;
      if (started && Date.now() - started > RUNNING_NOTICE_MS) {
        setShowLongRunningNotice(true);
      }
    }, POLL_INTERVAL_MS);

    return () => clearInterval(interval);
  }, [auditStatus, loadAudit]);

  async function triggerAudit() {
    setRunningAudit(true);
    setAuditStatus("running");
    setAuditError(null);
    setProgress({ completedPages: 0 });
    setShowLongRunningNotice(false);
    runStartRef.current = Date.now();
    try {
      // Fire the run but don't block the UI on the full response —
      // the runner writes progress rows incrementally and the poll
      // loop (above) will surface completion the moment it lands in
      // the DB. We still await the POST so a synchronous failure
      // (401, 403, 500) surfaces immediately rather than after a
      // 55s "running" spin.
      const res = await fetch("/api/performance-audit/run", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ brandId: brand.id }),
      });
      const data = await res.json();
      // Runner returns one of: "completed" (all pages scored),
      // "partial" (some pages scored, note retained via errorMessage),
      // "failed" (no pages scored), "already_running" (concurrency
      // guard — another tab / user kicked off the audit already).
      //
      // Everything except "failed" flows through loadAudit() which
      // reads the authoritative DB state and flips client status
      // accordingly. "partial" + "completed" both render the score
      // cards; loadAudit populates the partialNote banner when
      // appropriate.
      if (data.status === "failed") {
        setAuditStatus("failed");
        setAuditError(data.message || "Audit failed");
        setRunningAudit(false);
        runStartRef.current = null;
        return;
      }
      await loadAudit();
    } catch {
      setAuditStatus("failed");
      setAuditError("Network error");
      setRunningAudit(false);
      runStartRef.current = null;
    }
  }

  /** Kick off a competitor CWV comparison. Hits
   *  /api/performance-audit/compare-competitors which runs PageSpeed
   *  against the brand's homepage + up to 3 active competitor
   *  homepages (mobile strategy only). ~20s typical, 45s hard ceiling. */
  async function triggerCompare() {
    setCompareLoading(true);
    setCompareError(null);
    try {
      const res = await fetch("/api/performance-audit/compare-competitors", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ brandId: brand.id }),
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({})) as { error?: string };
        setCompareError(body.error || `Comparison failed (HTTP ${res.status}).`);
        setCompareData(null);
      } else {
        const data = await res.json() as {
          brand: CompetitorRow | null;
          competitors: CompetitorRow[];
          message: string;
          partial: boolean;
        };
        setCompareData(data);
      }
    } catch (err) {
      console.warn("[cwi:compare] fetch failed", err);
      setCompareError("Network error reaching the comparison service.");
    } finally {
      setCompareLoading(false);
    }
  }

  // Clear comparison data when the active brand changes — the
  // competitor set is brand-scoped, so showing Brand A's comparison
  // under Brand B's header would be flat-out wrong.
  useEffect(() => {
    setCompareData(null);
    setCompareError(null);
  }, [brand.id]);

  const progressLabel = (() => {
    if (!runningAudit) return null;
    const done = progress?.completedPages ?? 0;
    if (done === 0) return "Preparing audit…";
    return `Auditing page ${Math.min(done + 1, EXPECTED_PAGE_COUNT)} of ${EXPECTED_PAGE_COUNT}…`;
  })();

  // Empty / running / failed state
  if (auditStatus !== "completed") {
    return (
      <div className="space-y-6 max-w-5xl">
        <div>
          <h1 className="text-2xl font-bold text-slate-900">Core Web Intelligence</h1>
          <p className="text-slate-500 text-sm mt-1">
            Performance and Core Web Vitals for <span className="font-medium text-slate-700">{brand.domain || brand.name}</span>
          </p>
        </div>
        <div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
          {auditStatus === "running" ? (
            <Loader2 className="w-12 h-12 text-blue-400 mx-auto mb-3 animate-spin" />
          ) : auditStatus === "failed" ? (
            <AlertCircle className="w-12 h-12 text-red-300 mx-auto mb-3" />
          ) : (
            <ShieldCheck className="w-12 h-12 text-slate-200 mx-auto mb-3" />
          )}
          <h2 className="text-lg font-bold text-slate-900 mb-2">
            {runningAudit ? "Audit Running…" : auditStatus === "failed" ? "Audit Failed" : "No Audit Yet"}
          </h2>
          <p className="text-sm text-slate-500 max-w-md mx-auto mb-4">
            {runningAudit
              ? progressLabel ?? "Testing your site's performance — this may take 30-60 seconds."
              : auditStatus === "failed"
                ? "The last audit encountered an error. You can try again."
                : "Run a performance audit to measure Core Web Vitals, speed scores, and accessibility."}
          </p>
          {/* Progress bar — only while running, only once we have a
              non-zero completed count so the bar doesn't flash empty. */}
          {runningAudit && progress && (
            <div className="max-w-sm mx-auto mb-4">
              <div className="h-1.5 bg-slate-100 rounded-full overflow-hidden">
                <div
                  className="h-full bg-blue-500 transition-all duration-500"
                  style={{ width: `${Math.min(100, Math.round(((progress.completedPages) / EXPECTED_PAGE_COUNT) * 100))}%` }}
                />
              </div>
              <p className="text-[11px] text-slate-400 mt-1.5 tabular-nums">
                {progress.completedPages} of {EXPECTED_PAGE_COUNT} pages scored
              </p>
            </div>
          )}
          {/* Long-running notice — explicit reassurance that we
              haven't frozen. Surfaced after 90s of "running" state. */}
          {showLongRunningNotice && runningAudit && (
            <p className="text-xs text-amber-600 max-w-md mx-auto mb-4">
              Audit is taking longer than expected — results will appear automatically when ready. It&apos;s safe to navigate away and return later.
            </p>
          )}
          {auditError && <p className="text-sm text-red-600 mb-4">{auditError}</p>}
          <button
            onClick={triggerAudit}
            disabled={runningAudit}
            className="px-6 py-3 bg-brand-600 text-white text-sm font-semibold rounded-xl hover:bg-brand-700 disabled:opacity-50 transition-colors"
          >
            {runningAudit ? "Running…" : auditStatus === "failed" ? "Retry Audit" : "Run Performance Audit"}
          </button>
        </div>
      </div>
    );
  }

  // Completed — show real scores
  // Group per-page scores so the table can render one row per URL
  // with mobile + desktop side by side. A single PerformanceAudit
  // produces up to N pages × 2 devices rows; grouping keys on
  // pageUrl and drops anything that didn't score on a given device
  // (runner's Promise.allSettled per URL can emit mobile-only or
  // desktop-only rows if one strategy times out).
  type PageGroup = {
    url: string;
    mobile: typeof scores[number] | undefined;
    desktop: typeof scores[number] | undefined;
  };
  const byUrl = new Map<string, PageGroup>();
  for (const s of scores) {
    const existing = byUrl.get(s.pageUrl) ?? { url: s.pageUrl, mobile: undefined, desktop: undefined };
    if (s.device === "mobile") existing.mobile = s;
    else if (s.device === "desktop") existing.desktop = s;
    byUrl.set(s.pageUrl, existing);
  }
  const pageRows = Array.from(byUrl.values());

  // Site-wide averages — computed separately per device so the Mobile
  // and Desktop summary cards don't mix. Uses mobile+desktop present
  // rows; rows missing a device just drop out of that device's avg.
  function avgPerf(rows: typeof scores): number {
    if (rows.length === 0) return 0;
    return Math.round(rows.reduce((a, r) => a + r.performance, 0) / rows.length);
  }
  function avgLcp(rows: typeof scores): number {
    if (rows.length === 0) return 0;
    return Math.round(rows.reduce((a, r) => a + r.lcpMs, 0) / rows.length);
  }
  function avgCls(rows: typeof scores): number {
    if (rows.length === 0) return 0;
    return rows.reduce((a, r) => a + r.clsVal, 0) / rows.length;
  }
  const mobileRows = scores.filter(s => s.device === "mobile");
  const desktopRows = scores.filter(s => s.device === "desktop");
  const mobileAvg = {
    performance: avgPerf(mobileRows),
    lcpMs: avgLcp(mobileRows),
    clsVal: avgCls(mobileRows),
  };
  const desktopAvg = {
    performance: avgPerf(desktopRows),
    lcpMs: avgLcp(desktopRows),
    clsVal: avgCls(desktopRows),
  };

  return (
    <div className="space-y-6 max-w-5xl">
      <div className="flex items-start justify-between">
        <div>
          <h1 className="text-2xl font-bold text-slate-900">Core Web Intelligence</h1>
          <p className="text-slate-500 text-sm mt-1">
            Performance report for <span className="font-medium text-slate-700">{brand.domain}</span>
            {auditDate && <span className="text-slate-400"> — {auditDate}</span>}
          </p>
        </div>
        <button
          onClick={triggerAudit}
          disabled={runningAudit || mode === "rum"}
          className="px-4 py-2 bg-brand-600 text-white text-sm font-semibold rounded-lg hover:bg-brand-700 disabled:opacity-50 transition-colors"
        >
          {runningAudit ? "Running…" : "Re-run Audit"}
        </button>
      </div>

      {/* Tab switcher — Lab Scores (legacy PageSpeed view) vs. Real
          User Data (live CWV captured by t.js). Default lands on lab
          so the existing landing experience is preserved; ?tab=rum
          deep-links to the new panel. */}
      <div className="inline-flex bg-slate-100 rounded-lg p-0.5">
        <button
          onClick={() => setMode("lab")}
          className={`px-3 py-1.5 text-xs font-semibold rounded-md transition-colors ${
            mode === "lab" ? "bg-white text-slate-900 shadow-sm" : "text-slate-500 hover:text-slate-700"
          }`}
        >Lab Scores</button>
        <button
          onClick={() => setMode("rum")}
          className={`px-3 py-1.5 text-xs font-semibold rounded-md transition-colors ${
            mode === "rum" ? "bg-white text-slate-900 shadow-sm" : "text-slate-500 hover:text-slate-700"
          }`}
        >Real User Data</button>
      </div>

      {mode === "rum" && <RumPanel brandId={brand.id} />}

      {mode === "lab" && (<>
      {/* Partial-audit banner. Rendered above the score cards when the
          runner completed with fewer than all expected pages — usually
          because the wall budget was reached or some pages timed out
          against PageSpeed. The scores below are still real; this
          just warns the user the sample was smaller than intended so
          they don't misread "only 2 cards" as a UI bug. */}
      {partialNote && (
        <div className="flex items-start gap-2.5 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3">
          <AlertCircle className="w-4 h-4 text-amber-600 mt-0.5 shrink-0" />
          <p className="text-sm text-amber-900 leading-relaxed">{partialNote}</p>
        </div>
      )}

      {/* Site-wide average cards. Computed from every scored page
          (not the first row), so a site with mixed-quality pages
          surfaces the REAL aggregate instead of whichever URL
          happened to be first in the result set. */}
      <div className="grid grid-cols-2 gap-4">
        {[{ label: "Mobile Avg", avg: mobileAvg, count: mobileRows.length },
          { label: "Desktop Avg", avg: desktopAvg, count: desktopRows.length }].map(({ label, avg, count }) => (
          <div key={label} className="bg-white rounded-xl border border-slate-200 p-5">
            <div className="flex items-center justify-between mb-3">
              <p className="text-xs font-bold uppercase tracking-widest text-slate-400">{label}</p>
              <p className="text-[10px] text-slate-400 tabular-nums">across {count} page{count === 1 ? "" : "s"}</p>
            </div>
            {count > 0 ? (
              <div className="space-y-3">
                <div className="flex items-baseline gap-2">
                  <span className={`text-4xl font-black ${scoreColor(avg.performance)}`}>
                    {avg.performance}
                  </span>
                  <span className="text-sm text-slate-400">/100 Performance</span>
                </div>
                <div className="grid grid-cols-2 gap-2 text-xs">
                  <div><span className="text-slate-400">Avg LCP:</span> <span className="font-semibold">{(avg.lcpMs / 1000).toFixed(1)}s</span></div>
                  <div><span className="text-slate-400">Avg CLS:</span> <span className="font-semibold">{avg.clsVal.toFixed(3)}</span></div>
                </div>
              </div>
            ) : (
              <p className="text-sm text-slate-400">No {label.toLowerCase().replace(" avg", "")} data</p>
            )}
          </div>
        ))}
      </div>

      {/* Per-page results table. One row per audited URL with both
          devices' perf scores + the three Core Web Vitals (mobile is
          shown in the row; desktop numbers appear in the expanded
          detail panel). Click a row to expand full metrics (CWV
          pass/fail badges, FCP, TTFB, accessibility / best-practices
          / SEO sub-scores for both devices). Replaces the previous
          "first mobile row + first desktop row" layout that ignored
          everything beyond the first page in the result set. */}
      <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
        <div className="px-5 py-3 border-b border-slate-100 flex items-center justify-between">
          <h2 className="text-sm font-bold text-slate-800">Per-page Results</h2>
          <p className="text-[11px] text-slate-400 tabular-nums">{pageRows.length} page{pageRows.length === 1 ? "" : "s"} scored</p>
        </div>
        <div className="overflow-x-auto">
          <table className="w-full">
            <thead className="bg-slate-50/60">
              <tr className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
                <th className="px-2 py-2.5 text-left w-6"></th>
                <th className="px-3 py-2.5 text-left">URL</th>
                <th className="px-3 py-2.5 text-center">Mobile</th>
                <th className="px-3 py-2.5 text-center">Desktop</th>
                <th className="px-3 py-2.5 text-right">LCP</th>
                <th className="px-3 py-2.5 text-right">INP</th>
                <th className="px-3 py-2.5 text-right">CLS</th>
                <th className="px-3 py-2.5 text-center">CWV</th>
              </tr>
            </thead>
            <tbody className="divide-y divide-slate-100">
              {pageRows.map((row) => {
                const m = row.mobile;
                const d = row.desktop;
                const path = row.url.replace(/^https?:\/\/[^/]+/, "") || "/";
                const isOpen = expandedUrl === row.url;
                // CWV pass/fail uses mobile data (Google's primary
                // ranking signal). Falls back to desktop when mobile
                // didn't score — honest rather than pretending the
                // page has no CWV data at all.
                const cwvSource = m ?? d;
                const lcpStatus = cwvSource ? cwvStatus("lcpMs", cwvSource.lcpMs) : null;
                const inpStatus = cwvSource ? cwvStatus("inpMs", cwvSource.inpMs) : null;
                const clsStatus = cwvSource ? cwvStatus("clsVal", cwvSource.clsVal) : null;
                const allCwvGood = lcpStatus === "good" && inpStatus === "good" && clsStatus === "good";
                const anyCwvPoor = lcpStatus === "poor" || inpStatus === "poor" || clsStatus === "poor";

                return (
                  <tr
                    key={row.url}
                    className={`hover:bg-slate-50/60 cursor-pointer ${isOpen ? "bg-slate-50/60" : ""}`}
                    onClick={() => setExpandedUrl((cur) => (cur === row.url ? null : row.url))}
                  >
                    <td className="px-2 py-2.5 text-center">
                      <ChevronRight className={`w-3.5 h-3.5 text-slate-400 transition-transform ${isOpen ? "rotate-90 text-slate-700" : ""}`} />
                    </td>
                    <td className="px-3 py-2.5 text-xs font-mono text-slate-700 max-w-[280px] truncate" title={row.url}>
                      {path}
                    </td>
                    <td className="px-3 py-2.5 text-center">
                      {m ? <span className={`text-sm font-bold ${scoreColor(m.performance)}`}>{m.performance}</span> : <span className="text-xs text-slate-300">—</span>}
                    </td>
                    <td className="px-3 py-2.5 text-center">
                      {d ? <span className={`text-sm font-bold ${scoreColor(d.performance)}`}>{d.performance}</span> : <span className="text-xs text-slate-300">—</span>}
                    </td>
                    <td className="px-3 py-2.5 text-right text-xs tabular-nums">
                      {cwvSource ? <MetricCell value={`${(cwvSource.lcpMs / 1000).toFixed(1)}s`} status={lcpStatus} /> : "—"}
                    </td>
                    <td className="px-3 py-2.5 text-right text-xs tabular-nums">
                      {cwvSource ? <MetricCell value={`${cwvSource.inpMs}ms`} status={inpStatus} /> : "—"}
                    </td>
                    <td className="px-3 py-2.5 text-right text-xs tabular-nums">
                      {cwvSource ? <MetricCell value={cwvSource.clsVal.toFixed(3)} status={clsStatus} /> : "—"}
                    </td>
                    <td className="px-3 py-2.5 text-center">
                      {allCwvGood ? (
                        <span className="inline-flex items-center gap-1 text-[10px] font-bold text-green-700 bg-green-50 border border-green-200 rounded-full px-2 py-0.5">
                          <CheckCircle2 className="w-2.5 h-2.5" /> Pass
                        </span>
                      ) : anyCwvPoor ? (
                        <span className="inline-flex items-center gap-1 text-[10px] font-bold text-red-700 bg-red-50 border border-red-200 rounded-full px-2 py-0.5">
                          <XCircle className="w-2.5 h-2.5" /> Fail
                        </span>
                      ) : (
                        <span className="inline-flex items-center gap-1 text-[10px] font-bold text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5">
                          <AlertTriangle className="w-2.5 h-2.5" /> Needs work
                        </span>
                      )}
                    </td>
                  </tr>
                );
              })}
              {/* Per-row expansion panel. Rendered as a separate <tr>
                  spanning every column so the grid alignment of the
                  summary row stays intact regardless of expand state.
                  Shows mobile + desktop side by side with the full
                  metric set (adds FCP, TTFB, accessibility, best
                  practices, SEO that don't fit in the summary row). */}
              {expandedUrl && (() => {
                const row = pageRows.find(p => p.url === expandedUrl);
                if (!row) return null;
                return (
                  <tr className="bg-slate-50/40">
                    <td colSpan={8} className="px-4 py-4 border-t border-slate-100">
                      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                        <PageDetailCard label="Mobile" score={row.mobile} />
                        <PageDetailCard label="Desktop" score={row.desktop} />
                      </div>
                    </td>
                  </tr>
                );
              })()}
            </tbody>
          </table>
        </div>
      </div>

      {/* Performance trend chart. Rendered only when we have 2+
          historical data points — a single-point "trend" is just a
          dot, which users interpret as a broken chart rather than
          "first run, come back later". The numeric snapshot (latest
          + delta) still shows below 2 points so first-timers see
          SOMETHING, just not the sparkline. */}
      {history && history.length > 0 && (
        <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
          <div className="px-5 py-3 border-b border-slate-100 flex items-center justify-between flex-wrap gap-3">
            <div>
              <h2 className="text-sm font-bold text-slate-800">Performance over time</h2>
              <p className="text-[11px] text-slate-400 mt-0.5">
                Mobile Performance score across your last {history.length} audit{history.length === 1 ? "" : "s"}.
              </p>
            </div>
            {history.length >= 2 && (() => {
              // Simple delta between first + last points so users see
              // the top-line direction without reading the chart.
              const first = history[0].mobilePerf;
              const last = history[history.length - 1].mobilePerf;
              const delta = last - first;
              return (
                <div className="text-right">
                  <p className={`text-lg font-bold tabular-nums ${delta > 0 ? "text-green-600" : delta < 0 ? "text-red-600" : "text-slate-600"}`}>
                    {delta > 0 ? "+" : ""}{delta}
                  </p>
                  <p className="text-[10px] text-slate-400">since first audit</p>
                </div>
              );
            })()}
          </div>
          <div className="px-5 py-4">
            <Sparkline points={history.map((p) => p.mobilePerf)} />
            {/* Compact per-run readout below the sparkline — lets
                users click-to-investigate individual points without
                needing a full interactive chart library. Shows up to
                5 most-recent points; earlier ones live in the
                chart only. */}
            <div className="flex items-center gap-3 flex-wrap mt-3 pt-3 border-t border-slate-100">
              {history.slice(-5).map((p, i) => (
                <div key={p.auditId} className="flex items-center gap-1.5">
                  <span className="text-[10px] text-slate-400 tabular-nums">
                    {new Date(p.completedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
                  </span>
                  <span className={`text-xs font-semibold tabular-nums ${scoreColor(p.mobilePerf)}`}>
                    {p.mobilePerf}
                  </span>
                  {i < Math.min(4, history.slice(-5).length - 1) && <span className="text-slate-200">·</span>}
                </div>
              ))}
            </div>
          </div>
        </div>
      )}

      {/* Competitor CWV comparison section. Hidden until the user
          clicks the button — each click costs 3 PageSpeed calls and
          ~20-45s of wall-clock, so we don't pre-warm. Renders
          whatever the runner returned (scored rows first, any
          failed-to-score rows get a "—" treatment in the cells). */}
      <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
        <div className="px-5 py-3 border-b border-slate-100 flex items-center justify-between flex-wrap gap-3">
          <div>
            <h2 className="text-sm font-bold text-slate-800">Competitor Comparison</h2>
            <p className="text-[11px] text-slate-400 mt-0.5">
              Compare your CWV against active competitors&apos; homepages (mobile).
            </p>
          </div>
          <button
            onClick={triggerCompare}
            disabled={compareLoading}
            className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-semibold bg-slate-800 text-white hover:bg-slate-900 disabled:opacity-50 transition-colors"
          >
            {compareLoading ? (
              <>
                <Loader2 className="w-3.5 h-3.5 animate-spin" />
                Running…
              </>
            ) : (
              <>Compare with Competitors</>
            )}
          </button>
        </div>

        {compareError && (
          <div className="px-5 py-3 bg-red-50 border-b border-red-100 text-xs text-red-700 flex items-start gap-2">
            <AlertCircle className="w-3.5 h-3.5 mt-0.5 shrink-0" />
            <span>{compareError}</span>
          </div>
        )}

        {compareData && compareData.partial && (
          <div className="px-5 py-3 bg-amber-50 border-b border-amber-100 text-xs text-amber-800 flex items-start gap-2">
            <AlertCircle className="w-3.5 h-3.5 mt-0.5 shrink-0" />
            <span>{compareData.message}</span>
          </div>
        )}

        {compareData && compareData.brand === null && compareData.competitors.length === 0 && (
          <div className="px-5 py-8 text-center text-sm text-slate-500">{compareData.message}</div>
        )}

        {compareData && (compareData.brand || compareData.competitors.length > 0) && (
          <div className="overflow-x-auto">
            {(() => {
              // Build the full row list for rendering + per-metric
              // winner detection. "Your Site" first so the side-by-
              // side comparison reads naturally left-to-right.
              const rows = [
                ...(compareData.brand ? [compareData.brand] : []),
                ...compareData.competitors,
              ].filter((r) => r.scored);
              const metrics = [
                { key: "performance",   label: "Performance", unit: "",  lowerBetter: false },
                { key: "lcpMs",         label: "LCP",         unit: "s", lowerBetter: true  },
                { key: "inpMs",         label: "INP",         unit: "ms", lowerBetter: true },
                { key: "clsVal",        label: "CLS",         unit: "",  lowerBetter: true  },
                { key: "accessibility", label: "A11y",        unit: "",  lowerBetter: false },
                { key: "bestPractices", label: "Best Prac.",  unit: "",  lowerBetter: false },
                { key: "seo",           label: "SEO",         unit: "",  lowerBetter: false },
              ] as const;

              // For each metric, find the winning row(s) so the
              // cell can render bold + green. Ties win for both
              // rows. Rows that didn't score are excluded from
              // winner detection.
              function winnerSetFor(metric: typeof metrics[number]) {
                if (rows.length === 0) return new Set<string>();
                const values = rows.map((r) => (r as unknown as Record<string, number>)[metric.key]);
                const best = metric.lowerBetter ? Math.min(...values) : Math.max(...values);
                return new Set(rows.filter((r) => (r as unknown as Record<string, number>)[metric.key] === best).map((r) => r.url));
              }
              const winners = new Map(metrics.map((m) => [m.key, winnerSetFor(m)]));

              function formatValue(row: CompetitorRow, metric: typeof metrics[number]): string {
                if (!row.scored) return "—";
                const v = (row as unknown as Record<string, number>)[metric.key];
                // Column units differ by metric. Only the keys in
                // `metrics` reach this helper; FCP / TTFB aren't
                // columns in the comparison table so they're not
                // formatted here.
                if (metric.key === "lcpMs") return `${(v / 1000).toFixed(1)}s`;
                if (metric.key === "inpMs") return `${v}ms`;
                if (metric.key === "clsVal") return v.toFixed(3);
                return String(v);
              }

              return (
                <table className="w-full">
                  <thead className="bg-slate-50/60">
                    <tr className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
                      <th className="px-3 py-2.5 text-left">Site</th>
                      {metrics.map((m) => (
                        <th key={m.key} className="px-3 py-2.5 text-right">{m.label}</th>
                      ))}
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-slate-100">
                    {[
                      ...(compareData.brand ? [compareData.brand] : []),
                      ...compareData.competitors,
                    ].map((row) => (
                      <tr key={row.url} className={row.label === "Your Site" ? "bg-brand-50/30" : ""}>
                        <td className="px-3 py-2.5 text-xs">
                          <div className="font-semibold text-slate-800 truncate max-w-[200px]" title={row.url}>
                            {row.label}
                          </div>
                          <div className="text-[10px] font-mono text-slate-400 truncate max-w-[200px]" title={row.url}>
                            {row.url.replace(/^https?:\/\//, "")}
                          </div>
                        </td>
                        {metrics.map((m) => {
                          const isWinner = row.scored && winners.get(m.key)?.has(row.url);
                          return (
                            <td
                              key={m.key}
                              className={`px-3 py-2.5 text-right text-xs tabular-nums ${
                                !row.scored ? "text-slate-300" :
                                isWinner ? "font-bold text-green-600" : "text-slate-700"
                              }`}
                            >
                              {formatValue(row, m)}
                            </td>
                          );
                        })}
                      </tr>
                    ))}
                  </tbody>
                </table>
              );
            })()}
          </div>
        )}
      </div>
      </>)}
    </div>
  );
}

// ─── Shared sub-components ─────────────────────────────────────────────────

/** Single metric cell in the per-page row. Colors the value against
 *  CWV good/warn/poor thresholds so a scan of the LCP / INP / CLS
 *  columns reads the same way Google's own PageSpeed report reads. */
function MetricCell({ value, status }: { value: string; status: "good" | "warn" | "poor" | null }) {
  const color = status === "good" ? "text-green-600"
    : status === "warn" ? "text-amber-600"
    : status === "poor" ? "text-red-600"
    : "text-slate-400";
  return <span className={`font-semibold ${color}`}>{value}</span>;
}

/** Full per-device detail card rendered inside the expanded row. Shows
 *  every metric PerformancePageScore stores for a single device — the
 *  row-summary table only has room for the CWV triple, so this is the
 *  place FCP / TTFB / accessibility / best-practices / SEO surface. */
function PageDetailCard({ label, score }: {
  label: "Mobile" | "Desktop";
  score: {
    performance: number; accessibility: number; bestPractices: number; seo: number;
    lcpMs: number; inpMs: number; clsVal: number; fcpMs: number; ttfbMs: number;
  } | undefined;
}) {
  return (
    <div className="bg-white rounded-lg border border-slate-200 p-4">
      <div className="flex items-center justify-between mb-3">
        <p className="text-[10px] font-bold uppercase tracking-widest text-slate-400">{label}</p>
        {score && (
          <span className={`text-2xl font-black ${scoreColor(score.performance)}`}>{score.performance}</span>
        )}
      </div>
      {!score ? (
        <p className="text-xs text-slate-400">No {label.toLowerCase()} data — PageSpeed either timed out or the strategy was skipped.</p>
      ) : (
        <div className="space-y-2">
          <div className="grid grid-cols-3 gap-2 text-xs">
            <DetailMetric label="LCP"  value={`${(score.lcpMs / 1000).toFixed(1)}s`} status={cwvStatus("lcpMs",  score.lcpMs)}  />
            <DetailMetric label="INP"  value={`${score.inpMs}ms`}                    status={cwvStatus("inpMs",  score.inpMs)}  />
            <DetailMetric label="CLS"  value={score.clsVal.toFixed(3)}               status={cwvStatus("clsVal", score.clsVal)} />
            <DetailMetric label="FCP"  value={`${(score.fcpMs / 1000).toFixed(1)}s`} />
            <DetailMetric label="TTFB" value={`${score.ttfbMs}ms`} />
          </div>
          <div className="grid grid-cols-3 gap-2 text-xs pt-2 border-t border-slate-100">
            <DetailMetric label="A11y" value={String(score.accessibility)} />
            <DetailMetric label="BP"   value={String(score.bestPractices)} />
            <DetailMetric label="SEO"  value={String(score.seo)} />
          </div>
        </div>
      )}
    </div>
  );
}

function DetailMetric({ label, value, status }: {
  label: string; value: string; status?: "good" | "warn" | "poor";
}) {
  const color = status === "good" ? "text-green-600"
    : status === "warn" ? "text-amber-600"
    : status === "poor" ? "text-red-600"
    : "text-slate-700";
  return (
    <div>
      <p className="text-[9px] font-bold uppercase tracking-widest text-slate-400">{label}</p>
      <p className={`font-semibold tabular-nums ${color}`}>{value}</p>
    </div>
  );
}

/** Inline SVG sparkline. Chosen over recharts / a chart library for
 *  three reasons:
 *    1. Zero runtime deps — the audit page already pulls enough.
 *    2. Single-metric "trend" doesn't need axes / tooltips / legends;
 *       a polyline over scaled points communicates direction fine.
 *    3. Responsive via viewBox — renders crisp at any CSS size.
 *
 *  Values normalise against the 0-100 performance-score scale
 *  explicitly (clamped) so a run with an outlier score doesn't
 *  visually compress all the other points into a flat line at the
 *  bottom. If the min/max of the data were used instead, a single
 *  regression from 95→85 would look like a catastrophic drop; fixed
 *  0-100 axis keeps the reading stable. */
function Sparkline({ points }: { points: number[] }) {
  const WIDTH = 600;
  const HEIGHT = 80;
  const PAD = 4;
  if (points.length === 0) return null;
  if (points.length === 1) {
    return (
      <p className="text-xs text-slate-400 italic">
        Run another audit to see a trend line — a single data point
        isn&apos;t enough to plot direction.
      </p>
    );
  }
  const xStep = (WIDTH - PAD * 2) / (points.length - 1);
  const coords = points.map((v, i) => {
    const clamped = Math.max(0, Math.min(100, v));
    const x = PAD + i * xStep;
    // Invert Y because SVG coordinates go top-down; score 100 → top.
    const y = HEIGHT - PAD - (clamped / 100) * (HEIGHT - PAD * 2);
    return { x, y, v: clamped };
  });
  const pathD = coords.map((c, i) => `${i === 0 ? "M" : "L"}${c.x.toFixed(1)},${c.y.toFixed(1)}`).join(" ");
  // Area-fill under the line for visual weight. Closed back to the
  // baseline at left/right edges so the gradient has a closed shape.
  const areaD = `${pathD} L${coords[coords.length - 1].x.toFixed(1)},${HEIGHT - PAD} L${coords[0].x.toFixed(1)},${HEIGHT - PAD} Z`;
  const last = coords[coords.length - 1];
  // Tint based on the latest score so the sparkline's dominant colour
  // tells the user whether the current run is good/mediocre/poor.
  const stroke = last.v >= 90 ? "rgb(22 163 74)" : last.v >= 50 ? "rgb(217 119 6)" : "rgb(220 38 38)";
  const fill = last.v >= 90 ? "rgb(22 163 74 / 0.12)" : last.v >= 50 ? "rgb(217 119 6 / 0.12)" : "rgb(220 38 38 / 0.12)";

  return (
    <svg
      viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
      className="w-full h-20"
      preserveAspectRatio="none"
      role="img"
      aria-label={`Performance score trend. Latest: ${last.v}.`}
    >
      {/* Guide lines at 50 + 90 so the reader can eyeball the
          thresholds without a full axis render. */}
      {[50, 90].map((g) => {
        const y = HEIGHT - PAD - (g / 100) * (HEIGHT - PAD * 2);
        return (
          <line key={g} x1={PAD} y1={y} x2={WIDTH - PAD} y2={y} stroke="rgb(226 232 240)" strokeDasharray="2 3" strokeWidth="1" />
        );
      })}
      <path d={areaD} fill={fill} />
      <path d={pathD} fill="none" stroke={stroke} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" />
      {/* Endpoint dot — makes the "current score" visually obvious
          even on a 2-point line. */}
      <circle cx={last.x} cy={last.y} r="3" fill={stroke} />
    </svg>
  );
}

// ─── Real User Data (RUM) panel ─────────────────────────────────────────────
//
// Pulls median-aggregated RealUserMetric samples from
// /api/site-tag/analytics/rum and renders them alongside a
// device / connection breakdown. Sample count is shown prominently
// so a brand with no t.js installed understands why numbers are
// blank rather than assuming the page is broken.

interface RumPayload {
  samples: number;
  windowDays: number;
  cwv: { lcp: number | null; fcp: number | null; cls: number | null; inp: number | null; ttfb: number | null };
  byConnection: Array<{ connection: string; samples: number; lcp: number | null; inp: number | null; cls: number | null }>;
  byDevice:     Array<{ device: string;     samples: number; lcp: number | null; inp: number | null; cls: number | null }>;
  topPages: Array<{ url: string; samples: number; lcp: number | null; fcp: number | null; cls: number | null; inp: number | null; ttfb: number | null }>;
}

function RumPanel({ brandId }: { brandId: string }) {
  const [range, setRange] = useState<"7d" | "30d" | "90d">("30d");
  const [data, setData] = useState<RumPayload | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(`/api/site-tag/analytics/rum?brandId=${encodeURIComponent(brandId)}&range=${range}`)
      .then((r) => (r.ok ? r.json() : null))
      .then((d: RumPayload | null) => { if (!cancelled) setData(d); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [brandId, range]);

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between flex-wrap gap-3">
        <div>
          <h2 className="text-sm font-bold text-slate-900">Real User Data</h2>
          <p className="text-xs text-slate-500 mt-0.5">
            Core Web Vitals captured from real visitors via the Site Tag. Median across every sampled page load.
          </p>
        </div>
        <div className="inline-flex bg-slate-100 rounded-lg p-0.5">
          {(["7d", "30d", "90d"] as const).map((r) => (
            <button
              key={r}
              onClick={() => setRange(r)}
              className={`px-2.5 py-1 text-[11px] font-semibold rounded-md transition-colors ${
                range === r ? "bg-white text-slate-900 shadow-sm" : "text-slate-500 hover:text-slate-700"
              }`}
            >{r}</button>
          ))}
        </div>
      </div>

      {loading && !data ? (
        <div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-500">Loading real-user metrics…</div>
      ) : !data || data.samples === 0 ? (
        <div className="rounded-xl border border-dashed border-slate-200 bg-slate-50/60 p-8 text-center">
          <p className="text-sm font-semibold text-slate-700">No real-user samples yet</p>
          <p className="text-xs text-slate-500 mt-1 max-w-md mx-auto">
            The Site Tag emits a <code className="font-mono text-[10px] bg-white px-1 py-0.5 rounded border border-slate-200">web_vitals</code> event
            on every page load with LCP / INP / CLS / TTFB / FCP. Once the tag has been live long enough to accumulate samples, the medians will appear here.
          </p>
        </div>
      ) : (
        <>
          <p className="text-[11px] text-slate-500">
            {data.samples.toLocaleString()} page load{data.samples === 1 ? "" : "s"} in the last {data.windowDays} days
          </p>
          <div className="grid grid-cols-2 md:grid-cols-5 gap-3">
            <RumStat label="LCP"  unit="ms"  value={data.cwv.lcp}  thresholds={[2500, 4000]} />
            <RumStat label="INP"  unit="ms"  value={data.cwv.inp}  thresholds={[200, 500]} />
            <RumStat label="CLS"  unit=""    value={data.cwv.cls}  thresholds={[0.1, 0.25]} digits={3} />
            <RumStat label="FCP"  unit="ms"  value={data.cwv.fcp}  thresholds={[1800, 3000]} />
            <RumStat label="TTFB" unit="ms"  value={data.cwv.ttfb} thresholds={[800, 1800]} />
          </div>

          <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
            <RumBucketTable title="By connection"  rows={data.byConnection.map((r) => ({ label: r.connection, samples: r.samples, lcp: r.lcp, inp: r.inp, cls: r.cls }))} />
            <RumBucketTable title="By device"      rows={data.byDevice    .map((r) => ({ label: r.device,     samples: r.samples, lcp: r.lcp, inp: r.inp, cls: r.cls }))} />
          </div>

          {data.topPages.length > 0 && (
            <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
              <div className="px-5 py-3 border-b border-slate-100">
                <h3 className="text-xs font-bold text-slate-800">Top pages by sample volume</h3>
              </div>
              <div className="overflow-x-auto">
                <table className="w-full text-xs">
                  <thead className="text-[10px] font-semibold uppercase tracking-widest text-slate-400 border-b border-slate-100 bg-slate-50/40">
                    <tr>
                      <th className="text-left px-4 py-2">Page</th>
                      <th className="text-right px-4 py-2">Samples</th>
                      <th className="text-right px-4 py-2">LCP</th>
                      <th className="text-right px-4 py-2">INP</th>
                      <th className="text-right px-4 py-2">CLS</th>
                      <th className="text-right px-4 py-2">FCP</th>
                      <th className="text-right px-4 py-2">TTFB</th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-slate-100">
                    {data.topPages.map((p) => (
                      <tr key={p.url} className="hover:bg-slate-50 transition-colors">
                        <td className="px-4 py-2 font-mono text-[10px] text-brand-600 truncate max-w-[260px]" title={p.url}>{p.url}</td>
                        <td className="px-4 py-2 text-right tabular-nums text-slate-700">{p.samples.toLocaleString()}</td>
                        <td className={`px-4 py-2 text-right tabular-nums ${rumColor(p.lcp, [2500, 4000])}`}>{fmtMs(p.lcp)}</td>
                        <td className={`px-4 py-2 text-right tabular-nums ${rumColor(p.inp, [200, 500])}`}>{fmtMs(p.inp)}</td>
                        <td className={`px-4 py-2 text-right tabular-nums ${rumColor(p.cls, [0.1, 0.25])}`}>{p.cls != null ? p.cls.toFixed(3) : "—"}</td>
                        <td className={`px-4 py-2 text-right tabular-nums ${rumColor(p.fcp, [1800, 3000])}`}>{fmtMs(p.fcp)}</td>
                        <td className={`px-4 py-2 text-right tabular-nums ${rumColor(p.ttfb, [800, 1800])}`}>{fmtMs(p.ttfb)}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </div>
          )}
        </>
      )}
    </div>
  );
}

function rumColor(value: number | null, thresholds: [number, number]): string {
  if (value == null) return "text-slate-300";
  if (value <= thresholds[0]) return "text-green-600 font-semibold";
  if (value <= thresholds[1]) return "text-amber-600 font-semibold";
  return "text-red-600 font-semibold";
}
function fmtMs(v: number | null): string {
  if (v == null) return "—";
  if (v >= 1000) return `${(v / 1000).toFixed(2)}s`;
  return `${Math.round(v)}ms`;
}

function RumStat({
  label, unit, value, thresholds, digits = 0,
}: {
  label: string;
  unit: string;
  value: number | null;
  thresholds: [number, number];
  digits?: number;
}) {
  const color = rumColor(value, thresholds);
  const fmtd = value == null
    ? "—"
    : unit === "ms"
      ? fmtMs(value)
      : value.toFixed(digits);
  return (
    <div className="bg-white rounded-xl border border-slate-200 px-4 py-3">
      <p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">{label}</p>
      <p className={`text-2xl tabular-nums leading-none mt-1 ${color}`}>{fmtd}{unit && value != null && unit !== "ms" ? unit : ""}</p>
    </div>
  );
}

interface RumBucketRow { label: string; samples: number; lcp: number | null; inp: number | null; cls: number | null }

function RumBucketTable({ title, rows }: { title: string; rows: RumBucketRow[] }) {
  if (rows.length === 0) return null;
  return (
    <div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
      <div className="px-5 py-3 border-b border-slate-100">
        <h3 className="text-xs font-bold text-slate-800">{title}</h3>
      </div>
      <table className="w-full text-xs">
        <thead className="text-[10px] font-semibold uppercase tracking-widest text-slate-400 border-b border-slate-100 bg-slate-50/40">
          <tr>
            <th className="text-left px-4 py-2">Bucket</th>
            <th className="text-right px-4 py-2">Samples</th>
            <th className="text-right px-4 py-2">LCP</th>
            <th className="text-right px-4 py-2">INP</th>
            <th className="text-right px-4 py-2">CLS</th>
          </tr>
        </thead>
        <tbody className="divide-y divide-slate-100">
          {rows.map((r) => (
            <tr key={r.label}>
              <td className="px-4 py-2 font-mono text-[11px] text-slate-700">{r.label}</td>
              <td className="px-4 py-2 text-right tabular-nums text-slate-700">{r.samples.toLocaleString()}</td>
              <td className={`px-4 py-2 text-right tabular-nums ${rumColor(r.lcp, [2500, 4000])}`}>{fmtMs(r.lcp)}</td>
              <td className={`px-4 py-2 text-right tabular-nums ${rumColor(r.inp, [200, 500])}`}>{fmtMs(r.inp)}</td>
              <td className={`px-4 py-2 text-right tabular-nums ${rumColor(r.cls, [0.1, 0.25])}`}>{r.cls != null ? r.cls.toFixed(3) : "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
