import React, { useEffect, useMemo, useRef, useState } from "react";
import {
  ArrowLeft, FolderOpen, ScrollText, Sparkles, Send, Loader2, ShieldCheck,
  Boxes, Bot, Plug, ChevronRight, ChevronDown, FileClock, CornerDownRight, Terminal, ShieldAlert,
  AlertTriangle, Flame, Network, Code2, Clock, Wrench, Info, Rocket, Database, Server,
} from "lucide-react";
import { C, FD, FB, FM, fmt, fmtWhen } from "../theme.js";
import { BinaryLogo, BinaryBadge } from "./Primitives.jsx";
import { fetchProjectChat, fetchProjectNarrative } from "../useAnalysis.js";
import { withClient } from "../client.js";

const PROJ_RISK = { High: C.red, Medium: C.amber, Low: C.cyan, None: C.muted };
const PROJ_TAG = {
  LIVE: { c: C.red, icon: Rocket }, PRODUCTION: { c: C.red, icon: Rocket },
  SECURITY: { c: C.orange, icon: ShieldAlert }, DATA: { c: "#f472b6", icon: Database },
  NETWORK: { c: C.cyan, icon: Network }, CONFIG: { c: C.amber, icon: Code2 },
  DEV: { c: C.blue, icon: Server },
};

// PROJECT VIEW — one cwd, many sessions. A plain-English changelog of what
// happened across sessions, an ENTITY INVENTORY (skills / sub-agents / MCP
// servers, each traceable to the sessions that used it — so a bad skill can be
// traced back), and a cross-session chat ("when did we add column X?") whose
// answers name the exact session. Clicking any session opens its graph.

const ENTITY_META = {
  skills: { label: "Skills", icon: Boxes, color: C.amber },
  subAgents: { label: "Sub-agents", icon: Bot, color: C.blue },
  mcpServers: { label: "MCP servers", icon: Plug, color: C.cyan },
};

function fmtAge(sec) {
  if (!sec) return "";
  const d = Date.now() / 1000 - sec;
  if (d < 3600) return Math.max(1, Math.round(d / 60)) + "m ago";
  if (d < 86400) return Math.round(d / 3600) + "h ago";
  return Math.round(d / 86400) + "d ago";
}

