const { useEffect, useRef, useState } = React;

const SEV_META = {
  high: { label: "High", tone: "high", color: "var(--high)" },
  medium: { label: "Medium", tone: "med", color: "var(--med)" },
  low: { label: "Low", tone: "low", color: "var(--low)" },
};

const TYPE_META = {
  behavior_exec: { label: "Executed behavior", icon: "bolt" },
  validator_behavior: { label: "Validator (executed)", icon: "shieldCheck" },
  state_machine: { label: "State machine", icon: "branch" },
  validator: { label: "Validator", icon: "shieldCheck" },
  dependency: { label: "Dependency", icon: "func" },
  database: { label: "Database", icon: "database" },
  schema: { label: "Schema", icon: "layers" },
  contract: { label: "Contract", icon: "doc" },
  fix_confirmed: { label: "Fix-confirmed", icon: "checkCircle" },
  test_breakage: { label: "Behavior drift", icon: "commit" },
  behavioral_drift: { label: "Behavior drift", icon: "diff" },
  state_transition: { label: "State transition", icon: "branch" },
  test_deletion: { label: "Test churn", icon: "doc" },
  test_gap: { label: "Test gap", icon: "warn" },
  deploy: { label: "Deploy", icon: "bolt" },
  auth: { label: "Auth", icon: "lock" },
  agent_or_workflow: { label: "Agent/workflow", icon: "branch" },
  blast_radius: { label: "Blast radius", icon: "layers" },
  type_escape: { label: "Type escape", icon: "doc" },
  lint_suppression: { label: "Lint suppression", icon: "warn" },
};

const TWEAK_DEFAULTS = {
  density: "comfortable",
  accent: "#2f6df0",
};

const ACCENTS = {
  "#2f6df0": "oklch(0.55 0.185 256)",
  "#5b54e8": "oklch(0.55 0.18 280)",
  "#0d9488": "oklch(0.56 0.10 192)",
  "#7c5cff": "oklch(0.58 0.19 290)",
};

function shortSha(sha) {
  return String(sha || "").slice(0, 7);
}

function dateLabel(value) {
  if (!value) return "";
  try {
    return new Intl.DateTimeFormat(undefined, {
      month: "short",
      day: "numeric",
      hour: "numeric",
      minute: "2-digit",
    }).format(new Date(value));
  } catch {
    return value;
  }
}

function clientLog(type, data = {}) {
  fetch("/api/debug/client", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ type, data }),
  }).catch(() => {});
}

// Analytics. The PostHog loader stub lives in Kaval.html; initPosthog runs once
// /api/config delivers the token (the key stays in .env, never in this file).
// The stub creates capture() only inside init(), so events tracked before the
// config fetch resolves (the first $pageview) buffer here and drain on init;
// with no token configured nothing ever loads and track() stays a no-op.
let posthogStarted = false;
const preInitEvents = [];
function initPosthog(posthogConfig) {
  if (posthogStarted || !posthogConfig?.token || !window.posthog?.init) return;
  posthogStarted = true;
  window.posthog.init(posthogConfig.token, {
    api_host: posthogConfig.host || "https://us.i.posthog.com",
    persistence: "memory", // cookie-less: nothing stored on the visitor's machine
    capture_pageview: false, // SPA — handleRoute captures $pageview itself
    capture_pageleave: true,
    session_recording: { maskAllInputs: true }, // keeps "no credentials stored" honest
  });
  while (preInitEvents.length) {
    const [event, props] = preInitEvents.shift();
    window.posthog.capture(event, props);
  }
}
function track(event, props = {}) {
  try {
    if (!posthogStarted) {
      if (preInitEvents.length < 50) preInitEvents.push([event, props]);
      return;
    }
    window.posthog?.capture?.(event, props);
  } catch {
    // analytics must never break the app
  }
}

function SetupState({ config, error, onRetry }) {
  const supabaseReady = config?.supabase?.configured;
  const resultsReady = config?.results?.configured;
  const azureReady = config?.azureOpenAI?.configured;
  const rows = [
    ["Supabase Auth", supabaseReady, "SUPABASE_URL + SUPABASE_ANON_KEY"],
    ["Supabase Results", resultsReady, "SUPABASE_SERVICE_ROLE_KEY"],
    ["Azure OpenAI", azureReady, "AZURE_OPENAI_* deployment envs"],
  ];

  return (
    <Shell>
      <div style={{ maxWidth: 920, margin: "0 auto", padding: "64px 40px" }} className="rise">
        <Pill tone="high" icon={Icon.warn({ s: 13 })}>Setup required</Pill>
        <h1 style={{ fontSize: 36, lineHeight: 1.12, letterSpacing: "-0.03em", margin: "18px 0 12px", maxWidth: 680 }}>
          Kaval is ready to run only against real repos.
        </h1>
        <p style={{ fontSize: 16, color: "var(--text-2)", lineHeight: 1.55, maxWidth: 700, margin: "0 0 24px" }}>
          There is no local sample-data path. Connect Supabase and Azure, then use a GitHub OAuth session to analyze actual SHAs and store actual results.
        </p>
        {error && (
          <Card style={{ borderColor: "var(--high-border)", background: "var(--high-subtle)", marginBottom: 18 }}>
            <div style={{ fontSize: 13, color: "var(--high-text)", fontWeight: 560 }}>{error}</div>
          </Card>
        )}
        <Card pad={0} style={{ overflow: "hidden", marginBottom: 18 }}>
          {rows.map(([label, ready, hint], index) => (
            <div key={label} style={{ display: "grid", gridTemplateColumns: "190px 110px 1fr", gap: 16, alignItems: "center", padding: "14px 18px", borderTop: index ? "1px solid var(--border)" : "none" }}>
              <div style={{ fontWeight: 560, fontSize: 13.5 }}>{label}</div>
              <Pill tone={ready ? "pass" : "high"} dot>{ready ? "configured" : "missing"}</Pill>
              <code style={{ fontSize: 12, color: "var(--text-2)" }}>{hint}</code>
            </div>
          ))}
        </Card>
        <Button icon={Icon.restart({ s: 15 })} onClick={onRetry}>Recheck config</Button>
      </div>
    </Shell>
  );
}

function Shell({ children, right }) {
  return (
    <div>
      <div style={{ height: 56, borderBottom: "1px solid var(--border)", background: "var(--surface)", position: "sticky", top: 0, zIndex: 20, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 22px" }}>
        {/* Logo always leads home: authed -> repo picker, public -> landing site */}
        <a href="/" style={{ textDecoration: "none" }} aria-label="Kaval home"><Logo /></a>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <Pill tone="accent" icon={Icon.lock({ s: 12 })}>real repos only</Pill>
          {right}
        </div>
      </div>
      {children}
    </div>
  );
}

// The marketing site (landing/) is the front door; the app has no hero of
// its own. Unauthenticated visitors hitting "/" are sent to the landing.
// A function, not a const: /api/config sets window.KAVAL_LANDING_URL after
// this module parses (KAVAL_LANDING_URL env in production).
const LANDING_URL = () => window.KAVAL_LANDING_URL || "http://localhost:4321";

function ExamplesGallery({ examples, onOpenExample }) {
  if (!examples.length) {
    return <Card><EmptyState icon={Icon.search({ s: 22 })} title="No examples yet" body="Run scripts/build-examples.mjs to populate the gallery." /></Card>;
  }
  return (
    <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 14 }}>
      {examples.map((example) => (
        <Card key={example.slug} hover style={{ cursor: "pointer" }} onClick={() => onOpenExample?.(example.slug)}>
          <div style={{ padding: "14px 16px" }}>
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, marginBottom: 6 }}>
              <span style={{ fontWeight: 600, fontSize: 13.5, fontFamily: "var(--mono, ui-monospace, monospace)" }}>{example.repo}</span>
              {example.summary && (
                <Pill tone="accent" dot>{example.summary.hits}/{example.summary.analyzed} flagged</Pill>
              )}
            </div>
            <div style={{ fontSize: 12.5, color: "var(--text-3)", lineHeight: 1.5 }}>{example.blurb}</div>
            {example.summary?.strongHits > 0 && (
              <div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 8 }}>{example.summary.strongHits} with executed evidence · {example.commitsScanned} commits scanned</div>
            )}
          </div>
        </Card>
      ))}
    </div>
  );
}

function ExamplesScreen({ examples, onOpenExample, onTryRepo, busy, error, authed }) {
  const [repoInput, setRepoInput] = useState("");
  return (
    <Workspace title="Example reports" subtitle="Real open-source repos run through the full pipeline — honest misses included. Click any card to read the report.">
      <ExamplesGallery examples={examples} onOpenExample={onOpenExample} />
      <div style={{ marginTop: 26, paddingTop: 20, borderTop: "1px solid var(--border)", maxWidth: 570 }}>
        <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>{authed ? "Or dig a public repo of your own" : "Or dig a public repo of your own — sign in with GitHub to run it"}</div>
        <form onSubmit={(event) => { event.preventDefault(); onTryRepo?.(repoInput); }} style={{ display: "flex", gap: 8 }}>
          <input
            value={repoInput}
            onChange={(event) => setRepoInput(event.target.value)}
            placeholder="github.com/owner/repo"
            spellCheck={false}
            style={{ flex: 1, height: 38, borderRadius: 9, border: "1px solid var(--border)", padding: "0 12px", fontSize: 13.5, fontFamily: "inherit", background: "var(--surface)", color: "var(--text)", outline: "none" }}
          />
          <Button icon={authed ? Icon.bolt({ s: 15 }) : Icon.github({ s: 15 })} disabled={busy || !repoInput.trim()}>{busy ? "Digging…" : authed ? "Run Kaval" : "Sign in & run"}</Button>
        </form>
        {error && <div style={{ color: "var(--high, #c2410c)", fontSize: 12.5, marginTop: 8 }}>{error}</div>}
        <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 8 }}>Typical repo: 1–3 minutes. Large monorepos: up to ~8.</div>
      </div>
    </Workspace>
  );
}