export default function ProjectView({ cwd, llama, onOpenSession, onBack, onBrowse }) {
  const [state, setState] = useState({ status: "loading", data: null, error: null });
  const [sortBy, setSortBy] = useState("cost"); // "cost" (default) | "recent"
  const [narrStatus, setNarrStatus] = useState("idle"); // idle | loading | done

  useEffect(() => {
    let alive = true;
    setState({ status: "loading", data: null, error: null });
    setNarrStatus("idle");
    (async () => {
      // Cold-start race: right after an upload, discovery already lists the project
      // (sessionCount>0) but the per-session briefs may not be parsed yet (the bucket
      // is still syncing the just-written files), so `sessions` comes back empty. Don't
      // show a dead "no sessions" — keep an "analyzing" state and retry for ~15s.
      for (let attempt = 0; attempt < 7 && alive; attempt++) {
        try {
          const r = await fetch(`/api/project?cwd=${encodeURIComponent(cwd)}`, { cache: "no-store", headers: withClient() });
          const j = await r.json();
          if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`);
          if (!alive) return;
          const cold = (j.sessionCount || 0) > 0 && (j.sessions || []).length === 0;
          if (cold && attempt < 6) {
            setState({ status: "analyzing", data: j, error: null });
            await new Promise((res) => setTimeout(res, 2200));
            continue;
          }
          setState({ status: "ready", data: j, error: null });
          return;
        } catch (e) {
          if (alive) setState({ status: "error", data: null, error: String(e) });
          return;
        }
      }
    })();
    return () => { alive = false; };
  }, [cwd]);

  // On the ZeroGPU Space /api/project is deterministic-only (the GPU changelog prose
  // must be invoked via @gradio/client so auth headers forward). Fetch the narrative
  // separately and merge it in. Locally this is a no-op — the prose already arrived.
  // Track the fetch so the changelog panel can say "generating…" instead of falsely
  // reporting "model offline" while the model is still writing it.
  useEffect(() => {
    if (state.status !== "ready" || !state.data || state.data.narrative) return;
    let alive = true;
    setNarrStatus("loading");
    (async () => {
      try {
        const n = await fetchProjectNarrative(cwd);
        if (!alive) return;
        if (n && n.narrative) {
          setState((s) => (s.data ? { ...s, data: { ...s.data, narrative: n.narrative, model: n.model || s.data.model } } : s));
        }
      } catch { /* leave the deterministic-only changelog */ }
      finally { if (alive) setNarrStatus("done"); }
    })();
    return () => { alive = false; };
  }, [state.status, cwd]);

  const d = state.data;
  const name = cwd.split("/").filter(Boolean).pop() || cwd;
  // API returns sessions cost-ranked; re-sort client-side when the user picks recent.
  const sortedSessions = useMemo(() => {
    if (!d) return [];
    const by = sortBy === "recent"
      ? (a, b) => (b.mtime || 0) - (a.mtime || 0)
      : (a, b) => (b.cost || 0) - (a.cost || 0);
    return [...d.sessions].sort(by);
  }, [d, sortBy]);

  return (
    <div style={{ display: "flex", flex: 1, minHeight: 0 }}>
      {/* LEFT — nav + entity inventory (the traceability spine) */}
      <div style={{ width: 320, background: C.panel, borderRight: `1px solid ${C.borderSoft}`, display: "flex", flexDirection: "column", flexShrink: 0 }}>
        <div style={{ padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
          <div className="row lift" onClick={onBack} style={{ display: "flex", alignItems: "center", gap: 7, cursor: "pointer", fontFamily: FM, fontSize: 11.5, color: C.text2, border: `1px solid ${C.borderSoft}`, borderRadius: 7, padding: "7px 9px" }}>
            <ArrowLeft size={13} /> back to session
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <div style={{ width: 30, height: 30, borderRadius: 7, background: C.orange, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
              <FolderOpen size={16} color="#fff" />
            </div>
            <div style={{ minWidth: 0 }}>
              <div style={{ fontFamily: FD, fontWeight: 700, fontSize: 14, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{name}</div>
              <div style={{ fontFamily: FM, fontSize: 9.5, color: C.muted }}>{d ? `${d.sessionCount} sessions${typeof d.totalCost === "number" ? ` · ${fmt(d.totalCost)} cost` : ""}` : "loading…"}</div>
            </div>
          </div>
          <div className="row lift" onClick={onBrowse} style={{ display: "flex", alignItems: "center", gap: 7, cursor: "pointer", fontFamily: FM, fontSize: 10.5, color: C.muted, border: `1px solid ${C.borderSoft}`, borderRadius: 7, padding: "6px 9px" }}>
            <FolderOpen size={12} /> switch project / session
          </div>
        </div>

        <div style={{ padding: "6px 12px 4px", borderTop: `1px solid ${C.borderSoft}` }}>
          <span style={{ fontFamily: FM, fontSize: 10.5, letterSpacing: 0.7, color: C.text2, fontWeight: 600 }}>ENTITIES USED</span>
          <div style={{ fontFamily: FM, fontSize: 9.5, color: C.muted, marginTop: 2 }}>click to trace back to sessions</div>
        </div>
        <div style={{ overflowY: "auto", flex: 1, padding: "4px 8px 14px" }}>
          {d && d.binaries && d.binaries.length > 0 && (
            <BinaryGroup rows={d.binaries} onOpenSession={onOpenSession} />
          )}
          {d && ["skills", "subAgents", "mcpServers"].map((kind) => (
            <EntityGroup key={kind} kind={kind} rows={d.entities[kind]} onOpenSession={onOpenSession} />
          ))}
          {d && !(d.binaries && d.binaries.length) && ["skills", "subAgents", "mcpServers"].every((k) => !d.entities[k].length) && (
            <div style={{ fontFamily: FM, fontSize: 11, color: C.muted, padding: "8px 6px" }}>No binaries, skills, sub-agents, or MCP servers detected.</div>
          )}
        </div>
      </div>

      {/* CENTER — changelog + sessions */}
      <div style={{ flex: 1, minWidth: 0, overflowY: "auto", padding: "20px 24px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ fontFamily: FD, fontWeight: 700, fontSize: 20 }}>Project Report</span>
          <span style={{ fontFamily: FM, fontSize: 11, color: C.muted, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{cwd}</span>
        </div>

        {state.status === "loading" && <Note text="reading the project's sessions and asking the local model for a changelog…" spin />}
        {state.status === "analyzing" && <Note spin text="Just uploaded? Still analyzing this project's sessions — this can take a few seconds. Retrying…" />}
        {state.status === "error" && <Note bad text={`could not load project — ${state.error}`} />}

        {state.status === "ready" && d && (
          <>
            <ProjectReportBand d={d} onOpenSession={onOpenSession} />

            <SecHead icon={ScrollText} color={C.cyan} text="WHAT HAPPENED ACROSS SESSIONS" />
            <div className="pop" style={{ marginTop: 8, fontSize: 14, color: C.text, lineHeight: 1.72, background: `linear-gradient(135deg,${C.card},${C.panel})`, border: `1px solid ${C.borderSoft}`, borderLeft: `3px solid ${C.cyan}`, borderRadius: 9, padding: "14px 16px" }}>
              <div style={{ marginBottom: 8 }}>
                <span style={{ fontFamily: FM, fontSize: 9, letterSpacing: 0.5, color: C.amber, border: `1px solid ${C.amber}`, borderRadius: 4, padding: "1px 6px" }}>
                  GENERATED{d.model ? " · " + d.model.split("/").pop().slice(0, 20) : ""} · reads {d.shown} sessions
                </span>
              </div>
              {renderNarrative(d.narrative, d.sessions, onOpenSession, narrStatus)}
            </div>

            <SecHead icon={FileClock} color={C.orange} text={`SESSIONS · ${d.sessions.length}`} />
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 2 }}>
              <span style={{ fontFamily: FM, fontSize: 10, color: C.muted, flex: 1 }}>
                {sortBy === "cost" ? "ranked by cost (Anthropic token consumption) — what you actually pay for, highest first" : "most recently active first"}
              </span>
              <SortToggle sortBy={sortBy} setSortBy={setSortBy} />
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(280px,1fr))", gap: 10, marginTop: 8 }}>
              {sortedSessions.map((s) => (
                <div key={s.path} className="row lift" onClick={() => onOpenSession(s.path)} style={{ cursor: "pointer", background: C.card, border: `1px solid ${C.borderSoft}`, borderRadius: 9, padding: "11px 13px" }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                    <span style={{ fontFamily: FM, fontSize: 11, color: C.orange }}>[{(s.sessionId || "?").slice(0, 8)}]</span>
                    {typeof s.cost === "number" && <span title={`Anthropic token consumption (cost-weighted)${typeof s.cacheRead === "number" ? ` · ${fmt(s.cacheRead)} cache re-reads (cumulative)` : ""}`} style={{ fontFamily: FM, fontSize: 9.5, color: C.amber, border: `1px solid ${C.amber}55`, borderRadius: 4, padding: "1px 6px" }}>{fmt(s.cost)} cost</span>}
                    <span style={{ flex: 1 }} />
                    <span title={fmtAge(s.mtime) ? "last active " + fmtAge(s.mtime) : ""} style={{ fontFamily: FM, fontSize: 9.5, color: C.muted }}>{s.turns} turns · {s.startedAt ? fmtWhen(s.startedAt) : fmtAge(s.mtime)}</span>
                  </div>
                  <div style={{ fontSize: 12.5, color: C.text2, lineHeight: 1.45, marginTop: 6, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{s.title}</div>
                  <div style={{ display: "flex", flexWrap: "wrap", gap: 5, marginTop: 8 }}>
                    {(s.binaries || []).slice(0, 5).map((b) => (
                      <BinaryBadge key={"b" + b.binary} b={b} />
                    ))}
                    {["skills", "subAgents", "mcpServers"].flatMap((k) =>
                      (s.entities[k] || []).slice(0, 3).map((e) => (
                        <span key={k + e.name} style={{ fontFamily: FM, fontSize: 9, color: ENTITY_META[k].color, border: `1px solid ${ENTITY_META[k].color}55`, borderRadius: 4, padding: "1px 6px" }}>{e.name}</span>
                      ))
                    )}
                  </div>
                </div>
              ))}
            </div>
            <div style={{ height: 20 }} />
          </>
        )}
      </div>

      {/* RIGHT — project chat */}
      <div style={{ width: 392, background: C.panel, borderLeft: `1px solid ${C.borderSoft}`, flexShrink: 0 }}>
        <ProjectChat cwd={cwd} llama={llama} onOpenSession={onOpenSession} />
      </div>
    </div>
  );
}

// PROJECT REPORT BAND — the executive summary across the project's sessions:
// stat cards (sessions / total cost / risk / tools / date range) and the high-impact
// "actions worth reviewing" rolled up across every session, each traceable to the
// session it happened in. Sessions-as-context, the same lens as the Session Report.
function ProjectReportBand({ d, onOpenSession }) {
  const imp = d.impact || { riskLevel: "None", actions: [] };
  const riskC = PROJ_RISK[imp.riskLevel] || C.muted;
  const toolCount = (d.binaries?.length || 0) + (d.entities?.mcpServers?.length || 0);
  const dates = (d.sessions || []).map((s) => s.startedAt).filter(Boolean).sort();
  const range = dates.length
    ? (dates.length === 1 ? fmtWhen(dates[0]) : `${fmtWhen(dates[0])} → ${fmtWhen(dates[dates.length - 1])}`)
    : "—";
  const topSessions = [...(d.sessions || [])].sort((a, b) => (b.cost || 0) - (a.cost || 0)).slice(0, 3);

  return (
    <>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(150px,1fr))", gap: 10, marginTop: 14 }}>
        <PStat label="SESSIONS" value={String(d.sessionCount ?? d.sessions?.length ?? 0)} sub={`${d.shown || 0} analyzed`} />
        <PStat label="TOKEN COST" value={fmt(d.totalCost || 0)} valueColor={C.orange} grad sub="cost-weighted tokens · not $" />
        <PStat label="RISK LEVEL" value={imp.riskLevel} valueColor={riskC} icon={ShieldAlert} iconColor={riskC}
          sub={imp.actions?.length ? `${imp.actions.length} action${imp.actions.length === 1 ? "" : "s"} to review` : "no high-impact actions"} />
        <PStat label="TOOLS" value={String(toolCount)} icon={Wrench} iconColor={C.cyan} sub="binaries + MCP" />
        <PStat label="ACTIVE" value={range === "—" ? "—" : ""} icon={Clock} iconColor={C.muted} sub={range} small />
      </div>

      {imp.actions && imp.actions.length > 0 && (
        <>
          <SecHead icon={AlertTriangle} color={C.amber} text="ACTIONS WORTH REVIEWING" />
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(320px,1fr))", gap: 8, marginTop: 8 }}>
            {imp.actions.map((a, i) => {
              const tg = PROJ_TAG[a.tag] || { c: C.muted, icon: Info };
              const Ic = tg.icon;
              const sess = (a.sessions || [])[0];
              return (
                <div key={i} className="row lift" onClick={() => sess && onOpenSession(sess.path)}
                  style={{ display: "flex", gap: 10, alignItems: "center", cursor: sess ? "pointer" : "default", background: C.card, border: `1px solid ${C.borderSoft}`, borderRadius: 9, padding: "10px 12px" }}>
                  <div style={{ width: 26, height: 26, borderRadius: 7, background: C.elevated, border: `1px solid ${tg.c}`, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
                    <Ic size={13} color={tg.c} />
                  </div>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ fontSize: 12.5, fontWeight: 600, color: C.text }}>{a.title}</div>
                    <div style={{ fontSize: 10.5, color: C.muted, lineHeight: 1.4, marginTop: 1 }}>
                      {a.detail} · {(a.sessions || []).length} session{(a.sessions || []).length === 1 ? "" : "s"}
                    </div>
                  </div>
                  <span style={{ fontFamily: FM, fontSize: 8.5, letterSpacing: 0.5, color: tg.c, border: `1px solid ${tg.c}`, borderRadius: 4, padding: "1px 6px", flexShrink: 0 }}>{a.tag}</span>
                </div>
              );
            })}
          </div>
        </>
      )}

      {topSessions.length > 0 && (
        <>
          <SecHead icon={Flame} color={C.orange} text="WHERE THE COST WENT" />
          <div style={{ marginTop: 8 }}>
            {topSessions.map((s) => {
              const pct = Math.round((100 * (s.cost || 0)) / Math.max(1, d.totalCost || 1));
              return (
                <div key={s.path} className="row lift" onClick={() => onOpenSession(s.path)}
                  style={{ display: "flex", gap: 10, alignItems: "center", cursor: "pointer", padding: "7px 9px", borderRadius: 7 }}>
                  <span style={{ fontFamily: FM, fontSize: 10, color: C.orange, flexShrink: 0 }}>[{(s.sessionId || "?").slice(0, 8)}]</span>
                  <span style={{ fontSize: 12, color: C.text2, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.title}</span>
                  <div style={{ width: 90, height: 5, background: C.black, borderRadius: 3, overflow: "hidden", flexShrink: 0 }}>
                    <div style={{ width: `${Math.max(3, pct)}%`, height: "100%", background: `linear-gradient(90deg,${C.orange},${C.amber})` }} />
                  </div>
                  <span style={{ fontFamily: FM, fontSize: 10, color: C.orange, minWidth: 64, textAlign: "right", flexShrink: 0 }}>{fmt(s.cost || 0)} · {pct}%</span>
                </div>
              );
            })}
          </div>
        </>
      )}
    </>
  );
}

function PStat({ label, value, valueColor, sub, grad, icon: Icon, iconColor, small }) {
  return (
    <div style={{ background: `linear-gradient(135deg,${C.card},${C.panel})`, border: `1px solid ${C.borderSoft}`, borderRadius: 10, padding: "11px 13px" }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <span style={{ fontFamily: FM, fontSize: 8.5, letterSpacing: 0.6, color: C.muted }}>{label}</span>
        {Icon && <Icon size={13} color={iconColor || C.muted} />}
      </div>
      {value && <div className={grad ? "hb" : ""} style={{ fontFamily: FD, fontWeight: 700, fontSize: small ? 13 : 22, color: grad ? undefined : (valueColor || C.text), marginTop: 5, lineHeight: 1.1 }}>{value}</div>}
      {sub && <div style={{ fontSize: small ? 10.5 : 10, color: C.muted, marginTop: value ? 4 : 6, lineHeight: 1.35 }}>{sub}</div>}
    </div>
  );
}

// BINARIES — the real tools run via Bash across the project (npx remotion ->
// remotion, railway, …), a separate dimension from tool calls. Each row traces
// back to every session (and turns) that ran it, like the entity rows.
function BinaryGroup({ rows, onOpenSession }) {
  if (!rows || !rows.length) return null;
  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 6px 4px" }}>
        <Terminal size={12} color={C.orange} />
        <span style={{ fontFamily: FM, fontSize: 10, letterSpacing: 0.5, color: C.orange }}>Binaries</span>
        <span style={{ fontFamily: FM, fontSize: 9.5, color: C.muted }}>{rows.length}</span>
      </div>
      {rows.map((b) => <BinaryEntityRow key={b.binary} b={b} onOpenSession={onOpenSession} />)}
    </div>
  );
}

function BinaryEntityRow({ b, onOpenSession }) {
  const [open, setOpen] = useState(false);
  const label = b.product || b.name || b.binary;
  return (
    <div style={{ marginBottom: 3 }}>
      <div className="row" onClick={() => setOpen((o) => !o)} style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 8px", borderRadius: 7, cursor: "pointer", borderLeft: `2px solid ${C.orange}` }}>
        {open ? <ChevronDown size={12} color={C.muted} /> : <ChevronRight size={12} color={C.muted} />}
        <BinaryLogo b={b} size={15} />
        <span title={b.blurb || ""} style={{ fontFamily: FM, fontSize: 11.5, color: C.text, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          {label}{!b.identified && <span style={{ color: C.muted }}> · {b.binary}</span>}
        </span>
        {b.security && <ShieldAlert size={11} color={C.amber} title={b.security} />}
        <span style={{ fontFamily: FM, fontSize: 9, color: C.muted }}>×{b.total} · {(b.sessions || []).length} sess</span>
      </div>
      {open && (
        <div style={{ paddingLeft: 18, paddingTop: 3 }}>
          {b.blurb && <div style={{ fontFamily: FB, fontSize: 9.5, color: C.muted, padding: "2px 6px 4px", lineHeight: 1.45 }}>{b.blurb}</div>}
          {b.security && <div style={{ fontFamily: FM, fontSize: 9, color: C.amber, padding: "0 6px 4px", display: "flex", alignItems: "center", gap: 4 }}><ShieldAlert size={10} /> {b.security}</div>}
          {(b.sessions || []).map((s) => (
            <div key={s.path} className="row lift" onClick={() => onOpenSession(s.path)} title="open this session" style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 8px", borderRadius: 6, cursor: "pointer", fontFamily: FM, fontSize: 10 }}>
              <CornerDownRight size={10} color={C.orange} />
              <span style={{ color: C.orange }}>[{(s.sessionId || "?").slice(0, 8)}]</span>
              <span style={{ color: C.muted }}>×{s.count} · turns {(s.turns || []).slice(0, 6).join(",")}{(s.turns || []).length > 6 ? "…" : ""}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function EntityGroup({ kind, rows, onOpenSession }) {
  const meta = ENTITY_META[kind];
  const Icon = meta.icon;
  if (!rows || !rows.length) return null;
  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 6px 4px" }}>
        <Icon size={12} color={meta.color} />
        <span style={{ fontFamily: FM, fontSize: 10, letterSpacing: 0.5, color: meta.color }}>{meta.label}</span>
        <span style={{ fontFamily: FM, fontSize: 9.5, color: C.muted }}>{rows.length}</span>
      </div>
      {rows.map((e) => <EntityRow key={e.name} e={e} meta={meta} onOpenSession={onOpenSession} />)}
    </div>
  );
}

function EntityRow({ e, meta, onOpenSession }) {
  const [open, setOpen] = useState(false);
  return (
    <div style={{ marginBottom: 3 }}>
      <div className="row" onClick={() => setOpen((o) => !o)} style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 8px", borderRadius: 7, cursor: "pointer", borderLeft: `2px solid ${meta.color}` }}>
        {open ? <ChevronDown size={12} color={C.muted} /> : <ChevronRight size={12} color={C.muted} />}
        <span style={{ fontFamily: FM, fontSize: 11.5, color: C.text, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{e.name}</span>
        <span style={{ fontFamily: FM, fontSize: 9, color: C.muted }}>×{e.total} · {e.sessions.length} sess</span>
      </div>
      {open && (
        <div style={{ paddingLeft: 18, paddingTop: 3 }}>
          {e.tools && e.tools.length > 0 && (
            <div style={{ fontFamily: FM, fontSize: 9, color: C.muted, padding: "2px 6px 4px" }}>tools: {e.tools.slice(0, 8).join(", ")}{e.tools.length > 8 ? "…" : ""}</div>
          )}
          {e.sessions.map((s) => (
            <div key={s.path} className="row lift" onClick={() => onOpenSession(s.path)} title="open this session" style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 8px", borderRadius: 6, cursor: "pointer", fontFamily: FM, fontSize: 10 }}>
              <CornerDownRight size={10} color={meta.color} />
              <span style={{ color: C.orange }}>[{(s.sessionId || "?").slice(0, 8)}]</span>
              <span style={{ color: C.muted }}>×{s.count} · turns {s.turns.slice(0, 6).join(",")}{s.turns.length > 6 ? "…" : ""}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// Make [sessionId] mentions in the narrative clickable -> open that session.
function renderNarrative(text, sessions, onOpenSession, status) {
  if (!text) {
    // The prose is still being generated on the GPU — say so. Only call it "offline"
    // once the fetch has actually returned empty (status === "done"), never while waiting.
    if (status !== "done") {
      return (
        <span style={{ color: C.muted, display: "inline-flex", alignItems: "center", gap: 7 }}>
          <Loader2 size={13} className="spin" /> Writing the cross-session summary — this can take up to ~15 seconds…
        </span>
      );
    }
    return <span style={{ color: C.muted }}>No changelog available (model offline — give it a moment and reopen, or it may genuinely be down).</span>;
  }
  const byShort = {};
  sessions.forEach((s) => { if (s.sessionId) byShort[s.sessionId.slice(0, 8)] = s.path; });
  const parts = text.split(/(\[[0-9a-f]{6,8}\])/gi);
  return parts.map((p, i) => {
    const m = p.match(/^\[([0-9a-f]{6,8})\]$/i);
    if (m && byShort[m[1]]) {
      return (
        <span key={i} onClick={() => onOpenSession(byShort[m[1]])} style={{ cursor: "pointer", color: C.orange, fontFamily: FM, fontSize: 12.5, borderBottom: `1px dotted ${C.orange}` }} title="open this session">{p}</span>
      );
    }
    return <span key={i}>{p}</span>;
  });
}

function ProjectChat({ cwd, llama, onOpenSession }) {
  const [msgs, setMsgs] = useState([]);
  const [q, setQ] = useState("");
  const [busy, setBusy] = useState(false);
  const scroller = useRef(null);
  useEffect(() => { setMsgs([]); }, [cwd]);
  useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight; }, [msgs, busy]);

  async function ask(text) {
    const question = (text ?? q).trim();
    if (!question || busy) return;
    setQ(""); setMsgs((m) => [...m, { role: "user", text: question }]); setBusy(true);
    try {
      const j = await fetchProjectChat(question, cwd);
      setMsgs((m) => [...m, { role: "assistant", ...j }]);
    } catch (e) {
      setMsgs((m) => [...m, { role: "assistant", answer: `Couldn't answer: ${String(e)}`, sessionHits: [], error: true }]);
    } finally { setBusy(false); }
  }

  const SUGG = ["What was built across these sessions?", "Which session changed prod config?", "Where was the smruti skill used?"];

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <div style={{ padding: "13px 15px 11px", borderBottom: `1px solid ${C.borderSoft}` }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <Sparkles size={15} color={C.orange} />
          <span style={{ fontFamily: FD, fontWeight: 700, fontSize: 14 }}>Ask <span style={{ color: C.orange }}>Her</span> <span style={{ color: C.muted, fontWeight: 500, fontSize: 11 }}>· this project</span></span>
          <div style={{ flex: 1 }} />
          <span style={{ fontFamily: FM, fontSize: 9, color: llama ? C.cyan : C.muted, border: `1px solid ${llama ? C.cyan : C.border}`, borderRadius: 5, padding: "1px 6px", display: "flex", alignItems: "center", gap: 3 }}>
            <ShieldCheck size={10} /> {llama ? "LOCAL MODEL" : "MODEL OFF"}
          </span>
        </div>
        <div style={{ fontFamily: FM, fontSize: 10, color: C.muted, marginTop: 6, lineHeight: 1.5 }}>Her finds WHICH session something happened in · names it · open to go deeper</div>
      </div>

      <div ref={scroller} style={{ flex: 1, overflowY: "auto", padding: "12px 13px" }}>
        {msgs.length === 0 && SUGG.map((s) => (
          <div key={s} className="row lift" onClick={() => ask(s)} style={{ cursor: "pointer", fontFamily: FM, fontSize: 12, color: C.text2, border: `1px solid ${C.borderSoft}`, borderRadius: 8, padding: "9px 11px", marginBottom: 7, display: "flex", gap: 7, alignItems: "center" }}>
            <CornerDownRight size={12} color={C.orange} /> {s}
          </div>
        ))}
        {msgs.map((m, i) => m.role === "user" ? (
          <div key={i} style={{ display: "flex", justifyContent: "flex-end", marginBottom: 10 }}>
            <div style={{ maxWidth: "88%", background: C.orangeMut, border: `1px solid ${C.orangeBd}`, borderRadius: "10px 10px 2px 10px", padding: "8px 11px", fontSize: 12.5, color: C.text, lineHeight: 1.5 }}>{m.text}</div>
          </div>
        ) : (
          <div key={i} style={{ marginBottom: 14 }}>
            <div style={{ display: "flex", gap: 7, alignItems: "center", marginBottom: 5 }}>
              <Sparkles size={11} color={m.error ? C.red : C.orange} />
              <span style={{ fontFamily: FM, fontSize: 9, letterSpacing: 0.4, color: C.muted }}>{m.error ? "ERROR" : `GENERATED${m.model ? " · " + m.model.split("/").pop().slice(0, 18) : ""} · across sessions`}</span>
            </div>
            <div style={{ fontSize: 13, color: C.text2, lineHeight: 1.6, background: `linear-gradient(135deg,${C.card},${C.panel})`, border: `1px solid ${C.borderSoft}`, borderRadius: "2px 10px 10px 10px", padding: "10px 12px" }}>
              {m.answer}
              {m.sessionHits && m.sessionHits.length > 0 && (
                <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 10 }}>
                  {m.sessionHits.map((h) => (
                    <span key={h.path} className="lift" onClick={() => onOpenSession(h.path)} title={h.title} style={{ cursor: "pointer", fontFamily: FM, fontSize: 10.5, color: C.orange, background: C.black, border: `1px solid ${C.orangeBd}`, borderRadius: 6, padding: "2px 8px" }}>
                      open [{(h.sessionId || "?").slice(0, 8)}] →
                    </span>
                  ))}
                </div>
              )}
            </div>
          </div>
        ))}
        {busy && <div style={{ display: "flex", gap: 7, alignItems: "center", color: C.muted, fontFamily: FM, fontSize: 11 }}><Loader2 size={13} className="spin" /> searching across the project's sessions…</div>}
      </div>

      <div style={{ padding: "10px 12px", borderTop: `1px solid ${C.borderSoft}` }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, background: C.black, border: `1px solid ${C.borderSoft}`, borderRadius: 9, padding: "7px 9px 7px 12px" }}>
          <input value={q} onChange={(e) => setQ(e.target.value)} onKeyDown={(e) => e.key === "Enter" && ask()} placeholder="when did we add column x to sql?" style={{ flex: 1, background: "transparent", border: "none", outline: "none", color: C.text, fontFamily: FM, fontSize: 12.5 }} />
          <div onClick={() => ask()} className="lift" style={{ cursor: busy ? "default" : "pointer", width: 30, height: 30, borderRadius: 7, background: q.trim() && !busy ? C.orange : C.elevated, display: "flex", alignItems: "center", justifyContent: "center" }}>
            <Send size={14} color={q.trim() && !busy ? "#fff" : C.muted} />
          </div>
        </div>
      </div>
    </div>
  );
}

function SecHead({ icon: Icon, color, text }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 7, marginTop: 20, marginBottom: 2 }}>
      <Icon size={15} color={color} />
      <span style={{ fontFamily: FD, fontWeight: 700, fontSize: 13, letterSpacing: 0.4, color }}>{text}</span>
    </div>
  );
}

function Note({ text, bad, spin }) {
  return (
    <div style={{ marginTop: 18, display: "flex", alignItems: "center", gap: 8, fontFamily: FM, fontSize: 12, color: bad ? C.red : C.muted }}>
      {spin && <Loader2 size={14} className="spin" />} {text}
    </div>
  );
}

function SortToggle({ sortBy, setSortBy }) {
  const Opt = ({ id, label }) => (
    <span onClick={() => setSortBy(id)} className="lift" style={{ cursor: "pointer", fontFamily: FM, fontSize: 9.5, padding: "2px 9px", borderRadius: 5, color: sortBy === id ? C.text : C.muted, background: sortBy === id ? C.elevated : "transparent", border: `1px solid ${sortBy === id ? C.border : "transparent"}` }}>{label}</span>
  );
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <span style={{ fontFamily: FM, fontSize: 9, color: C.muted }}>sort</span>
      <div style={{ display: "flex", gap: 2, background: C.black, border: `1px solid ${C.borderSoft}`, borderRadius: 7, padding: 2 }}>
        <Opt id="cost" label="by cost" />
        <Opt id="recent" label="recent" />
      </div>
    </div>
  );
}