// URL-only demo entry (/try): nothing in the app or the landing links here.
// A hero the size of a business card — paste a repo, connect GitHub, or open
// the examples — so a live demo can end with "go to /try and run it yourself".
function TryScreen({ onTryRepo, onConnect, onExamples, busy, error, authed }) {
  const [repoInput, setRepoInput] = useState("");
  return (
    <div style={{ minHeight: "100vh", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "48px 20px", textAlign: "center" }}>
      <Logo />
      <h1 style={{ fontSize: "clamp(26px, 4.4vw, 40px)", fontWeight: 760, letterSpacing: "-0.022em", lineHeight: 1.12, maxWidth: 640, margin: "26px 0 0" }}>
        Paste a repo. Kaval shows the last bugs it shipped — <span style={{ color: "var(--accent)" }}>and which ones it would have caught.</span>
      </h1>
      <p style={{ fontSize: 14.5, color: "var(--text-3)", lineHeight: 1.6, maxWidth: 520, margin: "14px 0 0" }}>
        Kaval re-reviews each change that later needed a fix — blind, by <strong style={{ color: "var(--text)" }}>executing the code</strong> before
        and after with identical inputs. Hits and misses, in the repo&rsquo;s own numbers.
      </p>
      <form
        onSubmit={(event) => { event.preventDefault(); onTryRepo?.(repoInput); }}
        style={{ display: "flex", gap: 8, width: "100%", maxWidth: 520, marginTop: 28 }}
      >
        <input
          value={repoInput}
          onChange={(event) => setRepoInput(event.target.value)}
          placeholder="github.com/owner/repo — any public TypeScript repo"
          spellCheck={false}
          autoFocus
          style={{ flex: 1, height: 42, borderRadius: 10, border: "1px solid var(--border-2)", padding: "0 14px", fontSize: 14, fontFamily: "inherit", background: "var(--surface)", color: "var(--text)", outline: "none" }}
        />
        <Button icon={authed ? Icon.bolt({ s: 15 }) : Icon.github({ s: 15 })} disabled={busy || !repoInput.trim()}>{busy ? "Digging…" : authed ? "Dig" : "Sign in & dig"}</Button>
      </form>
      {error && <div style={{ color: "var(--high, #c2410c)", fontSize: 12.5, marginTop: 10 }}>{error}</div>}
      <div style={{ display: "flex", gap: 9, marginTop: 18 }}>
        <Button variant="secondary" icon={Icon.github({ s: 15 })} onClick={onConnect}>Connect GitHub</Button>
        <Button variant="secondary" icon={Icon.search({ s: 15 })} onClick={onExamples}>Example reports</Button>
      </div>
      <div style={{ fontSize: 11.5, color: "var(--text-4)", fontFamily: "var(--mono, ui-monospace, monospace)", marginTop: 22 }}>
        Read-only · No credentials stored · Honest misses included · typical repo 1–3 min
      </div>
    </div>
  );
}

// The run screen — a live feed of the backtest as it executes, fed by the
// engine's '@@KAVAL@@' milestones relayed through the poll. Each replayed
// change earns a verdict badge as it finishes: watching Kaval execute real
// code and catch (or clear) each change is a better demo than the spinner it
// replaces, and it makes a multi-minute run feel like progress, not a hang.
function RunningScreen({ runStage, runEvents = [], error, emailEnabled, userEmail, notifyDefault = false, notifyState = "idle", onNotify }) {
  const cloneEv = runEvents.find((event) => event.phase === "clone");
  const screenEv = runEvents.find((event) => event.phase === "screen");
  const incidentsEv = runEvents.find((event) => event.phase === "incidents");
  const controlEv = runEvents.find((event) => event.phase === "control");
  const doneEv = runEvents.find((event) => event.phase === "done");
  const incidents = runEvents.filter((event) => event.phase === "incident");
  const incidentBadge = (event) => {
    if (event.decision === "block") return { tone: "high", label: "would block" };
    if (event.decision === "escalate") return { tone: "med", label: "worth a look" };
    if (event.status === "hit") return { tone: "pass", label: "caught" };
    if (event.status === "out_of_scope" || event.status === "not_regression") return { tone: "neutral", label: "set aside" };
    if (event.status === "miss") return { tone: "neutral", label: "missed" };
    return { tone: "neutral", label: "analyzed" };
  };
  const Milestone = ({ done, children }) => (
    <div style={{ display: "flex", alignItems: "center", gap: 9, fontSize: 13, color: "var(--text-2)" }}>
      <span style={{ flexShrink: 0, display: "inline-flex" }}>{done ? Icon.check({ s: 14 }) : Icon.spinner({ s: 14 })}</span>
      <span>{children}</span>
    </div>
  );
  const caught = incidents.filter((event) => event.status === "hit" || event.decision === "block" || event.decision === "escalate").length;
  return (
    <Workspace title="Kaval is digging" subtitle={emailEnabled && notifyDefault
      ? "Running your real code, before & after each change. This takes a few minutes — you can close this tab and we’ll email you when the report is ready."
      : "Running your real code, before & after each change. This takes a few minutes — your report saves automatically, so you can leave this tab and come back to this page."}>
      <Card>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
          {Icon.spinner({ s: 18 })}
          <div style={{ fontSize: 13, color: "var(--text-3)" }}>{runStage || "Starting…"}</div>
        </div>

        {emailEnabled && !doneEv && (notifyDefault || notifyState === "sent") && (
          <div style={{ marginBottom: 18, display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, color: "var(--pass-text)" }}>
            {Icon.check({ s: 14 })} You can leave this page — we’ll email you{userEmail ? ` at ${userEmail}` : ""} when the report is ready.
          </div>
        )}

        {emailEnabled && !doneEv && !notifyDefault && notifyState !== "sent" && (
          <div style={{ marginBottom: 18 }}>
            <Button size="sm" variant="secondary" disabled={notifyState === "sending"} onClick={onNotify}>
              {notifyState === "sending" ? "Setting up…" : "Email me when it’s ready"}
            </Button>
          </div>
        )}

        <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
          {cloneEv && <Milestone done>Cloned <strong>{cloneEv.repo}</strong></Milestone>}
          {screenEv && <Milestone done>Screened {screenEv.commits} commits — {screenEv.fixLike} look like fixes</Milestone>}
          {incidentsEv && <Milestone done>Found {incidentsEv.total} changes that later needed a fix — replaying each one blind to the future</Milestone>}
        </div>

        {incidents.length > 0 && (
          <div style={{ marginTop: 16, paddingTop: 16, borderTop: "1px solid var(--border)", display: "flex", flexDirection: "column", gap: 9 }}>
            {incidents.map((event, index) => {
              const badge = incidentBadge(event);
              return (
                <div key={index} style={{ display: "flex", alignItems: "center", gap: 10 }}>
                  <Pill tone={badge.tone} dot>{badge.label}</Pill>
                  <div style={{ fontSize: 13, color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}>“{event.subject}”</div>
                </div>
              );
            })}
          </div>
        )}

        {controlEv && !doneEv && <div style={{ marginTop: 14 }}><Milestone>Checking clean commits — a tool that blocks good merges is worse than none</Milestone></div>}
        {doneEv && <div style={{ marginTop: 16, paddingTop: 14, borderTop: "1px solid var(--border)", fontSize: 13.5, fontWeight: 600 }}>Caught {caught} of {incidents.length} — building your report…</div>}
      </Card>
      <div style={{ fontSize: 11.5, color: "var(--text-3)", marginTop: 12, textAlign: "center" }}>
        Your code runs in a throwaway, network-isolated sandbox and is deleted after the run. We keep only the rendered report.
      </div>
      {error && <div style={{ color: "var(--high, #c2410c)", fontSize: 13, marginTop: 12 }}>{error}</div>}
    </Workspace>
  );
}

// /dashboard — the GitHub App home: every installation the signed-in user
// can see (their login + their orgs), its trial/plan state, and recent PR
// checks across them. The post-install redirect lands here with ?installed=1,
// usually signed out — hence the in-screen sign-in wall.
function DashboardScreen({ dashboard, authed, installed, onConnect, onOpenPr, onRetry }) {
  const app = window.KAVAL_GITHUB_APP || {};
  const title = installed ? "Kaval is installed" : "Dashboard";
  const subtitle = installed
    ? "Advisory checks will appear on new pull requests in the repos you picked. Nothing is ever blocked."
    : "Your installations, trial status, and recent PR checks.";

  if (!authed) {
    return (
      <Workspace title={title} subtitle={subtitle}>
        <Card>
          <EmptyState icon={Icon.github({ s: 24 })} title="Sign in with GitHub to see your dashboard"
            body="Installations and PR checks are matched to your GitHub login and orgs." />
          <div style={{ display: "flex", justifyContent: "center", paddingBottom: 34 }}>
            <Button icon={Icon.github({ s: 15 })} onClick={onConnect}>Sign in with GitHub</Button>
          </div>
        </Card>
      </Workspace>
    );
  }
  if (dashboard?.error) {
    return (
      <Workspace title={title} subtitle={subtitle}>
        <Card>
          <EmptyState icon={Icon.warn({ s: 24 })} title="Couldn't load the dashboard" body={dashboard.error} />
          <div style={{ display: "flex", justifyContent: "center", paddingBottom: 34 }}>
            <Button variant="secondary" icon={Icon.restart({ s: 15 })} onClick={onRetry}>Try again</Button>
          </div>
        </Card>
      </Workspace>
    );
  }
  if (!dashboard) {
    return (
      <Workspace title={title} subtitle={subtitle}>
        <Card><EmptyState icon={Icon.spinner({ s: 24 })} title="Loading your dashboard" body="" /></Card>
      </Workspace>
    );
  }

  const installations = dashboard.installations || [];
  const prRuns = dashboard.prRuns || [];
  const DECISION = {
    block: { tone: "high", label: "Would block" },
    escalate: { tone: "med", label: "Worth a look" },
    pass: { tone: "pass", label: "Pass" },
  };
  // Launch is free — every live installation is early access. (Uninstalled
  // ones are 'deleted' and filtered out server-side, so they never reach here.)
  const planPill = () => <Pill tone="pass" dot>Early access</Pill>;

  return (
    <Workspace title={title} subtitle={subtitle}>
      <div style={{ fontSize: 13, fontWeight: 600, margin: "0 0 10px" }}>Installations</div>
      <Card pad={0} style={{ overflow: "hidden" }}>
        {!installations.length && (
          <div>
            <EmptyState icon={Icon.github({ s: 24 })} title="No installations yet"
              body="Install the Kaval GitHub App and advisory checks appear on every new pull request." />
            {app.installUrl && (
              <div style={{ display: "flex", justifyContent: "center", paddingBottom: 34 }}>
                <Button icon={Icon.github({ s: 15 })} onClick={() => { track("dashboard_install_clicked"); window.open(app.installUrl, "_blank", "noreferrer"); }}>Install on GitHub</Button>
              </div>
            )}
          </div>
        )}
        {installations.map((inst, index) => (
          <div key={inst.id} style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 18px", borderTop: index ? "1px solid var(--border)" : "none", flexWrap: "wrap" }}>
            <Avatar initials={String(inst.accountLogin || "?").slice(0, 2).toUpperCase()} />
            <div style={{ flex: 1, minWidth: 180 }}>
              <div style={{ fontSize: 14.5, fontWeight: 560 }}>{inst.accountLogin}</div>
              <div style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 3 }}>
                {inst.accountType || "Account"} · {(inst.repos || []).length} repo{(inst.repos || []).length === 1 ? "" : "s"}
              </div>
            </div>
            {planPill()}
          </div>
        ))}
      </Card>

      <div style={{ fontSize: 13, fontWeight: 600, margin: "26px 0 10px" }}>Recent PR checks</div>
      <Card pad={0} style={{ overflow: "hidden" }}>
        {!prRuns.length && (
          <EmptyState icon={Icon.search({ s: 22 })} title="No PR checks yet"
            body="Open a pull request in an installed repo — the verdict and its report land here." />
        )}
        {prRuns.map((run, index) => {
          const decision = DECISION[run.decision] || DECISION.escalate;
          return (
            <button key={`${run.repo}#${run.prNumber}-${run.createdAt}`} onClick={() => onOpenPr(run)}
              style={{ display: "flex", width: "100%", alignItems: "center", gap: 14, padding: "13px 18px", border: "none", borderTop: index ? "1px solid var(--border)" : "none", background: "var(--surface)", textAlign: "left", cursor: "pointer" }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 14, fontWeight: 560, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
                  {run.repo}#{run.prNumber}{run.prTitle ? ` — ${run.prTitle}` : ""}
                </div>
                <div style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 3 }}>{dateLabel(run.createdAt)}</div>
              </div>
              <Pill tone={decision.tone} dot>{decision.label}</Pill>
            </button>
          );
        })}
      </Card>
    </Workspace>
  );
}

function RepoPicker({ repos, loading, selectedRepo, setSelectedRepo, onRefresh, onInspectFit, onAnalyzeRepo, error }) {
  const [query, setQuery] = useState("");
  const filtered = repos.filter((repo) => repo.fullName.toLowerCase().includes(query.toLowerCase()));

  return (
    <Workspace title="Choose one repository" subtitle="Kaval will screen the last 150 commits and deep-analyze the strongest candidates.">
      <div style={{ display: "flex", gap: 10, marginBottom: 14 }}>
        <div style={{ position: "relative", flex: 1 }}>
          <span style={{ position: "absolute", left: 13, top: "50%", transform: "translateY(-50%)", color: "var(--text-4)" }}>{Icon.search({ s: 16 })}</span>
          <input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Filter repositories..."
            style={{ width: "100%", padding: "11px 14px 11px 38px", fontSize: 14, fontFamily: "var(--font-sans)", border: "1px solid var(--border-2)", borderRadius: 8, background: "var(--surface)", color: "var(--text)", outline: "none" }} />
        </div>
        <Button variant="secondary" icon={loading ? Icon.spinner({ s: 15 }) : Icon.restart({ s: 15 })} onClick={onRefresh} disabled={loading}>Refresh</Button>
      </div>
      {error && <InlineError message={error} />}
      <Card pad={0} style={{ overflow: "hidden" }}>
        {loading && <EmptyState icon={Icon.spinner({ s: 22 })} title="Loading repositories" body="Fetching real GitHub repositories..." />}
        {!loading && filtered.map((repo, index) => (
          <button key={repo.id} onClick={() => setSelectedRepo(repo)}
            style={{ display: "flex", width: "100%", alignItems: "center", gap: 14, padding: "14px 18px", border: "none", borderTop: index ? "1px solid var(--border)" : "none", background: selectedRepo?.id === repo.id ? "var(--accent-subtle)" : "var(--surface)", textAlign: "left" }}>
            <Avatar initials={repo.avatar || repo.fullName.slice(0, 2).toUpperCase()} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14.5, fontWeight: 560, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{repo.fullName}</div>
              <div style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 3 }}>{repo.language} · {repo.defaultBranch} · pushed {dateLabel(repo.updatedAt)}</div>
            </div>
            <Pill tone={repo.private ? "neutral" : "pass"}>{repo.private ? "private" : "public"}</Pill>
          </button>
        ))}
        {!loading && filtered.length === 0 && <EmptyState icon={Icon.github({ s: 24 })} title="No repos found" body="Check the GitHub OAuth scopes or filter text." />}
      </Card>
      <BottomBar left={selectedRepo ? `Selected ${selectedRepo.fullName}` : "Select a repo to continue"}>
        <div style={{ display: "flex", gap: 9 }}>
          <Button disabled={!selectedRepo || loading} icon={Icon.bolt({ s: 16 })} onClick={onAnalyzeRepo}>Run Kaval</Button>
        </div>
      </BottomBar>
    </Workspace>
  );
}

function FitPanel({ fit, loading, error, onContinue, onAnalyzeRepo, onBack }) {
  const score = fit?.score || 0;
  return (
    <Workspace title={fit ? fit.repo.fullName : "Checking repo fit"} subtitle="Fit means available Kaval signal, not a checklist of frameworks the repo must use.">
      <BackButton onClick={onBack}>Back to repositories</BackButton>
      {error && <InlineError message={error} />}
      {loading && <Card><EmptyState icon={Icon.spinner({ s: 24 })} title="Scanning repository" body="Reading the default branch tree through GitHub..." /></Card>}
      {fit && (
        <div style={{ display: "grid", gridTemplateColumns: "260px 1fr", gap: 28, alignItems: "start" }}>
          <Card style={{ textAlign: "center" }}>
            <div style={{ fontSize: 12, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-4)", marginBottom: 16 }}>Repo fit</div>
            <div style={{ fontSize: 44, fontWeight: 600, letterSpacing: "-0.04em" }}>{score}</div>
            <FitBadge fit={fit.fit} />
            <p style={{ fontSize: 12.5, color: "var(--text-2)", lineHeight: 1.55, margin: "16px 0 0", textAlign: "left" }}>
              {fit.stats.files} files scanned. {fit.stats.tsFiles} TypeScript files and {fit.stats.backendFiles} backend-looking files were detected. Missing schema tools only disable those specific lanes.
            </p>
          </Card>
          <div>
            {fit.analysisModes?.length > 0 && (
              <>
                <div style={{ fontSize: 12, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: "var(--text-4)", marginBottom: 10 }}>Kaval signal lanes</div>
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 22 }}>
                  {fit.analysisModes.map((mode) => (
                    <Card key={mode.key}>
                      <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12 }}>
                        <div>
                          <div style={{ fontSize: 13.5, fontWeight: 560 }}>{mode.label}</div>
                          <div style={{ fontSize: 12, color: "var(--text-3)", lineHeight: 1.45, marginTop: 4 }}>{mode.detail}</div>
                        </div>
                        <Pill tone={mode.status === "available" ? "pass" : "med"}>{mode.status}</Pill>
                      </div>
                    </Card>
                  ))}
                </div>
              </>
            )}
            <div style={{ fontSize: 12, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", color: "var(--text-4)", marginBottom: 10 }}>Detected repo signals</div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 18 }}>
              {fit.signals.map((signal) => (
                <Card key={signal.key} style={{ opacity: signal.status === "absent" ? 0.62 : 1 }}>
                  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
                    <div>
                      <div style={{ fontSize: 13.5, fontWeight: 560 }}>{signal.label}</div>
                      <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 3 }}>{signal.detail}</div>
                    </div>
                    <Pill tone={signal.status === "strong" ? "pass" : signal.status === "present" ? "med" : "low"}>{signal.status}</Pill>
                  </div>
                </Card>
              ))}
            </div>
            {fit.treeTruncated && <InlineError message="GitHub returned a truncated tree. Fit is still useful, but some deep files may be missing from the signal." />}
            <div style={{ display: "flex", gap: 9 }}>
              <Button icon={Icon.bolt({ s: 16 })} onClick={onAnalyzeRepo}>Scan 150 commits</Button>
              <Button variant="secondary" iconRight={Icon.arrowRight({ s: 16 })} onClick={onContinue}>Review candidates</Button>
            </div>
          </div>
        </div>
      )}
    </Workspace>
  );
}

function ChangesPanel({ changes, loading, error, selectedChange, setSelectedChange, onAnalyze, onBack }) {
  return (
    <Workspace title="Select a change" subtitle="Commits are ranked from real default-branch history using revert, hotfix, rollback, and risky-surface signals.">
      <BackButton onClick={onBack}>Back to fit</BackButton>
      {error && <InlineError message={error} />}
      <Card pad={0} style={{ overflow: "hidden" }}>
        {loading && <EmptyState icon={Icon.spinner({ s: 22 })} title="Loading commits" body="Reading recent commits from GitHub..." />}
        {!loading && changes.map((change, index) => (
          <button key={change.full} onClick={() => setSelectedChange(change)}
            style={{ display: "flex", width: "100%", alignItems: "center", gap: 14, padding: "14px 18px", border: "none", borderTop: index ? "1px solid var(--border)" : "none", background: selectedChange?.full === change.full ? "var(--accent-subtle)" : "var(--surface)", textAlign: "left" }}>
            <Avatar initials={change.author.slice(0, 2).toUpperCase()} />
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
                <span style={{ fontSize: 14, fontWeight: 560, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{change.msg}</span>
                {change.signals.slice(0, 3).map((signal) => <Pill key={signal} tone={signal.includes("revert") || signal.includes("fix") ? "high" : "med"}>{signal}</Pill>)}
              </div>
              <div style={{ display: "flex", gap: 12, fontSize: 12, color: "var(--text-3)" }}>
                <code className="mono-sha">{shortSha(change.full)}</code>
                <span>{change.author}</span>
                <span>{dateLabel(change.when)}</span>
                {change.base && <span>base <code className="mono-sha">{shortSha(change.base)}</code></span>}
              </div>
            </div>
            <div style={{ width: 120 }}>
              <Bar pct={change.suspicion || 0} color={(change.suspicion || 0) >= 60 ? "var(--high)" : "var(--med)"} h={5} anim={false} />
              <div style={{ textAlign: "right", fontSize: 11.5, marginTop: 4, color: "var(--text-3)" }}>{change.suspicion || 0} signal</div>
            </div>
          </button>
        ))}
        {!loading && changes.length === 0 && <EmptyState icon={Icon.commit({ s: 24 })} title="No commits found" body="Kaval could not read recent commits for this branch." />}
      </Card>
      <BottomBar left={selectedChange ? `Selected ${shortSha(selectedChange.full)} -> compare against ${shortSha(selectedChange.base)}` : "Select a change to analyze"}>
        <Button disabled={!selectedChange?.base} icon={Icon.bolt({ s: 16 })} onClick={onAnalyze}>Run analysis</Button>
      </BottomBar>
    </Workspace>
  );
}

function BacktestScoreboard({ backtest, labels, onLabel }) {
  const summary = backtest?.summary;
  if (!summary || !summary.incidents) return null;
  const incidents = backtest.incidents || [];
  const statusMeta = {
    hit: { tone: "pass", label: "flagged" },
    miss: { tone: "high", label: "missed" },
    unresolvable: { tone: "neutral", label: "unresolvable" },
    not_regression: { tone: "neutral", label: "set aside — additive" },
    out_of_scope: { tone: "neutral", label: "set aside — not behavioral" },
  };

  return (
    <Card style={{ marginBottom: 18, padding: 0, overflow: "hidden", borderColor: "var(--accent-border)" }}>
      <div style={{ padding: "16px 18px 4px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <Pill tone="accent" dot>Backtest</Pill>
          <span style={{ fontSize: 12.5, color: "var(--text-3)" }}>what Kaval would have shown before each revert or hotfix</span>
        </div>
        <h2 style={{ fontSize: 19, fontWeight: 620, letterSpacing: "-0.01em", margin: "12px 0 2px" }}>
          {summary.incidents} incident{summary.incidents === 1 ? "" : "s"} (revert/hotfix/fix-shaped) · culprit flagged on {summary.hits} of {summary.analyzed}
        </h2>
        <p style={{ fontSize: 13, color: "var(--text-2)", margin: "0 0 12px" }}>
          For each incident, Kaval analyzed the change that introduced the problem — as it would have appeared pre-merge — and checked for findings in the files the fix later touched. Misses are listed too.
        </p>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", borderTop: "1px solid var(--border)", borderBottom: incidents.length ? "1px solid var(--border)" : "none" }}>
        {[
          ["Incidents", summary.incidents],
          ["Culprits flagged", summary.hits],
          ["Strong evidence", summary.strongHits],
          ["Missed", summary.misses],
        ].map(([label, value], index) => (
          <div key={label} style={{ padding: "13px 18px", borderLeft: index ? "1px solid var(--border)" : "none" }}>
            <div style={{ fontSize: 24, fontWeight: 650, letterSpacing: "-0.03em", fontVariantNumeric: "tabular-nums" }}>{value ?? 0}</div>
            <div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.05em", marginTop: 2 }}>{label}</div>
          </div>
        ))}
      </div>
      {incidents.map((incident, index) => {
        const meta = statusMeta[incident.status] || statusMeta.unresolvable;
        return (
          <div key={`${incident.fix.sha}_${index}`} style={{ borderTop: index ? "1px solid var(--border)" : "none", padding: "14px 18px" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap" }}>
              <Pill tone={meta.tone} dot>{meta.label}</Pill>
              <Pill tone="neutral">{incident.kind}</Pill>
              <span style={{ fontSize: 13, fontWeight: 560, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 420 }}>{incident.fix.msg}</span>
              <code className="mono-sha">{incident.fix.sha}</code>
              {incident.culprit && (
                <span style={{ fontSize: 12, color: "var(--text-3)" }}>
                  culprit <code className="mono-sha">{incident.culprit.sha}</code>{incident.culprit.subject ? ` — ${incident.culprit.subject}` : ""}
                </span>
              )}
            </div>
            {incident.status !== "hit" && incident.reason && (
              <div style={{ fontSize: 12.5, color: "var(--text-3)", marginTop: 7 }}>{incident.reason}</div>
            )}
            {(incident.matchedFindings || []).length > 0 && (
              <div style={{ display: "flex", flexDirection: "column", gap: 10, marginTop: 12 }}>
                {incident.matchedFindings.map((finding, findingIndex) => (
                  <FindingCard key={finding.id} finding={finding} index={findingIndex + 1} label={labels[finding.id]} onLabel={onLabel} />
                ))}
              </div>
            )}
          </div>
        );
      })}
    </Card>
  );
}

function Report({ result, labels, onLabel, onBack, onDownloadReport }) {
  const findings = result?.findings || [];
  const backtest = result?.backtest || null;
  const backtestFindingIds = new Set(
    (backtest?.incidents || []).flatMap((incident) => (incident.matchedFindings || []).map((finding) => finding.id)),
  );
  const listFindings = findings.filter((finding) => !backtestFindingIds.has(finding.id));
  const history = result?.history || result?.summary?.history || result?.metrics?.history || null;
  const counts = {
    high: findings.filter((finding) => finding.severity === "high").length,
    medium: findings.filter((finding) => finding.severity === "medium").length,
    low: findings.filter((finding) => finding.severity === "low").length,
  };
  const aiStatus = result.ai?.status || "not_configured";
  const aiLabel = {
    complete: "AI ranked",
    error: "AI ranking unavailable",
    not_configured: "Deterministic ranking",
    skipped_empty: "No findings to rank",
  }[aiStatus] || "Deterministic ranking";

  return (
    <Workspace title={history ? "History Scan Results" : "Findings"} subtitle={history ? "Kaval screened recent history, deep-analyzed candidate changes, and stored the strongest evidence." : "These findings came from a real GitHub compare and were stored in Supabase."}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <BackButton onClick={onBack}>Scan another repo</BackButton>
        {result?.run?.id && onDownloadReport && (
          <Button size="sm" variant="secondary" icon={Icon.doc({ s: 14 })} onClick={onDownloadReport}>Download report.md</Button>
        )}
      </div>
      {backtest && <BacktestScoreboard backtest={backtest} labels={labels} onLabel={onLabel} />}
      {history && (
        <Card style={{ marginBottom: 18, padding: 0, overflow: "hidden" }}>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", borderBottom: "1px solid var(--border)" }}>
            {[
              ["Commits screened", history.commitsScreened],
              ["Candidates analyzed", history.candidatesAnalyzed],
              ["Candidates with findings", history.candidatesWithFindings],
              ["Findings surfaced", findings.length],
            ].map(([label, value], index) => (
              <div key={label} style={{ padding: "16px 18px", borderLeft: index ? "1px solid var(--border)" : "none" }}>
                <div style={{ fontSize: 27, fontWeight: 650, letterSpacing: "-0.03em", fontVariantNumeric: "tabular-nums" }}>{value ?? 0}</div>
                <div style={{ fontSize: 11.5, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.05em", marginTop: 3 }}>{label}</div>
              </div>
            ))}
          </div>
          <div style={{ padding: "12px 18px", fontSize: 13, color: "var(--text-2)", lineHeight: 1.5 }}>
            Kaval screened a 150-commit history window, then ran base/head evidence extraction on the strongest candidates. The cards below are ranked across that history.
          </div>
        </Card>
      )}
      <div style={{ display: "flex", gap: 10, marginBottom: 18, flexWrap: "wrap" }}>
        {Object.entries(counts).map(([severity, count]) => (
          <Pill key={severity} tone={SEV_META[severity].tone} dot>{count} {SEV_META[severity].label}</Pill>
        ))}
        <Pill tone={aiStatus === "complete" ? "pass" : "neutral"}>{aiLabel}</Pill>
        {result?.selectedChange && <Pill tone="accent">selected {shortSha(result.selectedChange.full)}</Pill>}
        {history && <Pill tone="neutral">deep-analyzed {history.candidatesAnalyzed} candidates</Pill>}
      </div>
      {result?.metrics?.truncated && (
        <InlineError message={`GitHub truncated this comparison (${result.metrics.truncated.patchesMissing ? `${result.metrics.truncated.patchesMissing} files without patches` : "300-file cap reached"}). Findings cover the visible part of the diff only.`} />
      )}
      {findings.length === 0 && (
        <Card>
          <EmptyState icon={Icon.checkCircle({ s: 24 })} title="No high-signal diffs found" body={history ? `Kaval screened ${history.commitsScreened} commits and deep-analyzed ${history.candidatesAnalyzed} candidates without finding validator, contract, or backend dependency changes in the current signal set.` : "Kaval completed the compare and did not find validator, contract, or dependency changes in the current MVP signal set."} />
        </Card>
      )}
      <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
        {listFindings.map((finding, index) => (
          <FindingCard key={finding.id} finding={finding} index={index + 1} label={labels[finding.id]} onLabel={onLabel} />
        ))}
      </div>
    </Workspace>
  );
}

function FindingCard({ finding, index, label, onLabel }) {
  const [open, setOpen] = useState(index === 1);
  const sev = SEV_META[finding.severity] || SEV_META.low;
  const type = TYPE_META[finding.type] || { label: finding.type, icon: "doc" };
  return (
    <Card pad={0} style={{ overflow: "hidden", borderLeft: `3px solid ${sev.color}` }}>
      <button onClick={() => setOpen(!open)} style={{ width: "100%", textAlign: "left", border: "none", background: "transparent", padding: "18px 20px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 9 }}>
          <span style={{ fontSize: 11.5, fontWeight: 600, color: "var(--text-4)" }}>#{index}</span>
          <Pill tone={sev.tone}>{sev.label}</Pill>
          <span style={{ display: "inline-flex", gap: 5, alignItems: "center", fontSize: 12, color: "var(--text-2)" }}>{Icon[type.icon]({ s: 13 })}{type.label}</span>
          {(finding.executed || finding.boundary?.executed) && <Pill tone="pass" dot>executed</Pill>}
          {finding.backtest && <Pill tone="accent">would have flagged before {finding.backtest.fixSha}</Pill>}
            {finding.change && (
              <Pill tone="neutral">
                <span style={{ display: "inline-flex", alignItems: "center", gap: 5, maxWidth: 360, minWidth: 0 }}>
                  <code className="mono-sha">{shortSha(finding.change.full)}</code>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{finding.change.msg}</span>
                </span>
              </Pill>
            )}
          <div style={{ flex: 1 }} />
          {label && <Pill tone={{ real: "pass", noise: "low", unknown: "med" }[label]} dot>{label}</Pill>}
          {Icon.chevronDown({ s: 16, style: { transform: open ? "rotate(180deg)" : "none" } })}
        </div>
        <h3 style={{ fontSize: 16.5, fontWeight: 600, margin: "0 0 7px" }}>{finding.title}</h3>
        <p style={{ fontSize: 13.5, color: "var(--text-2)", lineHeight: 1.55, margin: 0 }}>{finding.summary}</p>
      </button>
      {open && (
        <div className="fade" style={{ padding: "0 20px 20px", display: "flex", flexDirection: "column", gap: 16 }}>
          <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
            <code style={{ fontSize: 11.5, padding: "2px 8px", borderRadius: 5, background: "var(--surface-3)", border: "1px solid var(--border)" }}>{finding.file}</code>
            {finding.change && <code style={{ fontSize: 11.5, padding: "2px 8px", borderRadius: 5, background: "var(--accent-subtle)", border: "1px solid var(--accent-border)", color: "var(--accent-text)" }}>{shortSha(finding.change.base)} -> {shortSha(finding.change.full)}</code>}
            {(finding.tags || []).map((tag) => <span key={tag} style={{ fontSize: 11.5, color: "var(--text-3)" }}>{tag}</span>)}
          </div>
          {finding.boundary && <BoundaryTable boundary={finding.boundary} />}
          {finding.deps && <DependencyList deps={finding.deps} />}
          <DiffBlock diff={finding.diff || []} />
          <p style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.6, margin: 0 }}>{finding.ai?.aiRationale || finding.reasoning}</p>
          <div style={{ borderTop: "1px solid var(--border)", paddingTop: 14, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
            <span style={{ fontSize: 12.5, color: "var(--text-2)", fontWeight: 540 }}>Validation label</span>
            <div style={{ display: "flex", gap: 6 }}>
              {["real", "noise", "unknown"].map((value) => (
                <Button key={value} size="sm" variant={label === value ? "subtle" : "secondary"} onClick={() => onLabel(finding.id, label === value ? null : value)}>{value}</Button>
              ))}
            </div>
          </div>
        </div>
      )}
    </Card>
  );
}

function DiffBlock({ diff }) {
  if (!diff.length) return null;
  return (
    <div style={{ border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden", background: "var(--surface-2)" }}>
      <div style={{ padding: "7px 12px", borderBottom: "1px solid var(--border)", fontSize: 11.5, color: "var(--text-3)", background: "var(--surface-3)" }}>unified diff evidence</div>
      <div style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, lineHeight: 1.7, overflowX: "auto" }}>
        {diff.map((line, index) => {
          const styles = {
            ctx: { bg: "transparent", fg: "var(--text-2)", sign: " " },
            del: { bg: "var(--high-subtle)", fg: "var(--high-text)", sign: "-" },
            add: { bg: "var(--pass-subtle)", fg: "var(--pass-text)", sign: "+" },
          }[line.t] || {};
          return (
            <div key={index} style={{ display: "flex", background: styles.bg, whiteSpace: "pre" }}>
              <span style={{ width: 44, textAlign: "right", padding: "0 10px 0 4px", color: "var(--text-4)", borderRight: "1px solid var(--border)", flexShrink: 0 }}>{line.n}</span>
              <span style={{ width: 18, textAlign: "center", color: styles.fg, flexShrink: 0 }}>{styles.sign}</span>
              <span style={{ color: line.t === "ctx" ? "var(--text)" : styles.fg, paddingRight: 16 }}>{line.s}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function BoundaryTable({ boundary }) {
  return (
    <div style={{ border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden" }}>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--text-4)", background: "var(--surface-3)" }}>
        <div style={{ padding: "8px 14px" }}>{boundary.field}</div>
        <div style={{ padding: "8px 14px" }}>Base</div>
        <div style={{ padding: "8px 14px" }}>Head</div>
      </div>
      {(boundary.cases || []).map((row, index) => (
        <div key={index} style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", fontFamily: "var(--font-mono)", fontSize: 12.5, borderTop: "1px solid var(--border)", background: row.before !== row.after ? "var(--high-subtle)" : "transparent" }}>
          <div style={{ padding: "9px 14px" }}>{row.input}</div>
          <div style={{ padding: "9px 14px" }}>{row.before}</div>
          <div style={{ padding: "9px 14px" }}>{row.after}</div>
        </div>
      ))}
    </div>
  );
}

function DependencyList({ deps }) {
  const removed = deps.removed || [];
  const kept = deps.kept || [];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
      {removed.map((item) => <code key={item} style={{ color: "var(--high-text)", fontSize: 12 }}>removed: {item}</code>)}
      {kept.map((item) => <code key={item} style={{ color: "var(--text-2)", fontSize: 12 }}>call-set: {item}</code>)}
    </div>
  );
}

// `fill` pins the workspace to the viewport (minus the 56px top bar) so the
// page itself never scrolls — only whatever child opts into overflow: auto.
function Workspace({ title, subtitle, children, fill = false }) {
  return (
    <Shell>
      <div
        className="fade"
        style={{ maxWidth: 980, margin: "0 auto",
          ...(fill
            ? { padding: "30px 40px 28px", boxSizing: "border-box", height: "calc(100vh - 56px)", display: "flex", flexDirection: "column", overflow: "hidden" }
            : { padding: "38px 40px 120px" }) }}>
        <h1 style={{ fontSize: 27, fontWeight: 600, letterSpacing: "-0.02em", margin: "0 0 6px", flexShrink: 0 }}>{title}</h1>
        <p style={{ fontSize: 14.5, color: "var(--text-2)", margin: "0 0 22px", flexShrink: 0 }}>{subtitle}</p>
        {children}
      </div>
    </Shell>
  );
}

function BackButton({ children, onClick }) {
  return (
    <button onClick={onClick} style={{ display: "inline-flex", alignItems: "center", gap: 5, fontSize: 13, color: "var(--text-3)", background: "none", border: "none", padding: 0, marginBottom: 16 }}>
      {Icon.chevron({ s: 14, style: { transform: "rotate(180deg)" } })}{children}
    </button>
  );
}

function BottomBar({ left, children }) {
  return (
    <div style={{ position: "fixed", bottom: 0, left: 0, right: 0, borderTop: "1px solid var(--border)", background: "var(--surface)", padding: "14px 40px", display: "flex", justifyContent: "space-between", alignItems: "center", zIndex: 5 }}>
      <div style={{ fontSize: 13, color: "var(--text-2)" }}>{left}</div>
      {children}
    </div>
  );
}

function InlineError({ message }) {
  return (
    <div style={{ display: "flex", gap: 9, alignItems: "flex-start", padding: "11px 14px", borderRadius: 8, border: "1px solid var(--high-border)", background: "var(--high-subtle)", color: "var(--high-text)", fontSize: 13, lineHeight: 1.45, marginBottom: 14 }}>
      {Icon.warn({ s: 15, style: { flexShrink: 0, marginTop: 1 } })}{message}
    </div>
  );
}

function EmptyState({ icon, title, body }) {
  return (
    <div style={{ padding: 42, textAlign: "center", color: "var(--text-3)" }}>
      <div style={{ display: "flex", justifyContent: "center", marginBottom: 10, color: "var(--text-4)" }}>{icon}</div>
      <div style={{ fontSize: 14, fontWeight: 560, color: "var(--text)" }}>{title}</div>
      <div style={{ fontSize: 13, marginTop: 4 }}>{body}</div>
    </div>
  );
}

function DeepReport({ deepScout, onBack, backLabel = "Back" }) {
  const summary = deepScout?.summary || null;
  const seconds = deepScout?.elapsedMs ? Math.round(deepScout.elapsedMs / 1000) : null;
  const view = deepScout?.view || null;
  // Highlights is the demo read; the markdown stays one toggle away as the
  // forwardable text artifact. Old payloads without a view render text only.
  const [mode, setMode] = useState(view ? "highlights" : "text");

  function download() {
    const blob = new Blob([deepScout?.markdown || ""], { type: "text/markdown" });
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement("a");
    anchor.href = url;
    anchor.download = `kaval-report-${(deepScout?.repo || "repo").replace(/[^\w.-]+/g, "-")}.md`;
    anchor.click();
    URL.revokeObjectURL(url);
  }

  return (
    <Workspace
      fill
      title={`Kaval report — ${deepScout?.repo || ""}`}
      subtitle="Kaval ran the old and new version of each change and shows what behavior changed — the changes worth confirming, honest misses included. Every claim reproducible from the listed SHAs."
    >
      <Card style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", padding: "14px 16px", borderBottom: "1px solid var(--border)" }}>
          {summary && <Pill tone="accent" dot>flagged {summary.hits}/{summary.analyzed}</Pill>}
          {summary && summary.strongHits > 0 && <Pill tone="accent">{summary.strongHits} strong</Pill>}
          {summary && summary.notRegression > 0 && <Pill>{summary.notRegression} additive (set aside)</Pill>}
          {seconds !== null && <Pill>ran in {seconds}s</Pill>}
          <div style={{ flex: 1 }} />
          <Button onClick={download}>Download report.md</Button>
          <Button variant="ghost" onClick={onBack}>{backLabel}</Button>
        </div>
        {/* The card fills the pinned workspace; only this body scrolls. */}
        {mode === "highlights" && view ? (
          <div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
            <DeepReportHighlights view={view} />
          </div>
        ) : window.marked ? (
          <div
            className="md-report"
            style={{ padding: "18px 24px", fontSize: 13.5, lineHeight: 1.6, color: "var(--text)", flex: 1, minHeight: 0, overflow: "auto" }}
            dangerouslySetInnerHTML={{ __html: window.marked.parse(deepScout?.markdown || "") }}
          />
        ) : (
          <pre style={{ margin: 0, padding: "18px 20px", whiteSpace: "pre-wrap", fontSize: 12.5, lineHeight: 1.55, fontFamily: "var(--mono, ui-monospace, monospace)", color: "var(--text)", flex: 1, minHeight: 0, overflow: "auto" }}>
            {deepScout?.markdown || ""}
          </pre>
        )}
      </Card>
    </Workspace>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [config, setConfig] = useState(null);
  const [configError, setConfigError] = useState("");
  const [client, setClient] = useState(null);
  const [session, setSession] = useState(null);
  const [githubToken, setGithubToken] = useState("");
  const [isAdmin, setIsAdmin] = useState(false);
  const [screen, setScreen] = useState("root");

  // Admin status (server-computed from KAVAL_ADMIN_EMAILS) gates internal-only
  // UI like the cold-email copy button. Mirrored onto a window global so the
  // deep-report tree can read it without prop-threading; the state change forces
  // the re-render that makes the global take effect.
  useEffect(() => {
    window.KAVAL_ADMIN = false;
    if (!session?.access_token) { setIsAdmin(false); return; }
    let cancelled = false;
    fetch("/api/me", { headers: { authorization: `Bearer ${session.access_token}` } })
      .then((response) => response.json())
      .then((payload) => { if (!cancelled) { window.KAVAL_ADMIN = Boolean(payload?.admin); setIsAdmin(Boolean(payload?.admin)); } })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [session]);
  const [connecting, setConnecting] = useState(false);
  const [repos, setRepos] = useState([]);
  const [selectedRepo, setSelectedRepo] = useState(null);
  const [fit, setFit] = useState(null);
  const [changes, setChanges] = useState([]);
  const [selectedChange, setSelectedChange] = useState(null);
  const [result, setResult] = useState(null);
  const [labels, setLabels] = useState({});
  const [loading, setLoading] = useState("");
  const [error, setError] = useState("");
  const [runStage, setRunStage] = useState("");
  const [runEvents, setRunEvents] = useState([]);
  const [runRepo, setRunRepo] = useState("");
  const [notifyState, setNotifyState] = useState("idle"); // idle | sending | sent
  const [notifyDefault, setNotifyDefault] = useState(false); // server auto-registered the run email
  const [deepScout, setDeepScout] = useState(null);
  // { view } once loaded, { error } when the fetch fails — null while loading.
  const [prReport, setPrReport] = useState(null);
  // { installations, prRuns } once loaded — null while loading or signed out.
  const [dashboard, setDashboard] = useState(null);
  const [examples, setExamples] = useState([]);
  const [authReady, setAuthReady] = useState(false);
  const screenRef = useRef(screen);
  const reposLoadedRef = useRef(false);
  const autoRepoRef = useRef(false);

  // Public example reports for the gallery (no auth needed).
  useEffect(() => {
    fetch("/api/examples")
      .then((response) => response.json())
      .then((payload) => setExamples(payload.examples || []))
      .catch(() => {});
  }, []);

  // --- URL routing -----------------------------------------------------------
  // /examples            gallery (public)
  // /examples/:slug      one example report (public)
  // /run/:owner/:name    start (or re-open) a deep-scout run (public)
  // /pr/:owner/:name/:n  PR advisory report — a check's "Details" link (public)
  // /dashboard           installations + PR checks (in-screen sign-in wall;
  //                      the GitHub App's post-install redirect lands here)
  // /try                 URL-only demo hero (public, linked from nowhere)
  // /connect             kick off GitHub OAuth immediately
  // /repos               repo picker (requires session)
  // /                    authed -> /repos, anonymous -> marketing landing
  function navigate(path, { replace = false } = {}) {
    if (window.location.pathname !== path) {
      window.history[replace ? "replaceState" : "pushState"]({}, "", path);
    }
    handleRoute(path);
  }

  function handleRoute(pathname) {
    track("$pageview", { path: pathname });
    const example = pathname.match(/^\/examples\/([\w.-]+)$/);
    const run = pathname.match(/^\/run\/([\w.-]+)\/([\w.-]+)$/);
    const pr = pathname.match(/^\/pr\/([\w.-]+)\/([\w.-]+)\/(\d+)$/);
    if (pathname === "/examples") {
      setScreen("examples");
    } else if (example) {
      openExample(example[1], { fromRoute: true });
    } else if (pr) {
      openPrReport(pr[1], pr[2], pr[3]);
    } else if (run) {
      const repo = `${run[1]}/${run[2]}`;
      // Re-entering the URL of a finished run shows its report; the server
      // caches completed jobs, so this never re-runs the analysis.
      if (deepScoutRef.current?.repo === repo) setScreen("deepReport");
      else runDeepScout(repo);
    } else if (pathname === "/dashboard") {
      setScreen("dashboard");
    } else if (pathname === "/try") {
      setScreen("try");
    } else if (pathname === "/connect") {
      pendingConnectRef.current = true;
      setScreen("connecting");
    } else if (pathname === "/repos") {
      setScreen("repos");
    } else {
      setScreen("root");
    }
  }

  const deepScoutRef = useRef(null);
  const pendingConnectRef = useRef(false);
  const clientRef = useRef(null);
  const configRef = useRef(null);
  useEffect(() => { deepScoutRef.current = deepScout; }, [deepScout]);

  useEffect(() => {
    if (autoRepoRef.current) return;
    autoRepoRef.current = true;
    // Legacy query links (?repo=owner/name) redirect into the path scheme.
    const legacyRepo = new URLSearchParams(window.location.search).get("repo");
    if (legacyRepo && /^[\w.-]+\/[\w.-]+$/.test(legacyRepo)) {
      navigate(`/run/${legacyRepo}`, { replace: true });
      return;
    }
    handleRoute(window.location.pathname);
    const onPop = () => handleRoute(window.location.pathname);
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  // /connect fires OAuth as soon as the Supabase client exists.
  useEffect(() => {
    if (pendingConnectRef.current && client && config) {
      pendingConnectRef.current = false;
      connect();
    }
  }, [client, config]);

  useEffect(() => {
    const root = document.documentElement;
    root.setAttribute("data-theme", "light");
    root.setAttribute("data-density", t.density === "compact" ? "compact" : "comfortable");
    root.style.setProperty("--accent", ACCENTS[t.accent] || ACCENTS["#2f6df0"]);
  }, [t.density, t.accent]);

  useEffect(() => {
    screenRef.current = screen;
  }, [screen]);

  async function loadConfig() {
    setConfigError("");
    try {
      const response = await fetch("/api/config");
      const payload = await response.json();
      if (!payload.ok) throw new Error(payload.error?.message || "Config failed");
      setConfig(payload.config);
      configRef.current = payload.config;
      if (payload.config.urls?.landing) window.KAVAL_LANDING_URL = payload.config.urls.landing;
      // InstallBanner (deep-report.jsx) reads the app's install URL here.
      if (payload.config.githubApp) window.KAVAL_GITHUB_APP = payload.config.githubApp;
      // Launch-day source tag (?ref=hn|ph|twitter) survives SPA navigation so
      // the install banner can carry it into the GitHub App's `state`.
      try {
        const refSource = new URLSearchParams(window.location.search).get("ref");
        if (refSource && /^[\w-]{1,32}$/.test(refSource)) sessionStorage.setItem("kaval_ref", refSource);
      } catch {}
      initPosthog(payload.config.posthog);
      if (payload.config.supabase.configured && window.supabase?.createClient) {
        const supabaseClient = window.supabase.createClient(payload.config.supabase.url, payload.config.supabase.anonKey, {
          auth: {
            persistSession: false,
            autoRefreshToken: false,
            detectSessionInUrl: true,
            flowType: "implicit",
          },
        });
        setClient(supabaseClient);
        clientRef.current = supabaseClient;
        const current = await supabaseClient.auth.getSession();
        const nextSession = current.data?.session || null;
        setSession(nextSession);
        setGithubToken(nextSession?.provider_token || "");
        supabaseClient.auth.onAuthStateChange((_event, next) => {
          setSession(next);
          setGithubToken(next?.provider_token || "");
          if (!next?.provider_token) return;
          if (_event === "SIGNED_IN") track("github_connected");

          // Fresh OAuth return lands on "/" or "/connect" — move to the
          // picker. Direct links (/examples, /run/...) keep their screen.
          const entryRoute = ["root", "connecting"].includes(screenRef.current);
          if (entryRoute) {
            window.history.replaceState({}, "", "/repos");
            setScreen("repos");
          }
          if (entryRoute || !reposLoadedRef.current) {
            loadRepos(next.provider_token, next.access_token);
          }
        });
        if (nextSession?.provider_token) {
          if (["root", "connecting"].includes(screenRef.current)) {
            window.history.replaceState({}, "", "/repos");
            setScreen("repos");
          }
          loadRepos(nextSession.provider_token, nextSession.access_token);
        }
        setAuthReady(true);
      } else if (payload.config.supabase.configured) {
        setConfigError("Supabase browser client did not load. Check network access to the Supabase JS CDN.");
        setAuthReady(true);
      } else {
        setAuthReady(true);
      }
    } catch (err) {
      setConfigError(err.message || "Unable to load config");
    }
  }

  useEffect(() => {
    loadConfig();
  }, []);

  async function apiPost(path, body = {}, tokenOverride, accessTokenOverride) {
    clientLog("api.start", { path });
    const response = await fetch(path, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${accessTokenOverride || session?.access_token || ""}`,
        "x-github-token": tokenOverride || githubToken,
      },
      body: JSON.stringify(body),
    });
    const payload = await response.json();
    if (!response.ok || !payload.ok) {
      const missing = payload.error?.details?.missing?.length ? ` Missing: ${payload.error.details.missing.join(", ")}` : "";
      clientLog("api.error", { path, status: response.status, message: payload.error?.message || "Request failed" });
      throw new Error(`${payload.error?.message || "Request failed"}.${missing}`.replace("..", "."));
    }
    clientLog("api.done", { path });
    return payload;
  }

  async function connect({ redirectTo } = {}) {
    // Refs, not state: connect can be reached from the mount-effect closure
    // (deep-link straight to /run/<repo>) where client/config state is stale.
    const supabaseClient = clientRef.current || client;
    const cfg = configRef.current || config;
    if (!supabaseClient || !cfg) return;
    track("github_connect_clicked");
    setConnecting(true);
    setError("");
    try {
      const { error: authError } = await supabaseClient.auth.signInWithOAuth({
        provider: "github",
        options: {
          scopes: cfg.github.scopes.join(" "),
          // Front-door runs round-trip through GitHub and come back to the
          // exact /run/<repo> URL so the run resumes without re-pasting.
          redirectTo: redirectTo || window.location.origin,
        },
      });
      if (authError) throw authError;
    } catch (err) {
      setError(err.message || "GitHub auth failed");
      setConnecting(false);
    }
  }

  // After the OAuth redirect lands back on /run/<repo>, the session parses
  // out of the URL hash asynchronously — poll briefly instead of firing an
  // unauthenticated request that would bounce to sign-in again.
  async function waitForSession(ms = 6000) {
    const startedAt = Date.now();
    while (Date.now() - startedAt < ms) {
      const supabaseClient = clientRef.current;
      if (supabaseClient) {
        const { data } = await supabaseClient.auth.getSession();
        if (data?.session?.access_token) return data.session;
      }
      await new Promise((resolve) => setTimeout(resolve, 250));
    }
    return null;
  }

  async function loadRepos(tokenOverride, accessTokenOverride) {
    setLoading("repos");
    setError("");
    try {
      const payload = await apiPost("/api/github/repos", {}, tokenOverride, accessTokenOverride);
      setRepos(payload.repos);
      reposLoadedRef.current = true;
      setSelectedRepo((current) => (
        current && payload.repos.some((repo) => repo.id === current.id)
          ? current
          : payload.repos[0] || null
      ));
    } catch (err) {
      setError(err.message || "Could not load repositories");
    } finally {
      setLoading("");
    }
  }

  async function loadFit() {
    if (!selectedRepo) return;
    setScreen("fit");
    setFit(null);
    setLoading("fit");
    setError("");
    try {
      const payload = await apiPost("/api/repo/fit", { repoFullName: selectedRepo.fullName });
      setFit(payload.fit);
    } catch (err) {
      setError(err.message || "Could not scan repository");
    } finally {
      setLoading("");
    }
  }

  // Full pipeline: local clone + executed evidence lanes + pickaxe causality.
  // The server runs deep-scout as a child process; we poll until the report
  // is ready. Same artifact `node scripts/deep-scout.mjs` writes. Works with
  // or without a session — logged-in runs also persist to Supabase.
  async function runDeepScout(repo) {
    setScreen("running");
    setLoading("analyze_repo");
    setError("");
    setDeepScout(null);
    const startedAt = Date.now();
    let wasQueued = false;
    setRunEvents([]);
    setRunRepo(repo);
    setNotifyState("idle");
    setNotifyDefault(false);
    setRunStage("Cloning (first run only), replaying recent fixes, executing changed handlers at base and head…");
    clientLog("flow.deep_scout.start", { repoFullName: repo });
    track("run_started", { repo });
    try {
      // Starting a run requires a GitHub sign-in (viewing reports doesn't).
      // Returning from the OAuth redirect, the session may still be parsing
      // out of the URL hash — wait for it instead of bouncing back to GitHub.
      let accessToken = session?.access_token || "";
      // The GitHub provider token rides along so the server can clone the
      // user's private repos (the whole point of Connect GitHub). It's sent
      // per-request and never stored server-side.
      let ghToken = githubToken || "";
      const resuming = window.location.hash.includes("access_token") || sessionStorage.getItem("kaval.resumeRun") === repo;
      if (!accessToken && resuming) {
        setRunStage("Finishing GitHub sign-in…");
        const restored = await waitForSession();
        accessToken = restored?.access_token || "";
        ghToken = ghToken || restored?.provider_token || "";
      }
      sessionStorage.removeItem("kaval.resumeRun");
      const start = await fetch("/api/deep-scout", {
        method: "POST",
        headers: {
          "content-type": "application/json",
          ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
          ...(ghToken ? { "x-github-token": ghToken } : {}),
        },
        body: JSON.stringify({ repo, commits: 200 }),
      }).then((response) => response.json());
      if (start?.ok === false && start?.error?.code === "auth_required") {
        sessionStorage.setItem("kaval.resumeRun", repo);
        setRunStage("Redirecting to GitHub sign-in — your run starts the moment you're back…");
        track("run_signin_redirect", { repo });
        // Deep links reach here before loadConfig finishes; wait for the
        // Supabase client rather than silently doing nothing.
        for (let i = 0; i < 24 && !clientRef.current; i += 1) await new Promise((resolve) => setTimeout(resolve, 250));
        await connect({ redirectTo: `${window.location.origin}/run/${repo}` });
        return; // the page navigates away; the /run route resumes on return
      }
      if (start?.ok === false) throw new Error(start?.error?.message || "Could not start deep scout");
      // Email-when-ready is ON by default — the server already holds the
      // signed-in runner's verified email and seeded the notify list. The UI
      // just reflects it ("you can leave"); no click needed.
      if (start?.notifyDefault) setNotifyDefault(true);

      let payload = start.status === "done" ? await fetch(`/api/deep-scout?repo=${encodeURIComponent(repo)}`).then((r) => r.json()) : null;
      for (let attempt = 0; !payload && attempt < 720; attempt += 1) {
        await new Promise((resolve) => setTimeout(resolve, 2500));
        const poll = await fetch(`/api/deep-scout?repo=${encodeURIComponent(repo)}`).then((response) => response.json());
        if (poll.status === "error") throw new Error(poll.error || "Deep scout failed");
        if (Array.isArray(poll.events)) setRunEvents(poll.events);
        if (poll.notifyDefault) setNotifyDefault(true);
        if (poll.status === "done") { payload = poll; break; }
        const seconds = Math.round((Date.now() - startedAt) / 1000);
        if (poll.status === "queued") wasQueued = true;
        // Scout runs share a small server-side worker pool; while queued, the
        // honest thing to show is the visible line position, not a misleading
        // elapsed clock. Queue theater beats a spinner.
        const inLine = poll.queue && !poll.queue.running ? poll.queue.position : 0;
        setRunStage(poll.status === "queued"
          ? (inLine > 0
            ? `You're #${inLine} in line — runs take a few minutes each; yours starts automatically`
            : "Queued — another analysis is running; yours starts the moment it finishes")
          : `Replaying recent fixes with executed evidence — ${seconds}s elapsed (typical repo: 1–3 min; large monorepos up to ~8 min)`);
      }
      if (!payload) throw new Error("Deep scout timed out after 30 minutes");

      setDeepScout({ repo, summary: payload.summary, markdown: payload.markdown, view: payload.view || null, elapsedMs: payload.elapsedMs });
      setRunStage("");
      clientLog("flow.deep_scout.done", { repoFullName: repo, elapsedMs: payload.elapsedMs, summary: payload.summary });
      track("run_completed", { repo, durationMs: Date.now() - startedAt, queued: wasQueued });
      track("report_viewed", { repo, isExample: false });
      setScreen("deepReport");
    } catch (err) {
      setError(err.message || "Analysis failed");
      setRunStage("");
      clientLog("flow.deep_scout.error", { repoFullName: repo, message: err.message || "Analysis failed" });
      track("run_failed", { repo, durationMs: Date.now() - startedAt, queued: wasQueued, message: err.message || "Analysis failed" });
      // Failed run: examples screen can show the error to anyone; authed
      // users land back on the picker.
      navigate(session?.provider_token ? "/repos" : "/examples", { replace: true });
    } finally {
      setLoading("");
    }
  }

  function analyzeRepo() {
    if (selectedRepo) navigate(`/run/${selectedRepo.fullName}`);
  }

  // "Email me when it's ready" — register the signed-in user's email against
  // the running job. The server reads the email from the verified session, so
  // we only send the repo. Stays "sent" for the rest of the run.
  async function notifyWhenReady() {
    if (!session?.access_token || !runRepo || notifyState !== "idle") return;
    setNotifyState("sending");
    track("notify_requested", { repo: runRepo });
    try {
      const response = await fetch("/api/notify", {
        method: "POST",
        headers: { "content-type": "application/json", authorization: `Bearer ${session.access_token}` },
        body: JSON.stringify({ repo: runRepo }),
      });
      const payload = await response.json();
      if (!response.ok || !payload.ok) throw new Error(payload.error?.message || "Could not set up the email");
      setNotifyState("sent");
    } catch {
      setNotifyState("idle");
    }
  }

  // Pasted GitHub URL or owner/name, no login required.
  function tryPublicRepo(input) {
    const match = String(input || "").trim()
      .replace(/^https?:\/\/(www\.)?github\.com\//i, "")
      .replace(/\.git$/i, "")
      .match(/^([\w.-]+\/[\w.-]+)/);
    if (!match) {
      setError("Paste a GitHub link like github.com/owner/repo");
      return;
    }
    track("repo_submitted", { repo: match[1], source: screenRef.current });
    navigate(`/run/${match[1]}`);
  }

  async function openExample(slug, { fromRoute = false } = {}) {
    if (!fromRoute) {
      navigate(`/examples/${slug}`);
      return;
    }
    setError("");
    setLoading("example");
    track("example_opened", { slug });
    try {
      const payload = await fetch(`/api/examples/${slug}`).then((response) => response.json());
      if (!payload.ok) throw new Error("Example not found");
      setDeepScout({ repo: payload.example.repo, summary: payload.example.summary, markdown: payload.markdown, view: payload.view || null, elapsedMs: null, isExample: true });
      track("report_viewed", { repo: payload.example.repo, isExample: true });
      setScreen("deepReport");
    } catch (err) {
      setError(err.message || "Could not load example");
      setScreen("examples");
    } finally {
      setLoading("");
    }
  }

  // The page a PR check's "Details" link opens. Public by design — anyone on
  // the PR can read the verdict; errors render in-screen, never a redirect.
  async function openPrReport(owner, name, number) {
    setError("");
    setPrReport(null);
    setScreen("prReport");
    track("pr_report_viewed", { repo: `${owner}/${name}`, number: Number(number) });
    try {
      const payload = await fetch(`/api/pr-run/${owner}/${name}/${number}`).then((response) => response.json());
      if (!payload.ok) throw new Error(payload.error?.message || "No Kaval report for this PR yet.");
      setPrReport({ view: payload.view });
    } catch (err) {
      setPrReport({ error: err.message || "Could not load this PR report" });
    }
  }

  // Dashboard data needs both tokens (Supabase session + GitHub provider
  // token) — the effect fires once they exist, so a deep link or the OAuth
  // round-trip back to /dashboard loads as soon as the session lands.
  useEffect(() => {
    if (screen !== "dashboard" || dashboard !== null) return;
    if (!session?.access_token || !githubToken) return;
    let cancelled = false;
    (async () => {
      track("dashboard_viewed");
      try {
        const response = await fetch("/api/dashboard", {
          headers: {
            authorization: `Bearer ${session.access_token}`,
            "x-github-token": githubToken,
          },
        });
        const payload = await response.json();
        if (!response.ok || !payload.ok) throw new Error(payload.error?.message || "Could not load the dashboard");
        if (!cancelled) setDashboard(payload.dashboard);
      } catch (err) {
        if (!cancelled) setDashboard({ error: err.message || "Could not load the dashboard" });
      }
    })();
    return () => { cancelled = true; };
  }, [screen, dashboard, session, githubToken]);

  // Previous API-only scan (no exec sandbox, no pickaxe) — kept for repos
  // that cannot be cloned locally.
  // eslint-disable-next-line no-unused-vars
  async function historyScanRepo() {
    if (!selectedRepo) return;
    setScreen("running");
    setLoading("analyze_repo");
    setError("");
    setRunStage("Screening 150 commits, deep-analyzing 35 candidates, and ranking evidence");
    clientLog("flow.history_scan.start", { repoFullName: selectedRepo.fullName, scanLimit: 150, deepLimit: 35 });
    try {
      const payload = await apiPost("/api/history/scan", {
        repoId: selectedRepo.id,
        repoFullName: selectedRepo.fullName,
        defaultBranch: selectedRepo.defaultBranch,
        scanLimit: 150,
        deepLimit: 35,
      });
      setFit(payload.fit);
      setChanges(payload.changes || []);
      setSelectedChange(payload.selectedChange || null);
      setResult(payload);
      setLabels(Object.fromEntries((payload.findings || []).map((finding) => [finding.id, finding.label || null])));
      setRunStage("");
      clientLog("flow.history_scan.done", {
        repoFullName: selectedRepo.fullName,
        selectedSha: payload.selectedChange?.sha,
        attempts: payload.attempts?.length || 0,
        findings: payload.findings?.length || 0,
      });
      setScreen("report");
    } catch (err) {
      setError(err.message || "Analysis failed");
      setRunStage("");
      clientLog("flow.history_scan.error", { repoFullName: selectedRepo.fullName, message: err.message || "Analysis failed" });
      setScreen("repos");
    } finally {
      setLoading("");
    }
  }

  async function loadChanges() {
    if (!selectedRepo) return;
    setScreen("changes");
    setChanges([]);
    setSelectedChange(null);
    setLoading("changes");
    setError("");
    try {
      const payload = await apiPost("/api/changes", {
        repoFullName: selectedRepo.fullName,
        branch: selectedRepo.defaultBranch,
        limit: 150,
      });
      setChanges(payload.changes);
      setSelectedChange(payload.changes.find((change) => change.base) || null);
    } catch (err) {
      setError(err.message || "Could not load changes");
    } finally {
      setLoading("");
    }
  }

  async function analyze() {
    if (!selectedRepo || !selectedChange?.base) return;
    setScreen("running");
    setLoading("analyze");
    setRunStage("Comparing selected base/head and extracting evidence");
    setError("");
    try {
      const payload = await apiPost("/api/analyze", {
        repoId: selectedRepo.id,
        repoFullName: selectedRepo.fullName,
        defaultBranch: selectedRepo.defaultBranch,
        sourceType: selectedChange.sourceType || "commit",
        baseSha: selectedChange.base,
        headSha: selectedChange.full,
      });
      setResult(payload);
      setLabels(Object.fromEntries((payload.findings || []).map((finding) => [finding.id, finding.label || null])));
      setScreen("report");
    } catch (err) {
      setError(err.message || "Analysis failed");
      setScreen("changes");
    } finally {
      setLoading("");
      setRunStage("");
    }
  }

  async function downloadReport() {
    if (!result?.run?.id) return;
    try {
      const response = await fetch(`/api/runs/${result.run.id}/report.md`, {
        headers: { authorization: `Bearer ${session?.access_token || ""}` },
      });
      if (!response.ok) throw new Error("Could not generate report");
      const text = await response.text();
      const blob = new Blob([text], { type: "text/markdown" });
      const url = URL.createObjectURL(blob);
      const anchor = document.createElement("a");
      anchor.href = url;
      anchor.download = `kaval-report-${(selectedRepo?.name || "run").replace(/[^\w.-]+/g, "-")}.md`;
      anchor.click();
      URL.revokeObjectURL(url);
    } catch (err) {
      setError(err.message || "Could not download report");
    }
  }

  async function labelFinding(findingId, label) {
    setLabels((current) => ({ ...current, [findingId]: label }));
    try {
      await apiPost(`/api/findings/${findingId}/labels`, { label });
    } catch (err) {
      setError(err.message || "Could not save label");
    }
  }

  const setupBlocking = config && (!config.supabase.configured || !config.results.configured);
  const authed = Boolean(session && githubToken);
  // "dashboard" is listed so the post-install redirect shows its own sign-in
  // wall instead of bouncing a fresh installer to the marketing landing.
  const publicScreens = ["running", "deepReport", "examples", "connecting", "try", "prReport", "dashboard"];

  // No hero in the app: anonymous visitors on private screens go to the
  // marketing landing. Waits for the initial session restore (authReady) and
  // never fires while an OAuth callback hash is being processed.
  useEffect(() => {
    if (!config || setupBlocking || !authReady) return;
    const oauthCallback = /access_token=|error_description=/.test(window.location.hash);
    if (!authed && !connecting && !oauthCallback && !publicScreens.includes(screen)) {
      window.location.replace(LANDING_URL());
    }
  }, [config, setupBlocking, authReady, authed, connecting, screen]);

  if (!config) return <SetupState config={config} error={configError || "Loading configuration..."} onRetry={loadConfig} />;
  if (setupBlocking || configError) return <SetupState config={config} error={configError} onRetry={loadConfig} />;

  // Public screens — deep-scout runs, reports, examples — need no session.
  if (screen === "running") {
    return <RunningScreen runStage={runStage} runEvents={runEvents} error={error}
      emailEnabled={Boolean(config?.email?.enabled)} userEmail={session?.user?.email || ""}
      notifyDefault={notifyDefault} notifyState={notifyState} onNotify={notifyWhenReady} />;
  }
  if (screen === "deepReport") {
    return (
      <DeepReport
        deepScout={deepScout}
        onBack={() => {
          if (deepScout?.isExample) navigate("/examples");
          else if (authed) navigate("/repos");
          else window.location.href = LANDING_URL();
        }}
        backLabel={deepScout?.isExample ? "All examples" : authed ? "Back to repos" : "Back to start"}
      />
    );
  }
  if (screen === "prReport") {
    const prView = prReport?.view || null;
    return (
      <Workspace
        title={prView ? `Kaval PR check — ${prView.repo}#${prView.prNumber}` : "Kaval PR check"}
        subtitle="Advisory pre-merge gate: Kaval ran the changed code at both ends of the PR and shows what behavior changed. Nothing is blocked."
      >
        <Card style={{ overflow: "hidden" }}>
          {prView ? (
            <PrReport view={prView} />
          ) : prReport?.error ? (
            <EmptyState icon={Icon.warn({ s: 24 })} title="No report here" body={prReport.error} />
          ) : (
            <EmptyState icon={Icon.spinner({ s: 24 })} title="Loading PR report" body="" />
          )}
        </Card>
      </Workspace>
    );
  }
  if (screen === "dashboard") {
    const installed = new URLSearchParams(window.location.search).get("installed") === "1";
    return (
      <DashboardScreen
        dashboard={dashboard}
        authed={authed}
        installed={installed}
        onConnect={() => {
          track("dashboard_signin_clicked");
          connect({ redirectTo: `${window.location.origin}/dashboard` });
        }}
        onOpenPr={(run) => navigate(`/pr/${run.repo}/${run.prNumber}`)}
        onRetry={() => setDashboard(null)}
      />
    );
  }
  if (screen === "examples") {
    return (
      <ExamplesScreen
        examples={examples}
        onOpenExample={openExample}
        onTryRepo={tryPublicRepo}
        busy={loading === "analyze_repo" || loading === "example"}
        error={error}
        authed={Boolean(session)}
      />
    );
  }
  if (screen === "connecting") {
    return (
      <Workspace title="Connecting GitHub" subtitle="Opening GitHub sign-in…">
        <Card><EmptyState icon={Icon.spinner({ s: 24 })} title="Redirecting to GitHub" body="Approve access and you'll come straight back." /></Card>
      </Workspace>
    );
  }
  if (screen === "try") {
    return (
      <TryScreen
        onTryRepo={tryPublicRepo}
        onConnect={() => navigate("/connect")}
        onExamples={() => navigate("/examples")}
        busy={loading === "analyze_repo" || loading === "example"}
        error={error}
        authed={Boolean(session)}
      />
    );
  }

  if (!authed) {
    // Redirect effect is in flight (or OAuth callback processing) — hold.
    return (
      <Workspace title="Kaval" subtitle="">
        <Card><EmptyState icon={Icon.spinner({ s: 24 })} title="Loading" body="" /></Card>
      </Workspace>
    );
  }
  if (screen === "fit") return <FitPanel fit={fit} loading={loading === "fit"} error={error} onContinue={loadChanges} onAnalyzeRepo={analyzeRepo} onBack={() => navigate("/repos")} />;
  if (screen === "changes") return <ChangesPanel changes={changes} loading={loading === "changes"} error={error} selectedChange={selectedChange} setSelectedChange={setSelectedChange} onAnalyze={analyze} onBack={() => setScreen("fit")} />;
  if (screen === "report") return <Report result={result} labels={labels} onLabel={labelFinding} onBack={() => navigate("/repos")} onDownloadReport={downloadReport} />;
  // "repos" and authed "root" both land on the picker.
  return <RepoPicker repos={repos} loading={loading === "repos" || loading === "analyze_repo"} selectedRepo={selectedRepo} setSelectedRepo={setSelectedRepo} onRefresh={() => loadRepos()} onAnalyzeRepo={analyzeRepo} error={error} />;
}

// Same cross-file mechanism as ui.jsx/deep-report.jsx: screens that other
// surfaces (or the preview harness) may mount directly.
Object.assign(window, { DashboardScreen, RunningScreen });

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
