// Page-level views, Overview, Fleet, Connections, Governance, Recommendations, ComingSoon.
const { Pill, OwnerChip, Sparkbars, Ring, Kpi, Tabs, AppGlyph, currency } = window;
const { WorryItem, AppTile, Pipeline, BlockedDeploys, FleetTable, ConnectionsGraph } = window;
const { Icons, DATA } = window;
const { useState, useMemo } = React;

// ---------- Signal item (the CTO's decision queue) ----------
function SignalItem({ item, onOpen }) {
  const iconMap = {
    risk: { key: "Alert", klass: "risk" },
    warn: { key: "Warn",  klass: "warn" },
    info: { key: "Spark", klass: "info" },
    ok:   { key: "Check", klass: "ok" },
  };
  const cfg = iconMap[item.sev] || iconMap.info;
  const Ic = Icons[cfg.key];
  const isOpp = item.kind === "opportunity";
  const pill = isOpp
    ? <Pill kind="info">opportunity</Pill>
    : <Pill kind={cfg.klass === "risk" ? "risk" : "warn"}>decision</Pill>;
  return (
    <div className="worry" onClick={() => onOpen && onOpen(item)}>
      <div className={"icon " + cfg.klass}><Ic/></div>
      <div className="body">
        <h4>
          <span>{item.title}</span>
          {pill}
          <span className="path">{item.path}</span>
        </h4>
        <div className="detail">{item.detail}</div>
        <div className="actions">
          <button className="btn sm primary" onClick={(e) => { e.stopPropagation(); onOpen && onOpen(item); }}>
            {item.cta} <Icons.Arrow s={12}/>
          </button>
          <button className="btn sm ghost" onClick={(e) => e.stopPropagation()}>Dismiss</button>
        </div>
      </div>
    </div>
  );
}

// ---------- Recommendation card ----------
function RecommendationCard({ rec, onOpen }) {
  const Ic = rec.kind === "rule" ? Icons.Shield : Icons.Book;
  const kindLabel = rec.kind === "rule" ? "Rule" : "Knowledge";
  return (
    <div className="card" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      <div style={{ display: "flex", gap: 10, alignItems: "flex-start" }}>
        <span style={{ width: 28, height: 28, borderRadius: 8, background: "var(--cream-3)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink)", flexShrink: 0 }}>
          <Ic s={14}/>
        </span>
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 4 }}>
            <Pill kind={rec.kind === "rule" ? "neutral" : "info"}>{kindLabel}</Pill>
          </div>
          <h3 style={{ fontSize: 15, color: "var(--ink)" }}>{rec.title}</h3>
          <div style={{ fontSize: 12, color: "var(--muted)", marginTop: 3 }}>{rec.subtitle}</div>
        </div>
      </div>
      <p style={{ fontSize: 13, color: "var(--ink-3)", lineHeight: 1.55, margin: 0 }}>{rec.detail}</p>
      {rec.apps && (
        <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
          {rec.apps.map((a,i) => <Pill key={i} kind="neutral">{a}</Pill>)}
        </div>
      )}
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 10, paddingTop: 10, borderTop: "1px dashed var(--tan-2)" }}>
        <span style={{ fontSize: 11.5, color: "var(--muted)" }}>{rec.impact}</span>
        <button className="btn primary sm" onClick={() => onOpen && onOpen(rec)}>
          {rec.cta} <Icons.Arrow s={12}/>
        </button>
      </div>
    </div>
  );
}

// ---------- Overview (CTO) ----------
function OverviewPage({ onOpenApp, onOpenSignal, onNav, onOpenContract }) {
  const t = DATA.FLEET_TOTALS;
  const sevRank = { risk: 0, warn: 1, info: 2, ok: 3 };
  const sorted = [...DATA.SIGNALS].sort((a, b) => (sevRank[a.sev] ?? 9) - (sevRank[b.sev] ?? 9));
  const topMerged = sorted.slice(0, 3);
  const handleOpen = (item) => onOpenSignal && onOpenSignal(item);

  const mostUsed = [...DATA.APPS].sort((a,b) => b.monthlyUsers - a.monthlyUsers).slice(0, 3);

  return (
    <div className="fade-in overview">
      {/* Hero, date + headline, no competing buttons */}
      <header className="overview-hero">
        <div className="eyebrow">Monday, Apr 27 · Meridia · BYOC: AWS us-east-1</div>
        <h1 className="page-title" style={{ marginTop: 8 }}>
          {t.apps} apps <em>shipping value.</em>
        </h1>
        <p className="page-sub">{t.monthlyUsers} people using them this month. Here's what's compounding, and where to spend your next hour.</p>
      </header>

      {/* Activity / usage stats, top-of-page summary */}
      <section className="overview-stats">
        <div className="stat">
          <div className="stat-label">Operators</div>
          <div className="stat-value">{t.operators}</div>
          <div className="stat-delta pos">+3 this month</div>
          <Sparkbars values={[0.32, 0.34, 0.38, 0.41, 0.44, 0.48, 0.52, 0.56, 0.62, 0.68, 0.74, 0.82]}/>
        </div>
        <div className="stat">
          <div className="stat-label">Apps</div>
          <div className="stat-value">{t.apps}</div>
          <div className="stat-delta pos">+6 this month</div>
          <Sparkbars values={[0.28, 0.32, 0.36, 0.39, 0.42, 0.48, 0.54, 0.6, 0.66, 0.72, 0.8, 0.88]}/>
        </div>
        <div className="stat">
          <div className="stat-label">Monthly users</div>
          <div className="stat-value">{t.monthlyUsers}</div>
          <div className="stat-delta pos">+22% vs last mo</div>
          <Sparkbars values={[0.36, 0.4, 0.44, 0.48, 0.5, 0.54, 0.58, 0.64, 0.7, 0.76, 0.84, 0.92]}/>
        </div>
      </section>

      {/* Recommendations, the unified stream: risks, attention, opportunities, codify-this. */}
      <section className="overview-section">
        <div className="section-head">
          <div>
            <div className="section-eye">Today's focus</div>
            <h2 className="section-title">Recommendations</h2>
            <div className="section-sub">Risks, opportunities, and things to codify, ranked by what needs your call next.</div>
          </div>
          <button className="btn sm ghost" onClick={() => onNav("recommendations")}>View all {DATA.SIGNALS.length} <Icons.Arrow s={12}/></button>
        </div>
        <div className="signals-list">
          {topMerged.map(s => <SignalItem key={s.id} item={s} onOpen={handleOpen}/>)}
        </div>
      </section>

      {/* Fleet, most used apps */}
      <section className="overview-section">
        <div className="section-head">
          <div>
            <div className="section-eye">Fleet</div>
            <h2 className="section-title">Most used</h2>
          </div>
          <button className="btn sm ghost" onClick={() => onNav("fleet")}>Open fleet · 24 apps <Icons.Arrow s={12}/></button>
        </div>
        <div className="grid g-3">
          {mostUsed.map(a => <AppTile key={a.id} app={a} onOpen={onOpenApp}/>)}
        </div>
      </section>

      {/* Connections + Governance side-by-side */}
      <section className="overview-section">
        <div className="grid g-12" style={{ gap: 16 }}>
          <div className="col-7 card">
            <div className="card-head" style={{ marginBottom: 10 }}>
              <div>
                <div className="section-eye">Connections</div>
                <h3>Shared across the fleet</h3>
                <div className="subtitle">{(DATA.SHARED_RESOURCES.schemas().length + DATA.SHARED_RESOURCES.services().length)} resources used by 2+ apps. The choke points.</div>
              </div>
              <button className="btn sm ghost" onClick={() => onNav("connections")}>Open <Icons.Arrow s={12}/></button>
            </div>
            <div style={{ display: "grid", gap: 10 }}>
              {DATA.SHARED_RESOURCES.schemas().slice(0, 4).map(s => (
                <div key={s.name} onClick={() => onOpenContract && onOpenContract(s)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 0", borderBottom: "1px solid var(--tan-2)", cursor: "pointer" }}>
                  <Icons.Database s={13}/>
                  <span className="sig" style={{ fontSize: 12, flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.name}</span>
                  <span style={{ color: "var(--muted)", fontSize: 11.5 }}>{s.apps.length} apps</span>
                  {s.contract === "typed"   ? <Pill kind="ok">typed</Pill>
                   : s.contract === "partial"? <Pill kind="warn">partial</Pill>
                   :                            <Pill kind="risk">no contract</Pill>}
                </div>
              ))}
            </div>
          </div>
          <div className="col-5 card audit-card">
            <div className="card-head" style={{ marginBottom: 18 }}>
              <div>
                <div className="section-eye">Governance</div>
                <h3>Rules holding</h3>
                <div className="subtitle">Last evaluated 38m ago</div>
              </div>
              <button className="btn sm ghost" onClick={() => onNav("governance")}>Open <Icons.Arrow s={12}/></button>
            </div>

            <div style={{ display: "grid", gap: 18 }}>
              {[
                { label: "Rules active",      value: "22",     suffix: <span style={{ fontSize: 17, color: "var(--muted-2)" }}> of 28</span> },
                { label: "Tests run · 30d",   value: "14,820", suffix: <span style={{ fontSize: 14, color: "var(--green)", marginLeft: 10 }}>+18%</span> },
                { label: "Average pass rate", value: "97.4",   suffix: <span style={{ fontSize: 20, color: "var(--muted-2)" }}>%</span> },
              ].map((s, i) => (
                <div key={i} style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", borderTop: i === 0 ? "none" : "1px solid var(--tan-2)", paddingTop: i === 0 ? 0 : 14 }}>
                  <span style={{ fontSize: 12.5, color: "var(--muted)" }}>{s.label}</span>
                  <span style={{ fontFamily: "var(--serif)", fontSize: 32, lineHeight: 1, letterSpacing: "-0.03em", color: "var(--ink)", fontWeight: 400 }}>
                    {s.value}{s.suffix}
                  </span>
                </div>
              ))}
            </div>
          </div>
        </div>
      </section>

      {/* Cost attribution */}
      <section className="overview-section">
        <div className="section-head">
          <div>
            <div className="section-eye">Cost attribution</div>
            <h2 className="section-title">This month's spend</h2>
          </div>
          <button className="btn sm ghost" onClick={() => onNav("costs")}>Open <Icons.Arrow s={12}/></button>
        </div>
        <div className="grid g-12" style={{ gap: 16 }}>
          {(() => {
            const totalBudget = DATA.TEAM_BUDGETS.reduce((s, t) => s + t.budget, 0);
            const totalSpent = DATA.TEAM_BUDGETS.reduce((s, t) => s + t.spent, 0);
            const unattributed = DATA.TOP_LINE_ITEMS.filter(i => i.unattributed).reduce((s, i) => s + i.cost, 0);
            return (
              <>
                <div className="col-7 card">
                  <div className="subtitle" style={{ marginBottom: 6 }}>By team · {currency(totalSpent)} of {currency(totalBudget)}</div>
                  <div style={{ display: "grid", gap: 10, marginTop: 10 }}>
                    {DATA.TEAM_BUDGETS.slice(0, 5).map(t => {
                      const pct = Math.round((t.spent / t.budget) * 100);
                      const over = pct >= 80;
                      return (
                        <div key={t.team} style={{ display: "grid", gridTemplateColumns: "140px 1fr 80px", gap: 10, alignItems: "center", fontSize: 12.5 }}>
                          <div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
                            <span style={{ width: 8, height: 8, borderRadius: 2, background: t.color, flexShrink: 0 }}/>
                            <span style={{ color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.team}</span>
                          </div>
                          <div style={{ height: 6, background: "var(--cream-3)", borderRadius: 3, overflow: "hidden" }}>
                            <div style={{ width: pct + "%", height: "100%", background: over ? "var(--orange)" : t.color }}/>
                          </div>
                          <div style={{ textAlign: "right", color: "var(--muted)", fontVariantNumeric: "tabular-nums" }}>
                            {currency(t.spent)}<span style={{ color: "var(--muted-2)" }}> / {currency(t.budget)}</span>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </div>
                <div className="col-5 card">
                  <div className="subtitle" style={{ marginBottom: 6 }}>Unattributed</div>
                  <div style={{ fontFamily: "var(--serif)", fontSize: 32, color: "var(--orange)", letterSpacing: "-0.02em", marginTop: 4 }}>{currency(unattributed)}</div>
                  <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 6, lineHeight: 1.5 }}>
                    Spend on shared keys that can't be pinned to one team. Split into per-app keys to recover attribution.
                  </div>
                  <button className="btn sm" style={{ marginTop: 12 }} onClick={() => onNav("costs")}>
                    Review shared keys <Icons.Arrow s={11}/>
                  </button>
                </div>
              </>
            );
          })()}
        </div>
      </section>
    </div>
  );
}

// ---------- Fleet page ----------
function FleetPage({ onOpenApp, role }) {
  const isOperator = role === "operator";
  const myApps  = useMemo(() => DATA.APPS.filter(a => a.owner === "ml"), []);
  const myIds   = useMemo(() => new Set(myApps.map(a => a.id)), [myApps]);
  const [q, setQ] = useState("");
  const [filter, setFilter] = useState("all"); // all | risk | warn | ok | flagged
  const [group, setGroup] = useState(isOperator ? "none" : "team");
  const [sort, setSort]   = useState("users"); // users | cost | audit | deploys | deps

  const filtered = useMemo(() => {
    let list = DATA.APPS.slice();
    if (q.trim()) {
      const needle = q.trim().toLowerCase();
      list = list.filter(a =>
        a.name.toLowerCase().includes(needle) ||
        a.team.toLowerCase().includes(needle) ||
        a.generator.toLowerCase().includes(needle) ||
        (DATA.OPERATORS[a.owner]?.name.toLowerCase().includes(needle))
      );
    }
    if (filter === "risk")    list = list.filter(a => a.audit < 70);
    if (filter === "warn")    list = list.filter(a => a.audit >= 70 && a.audit < 85);
    if (filter === "healthy") list = list.filter(a => a.audit >= 85 && a.flags.length === 0);
    if (filter === "flagged") list = list.filter(a => a.flags.length > 0);

    const sorters = {
      users:   (a, b) => (b.monthlyUsers || 0) - (a.monthlyUsers || 0),
      cost:    (a, b) => b.cost - a.cost,
      audit:   (a, b) => b.audit - a.audit,
      deploys: (a, b) => b.deploys7d - a.deploys7d,
      deps:    (a, b) => b.depsAge - a.depsAge,
    };
    list.sort(sorters[sort] || sorters.users);
    return list;
  }, [q, filter, group, sort]);

  return (
    <div className="fade-in">
      <div style={{ marginBottom: 22 }}>
        <div className="eyebrow">{isOperator ? `My apps · ${myApps.length} · plus ${DATA.APPS.length - myApps.length} others in the org` : `Fleet · ${DATA.APPS.length} apps · 7 teams`}</div>
        <h1 className="page-title" style={{ marginTop: 8 }}>{isOperator ? <>My <em>apps.</em></> : <>The <em>fleet.</em></>}</h1>
        <p className="page-sub">{isOperator ? "What you own. Other people's apps are listed below — read-only." : "Every AI-generated app across Meridia, grouped by team. Click any row for connections, cost, and audit."}</p>
      </div>

      {isOperator ? (
        <>
          <div className="card flush" style={{ marginBottom: 24 }}>
            <div className="card-head" style={{ padding: "14px 16px", marginBottom: 0 }}>
              <div className="card-title">My apps</div>
              <span className="tag">{myApps.length}</span>
            </div>
            <FleetTable apps={myApps} groupBy="none" onOpen={onOpenApp}/>
          </div>
          <OtherAppsBrowser apps={DATA.APPS.filter(a => !myIds.has(a.id))} onOpen={onOpenApp}/>
        </>
      ) : (
        <div className="card flush">
          <div className="card-head" style={{ padding: "14px 16px 14px", marginBottom: 0, gap: 10, flexWrap: "wrap" }}>
            <div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
              <div style={{ position: "relative", display: "flex", alignItems: "center" }}>
                <span style={{ position: "absolute", left: 10, color: "var(--muted-2)", display: "flex" }}><Icons.Search s={12}/></span>
                <input
                  value={q}
                  onChange={e => setQ(e.target.value)}
                  placeholder="Search apps, teams, operators…"
                  style={{
                    fontFamily: "inherit", fontSize: 12.5,
                    padding: "7px 12px 7px 30px", width: 260,
                    background: "var(--cream-3)", color: "var(--ink)",
                    border: "1px solid var(--tan-2)", borderRadius: 7,
                    outline: "none",
                  }}/>
              </div>
              <FleetSelect label="Filter" value={filter} setValue={setFilter} options={[
                ["all", "All apps"], ["risk", "At risk (<70)"], ["warn", "Watch (70–84)"],
                ["healthy", "Healthy"], ["flagged", "Has flags"],
              ]}/>
              <FleetSelect label="Group" value={group} setValue={setGroup} options={[
                ["none", "None"], ["team", "Team"], ["owner", "Operator"],
                ["generator", "Generator"], ["env", "Environment"],
              ]}/>
              <FleetSelect label="Sort" value={sort} setValue={setSort} options={[
                ["users", "Users"], ["cost", "Cost"], ["audit", "Audit"],
                ["deploys", "Deploys 7d"],
              ]}/>
            </div>
            <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
              <span className="tag">{filtered.length} of {DATA.APPS.length}</span>
            </div>
          </div>
          <FleetTable apps={filtered} groupBy={group} onOpen={onOpenApp}/>
        </div>
      )}
    </div>
  );
}

function OtherAppsBrowser({ apps, onOpen }) {
  // Group apps by operator
  const byOp = useMemo(() => {
    const map = {};
    apps.forEach(a => {
      if (!map[a.owner]) map[a.owner] = [];
      map[a.owner].push(a);
    });
    return map;
  }, [apps]);

  const operators = Object.keys(byOp).map(id => ({
    ...DATA.OPERATORS[id],
    count: byOp[id].length,
  })).sort((a, b) => b.count - a.count);

  const [selected, setSelected] = useState(operators[0]?.id || null);
  const [q, setQ] = useState("");

  const visible = (selected ? byOp[selected] || [] : []).filter(a => {
    if (!q.trim()) return true;
    const n = q.trim().toLowerCase();
    return a.name.toLowerCase().includes(n) || a.team.toLowerCase().includes(n);
  });

  const sel = DATA.OPERATORS[selected];

  return (
    <div className="card flush">
      <div className="card-head" style={{ padding: "14px 16px", marginBottom: 0, gap: 10 }}>
        <div className="card-title">Other apps in the org</div>
        <span className="tag">{apps.length} apps · {operators.length} operators</span>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "240px 1fr", borderTop: "1px solid var(--tan-2)" }}>
        {/* Sidebar: operators */}
        <div style={{ borderRight: "1px solid var(--tan-2)", padding: "8px 6px", maxHeight: 520, overflow: "auto" }}>
          {operators.map(op => {
            const active = op.id === selected;
            return (
              <button key={op.id} onClick={() => setSelected(op.id)}
                style={{
                  width: "100%", display: "flex", alignItems: "center", gap: 10,
                  padding: "9px 10px", border: "none", borderRadius: 7,
                  background: active ? "var(--cream-3)" : "transparent",
                  color: "var(--ink)", fontFamily: "inherit", cursor: "pointer", textAlign: "left",
                  marginBottom: 1,
                }}>
                <span style={{
                  width: 26, height: 26, borderRadius: 13,
                  background: op.color, color: "#1f1c17",
                  display: "inline-flex", alignItems: "center", justifyContent: "center",
                  fontSize: 10.5, fontWeight: 600, flexShrink: 0,
                }}>{op.initials}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 12.5, fontWeight: active ? 500 : 400, color: "var(--ink)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{op.name}</div>
                  <div style={{ fontSize: 10.5, color: "var(--muted-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{op.team}</div>
                </div>
                <span style={{ fontSize: 11, color: "var(--muted)", fontVariantNumeric: "tabular-nums" }}>{op.count}</span>
              </button>
            );
          })}
        </div>

        {/* Right: apps for selected operator */}
        <div>
          {sel && (
            <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--tan-2)", display: "flex", alignItems: "center", gap: 12 }}>
              <div>
                <div style={{ fontSize: 13, fontWeight: 500, color: "var(--ink)" }}>{sel.name}'s apps</div>
                <div style={{ fontSize: 11.5, color: "var(--muted)" }}>{sel.team} · {visible.length} app{visible.length === 1 ? "" : "s"} · read-only</div>
              </div>
              <div style={{ marginLeft: "auto", position: "relative", display: "flex", alignItems: "center" }}>
                <span style={{ position: "absolute", left: 10, color: "var(--muted-2)", display: "flex" }}><Icons.Search s={12}/></span>
                <input value={q} onChange={e => setQ(e.target.value)} placeholder="Filter…"
                  style={{ fontFamily: "inherit", fontSize: 12.5, padding: "6px 12px 6px 28px", width: 180,
                    background: "var(--cream-3)", color: "var(--ink)", border: "1px solid var(--tan-2)", borderRadius: 7, outline: "none" }}/>
              </div>
            </div>
          )}
          <FleetTable apps={visible} groupBy="none" onOpen={onOpen} readOnly/>
        </div>
      </div>
    </div>
  );
}

function FleetSelect({ label, value, setValue, options }) {
  const [open, setOpen] = useState(false);
  const cur = options.find(o => o[0] === value);
  return (
    <div style={{ position: "relative" }}>
      <button className="btn sm" onClick={() => setOpen(o => !o)}>
        <span style={{ color: "var(--muted)" }}>{label}:</span>
        <span style={{ color: "var(--ink)", marginLeft: 4 }}>{cur ? cur[1] : value}</span>
        <Icons.Caret s={10}/>
      </button>
      {open && (
        <>
          <div onClick={() => setOpen(false)} style={{ position: "fixed", inset: 0, zIndex: 30 }}/>
          <div style={{
            position: "absolute", top: "calc(100% + 4px)", left: 0, zIndex: 31,
            background: "var(--cream)", border: "1px solid var(--tan-2)", borderRadius: 8,
            boxShadow: "0 8px 24px -12px rgba(60,40,20,0.25)",
            minWidth: 180, padding: 4,
          }}>
            {options.map(([v, l]) => (
              <button key={v}
                onClick={() => { setValue(v); setOpen(false); }}
                style={{
                  display: "block", width: "100%", textAlign: "left",
                  padding: "7px 10px", fontSize: 12.5,
                  background: v === value ? "var(--tan-1)" : "transparent",
                  color: "var(--ink)", border: "none", borderRadius: 5,
                  cursor: "pointer", fontFamily: "inherit",
                }}
                onMouseEnter={e => { if (v !== value) e.currentTarget.style.background = "var(--cream-3)"; }}
                onMouseLeave={e => { if (v !== value) e.currentTarget.style.background = "transparent"; }}>
                {l}
              </button>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

// ---------- Connections page ----------
function ConnectionsPage({ onOpenApp, onOpenService, onOpenContract, initialTab, tpPosture, setTpPosture, policyFor, setPolicy }) {
  const [tab, setTab] = useState(initialTab || "third-party"); // third-party | data
  React.useEffect(() => { if (initialTab) setTab(initialTab); }, [initialTab]);
  const [tpMode, setTpMode] = useState("graph");  // graph | all | api | llm
  const [dataMode, setDataMode] = useState("graph"); // graph | schemas | unmapped
  const [filter, setFilter] = useState("all"); // all / shared / non-det

  const schemas = DATA.SCHEMAS;
  const services = DATA.SERVICES;
  const sharedSchemas = DATA.SHARED_RESOURCES.schemas();
  const noContract = sharedSchemas.filter(s => s.contract !== "typed").length;
  const sharedKeyCount = services.filter(s => s.sharedKey && s.apps.length > 1).length;
  const llmCount = services.filter(s => s.kind === "LLM").length;
  const apiCount = services.filter(s => s.kind !== "LLM").length;
  const newCount = services.filter(s => (s.firstSeen ?? 9999) <= 30).length;

  // Schemas that show up in app.connections but not in the SCHEMAS table => "unmapped".
  const knownSchemaNames = new Set(schemas.map(s => s.name));
  const unmappedMap = {};
  DATA.APPS.forEach(a => (a.connections || []).forEach(c => {
    if (c.type === "db" && !knownSchemaNames.has(c.name)) {
      if (!unmappedMap[c.name]) unmappedMap[c.name] = { name: c.name, apps: [], det: c.det };
      if (!unmappedMap[c.name].apps.includes(a.name)) unmappedMap[c.name].apps.push(a.name);
    }
  }));
  const unmapped = Object.values(unmappedMap);

  return (
    <div className="fade-in">
      <div style={{ marginBottom: 22, display: "flex", justifyContent: "space-between", alignItems: "flex-end", gap: 24 }}>
        <div>
          <div className="eyebrow">Connections · {tab === "third-party" ? "Third party services" : "Data sources"}</div>
          <h1 className="page-title" style={{ marginTop: 8 }}>What connects to <em>what.</em></h1>
          <p className="page-sub">{tab === "third-party"
            ? "Every external API and LLM the fleet talks to. Set the default outbound posture, and override per-domain in the table below."
            : "Every table, warehouse, and bucket the fleet reads. Codify a contract on shared schemas so changes go through a single reviewable diff."}</p>
        </div>
        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          <button className="btn sm"><Icons.Search s={12}/> Filter</button>
          <button className="btn sm"><Icons.Upload s={12}/> Export</button>
        </div>
      </div>

      {/* (sidebar handles tab switching now) */}

      {tab === "third-party" && (
        <>
          {/* Shared third-party summary */}
          <div className="card" style={{ marginBottom: 16, background: "var(--cream-2)" }}>
            <div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
              <span style={{ width: 36, height: 36, borderRadius: 10, background: "var(--paper)", border: "1px solid var(--tan-2)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink)" }}>
                <Icons.Cloud/>
              </span>
              <div style={{ flex: 1 }}>
                <h3 style={{ fontFamily: "var(--serif)", fontSize: 22, fontWeight: 400, letterSpacing: "-0.02em" }}>Third-party services</h3>
                <p style={{ fontSize: 13, color: "var(--muted)", marginTop: 4, maxWidth: "62ch" }}>
                  External APIs and LLMs every app talks to. {sharedKeyCount > 0 && <><b style={{ color: "var(--red)" }}>{sharedKeyCount} services share an API key</b> across multiple apps, the biggest blast-radius risk on this page.</>}
                </p>

                {/* Default outbound posture */}
                <div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 14, paddingTop: 14, borderTop: "1px solid var(--tan-2)" }}>
                  <div style={{ minWidth: 200 }}>
                    <div style={{ fontSize: 12, fontWeight: 500, color: "var(--ink)" }}>Default outbound posture</div>
                    <div style={{ fontSize: 11.5, color: "var(--muted)", marginTop: 2 }}>
                      {tpPosture === "default-deny"
                        ? "Apps can only call services on the allowlist."
                        : "Apps can call any service unless explicitly blocked."}
                    </div>
                  </div>
                  <div style={{ display: "flex", background: "var(--paper)", border: "1px solid var(--tan-2)", borderRadius: 8, padding: 3 }}>
                    <button onClick={() => setTpPosture("default-deny")}
                      style={{
                        padding: "6px 12px", fontSize: 12, fontWeight: 500,
                        border: "none", borderRadius: 6, cursor: "pointer",
                        background: tpPosture === "default-deny" ? "var(--ink-2)" : "transparent",
                        color: tpPosture === "default-deny" ? "#fff" : "var(--muted)",
                      }}>
                      Default-deny
                    </button>
                    <button onClick={() => setTpPosture("default-allow")}
                      style={{
                        padding: "6px 12px", fontSize: 12, fontWeight: 500,
                        border: "none", borderRadius: 6, cursor: "pointer",
                        background: tpPosture === "default-allow" ? "var(--ink-2)" : "transparent",
                        color: tpPosture === "default-allow" ? "#fff" : "var(--muted)",
                      }}>
                      Default-allow
                    </button>
                  </div>
                  {newCount > 0 && (
                    <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 8, padding: "6px 10px", background: "var(--amber-bg)", border: "1px solid var(--amber-border)", borderRadius: 8 }}>
                      <span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--amber)" }}/>
                      <span style={{ fontSize: 12, color: "var(--ink)", fontWeight: 500 }}>{newCount} first seen in last 30d</span>
                    </div>
                  )}
                </div>
              </div>
              <div style={{ display: "flex", gap: 18, alignItems: "center", flexShrink: 0 }}>
                <Stat label="APIs" value={apiCount}/>
                <Stat label="LLMs" value={llmCount}/>
                <Stat label="Shared key" value={sharedKeyCount} kind={sharedKeyCount > 0 ? "warn" : "ok"}/>
              </div>
            </div>
          </div>

          <div style={{ display: "flex", gap: 10, marginBottom: 14, alignItems: "center" }}>
            <Tabs value={tpMode} onChange={setTpMode} items={[
              { value: "graph", label: "Graph" },
              { value: "all",   label: `All (${services.length})` },
              { value: "api",   label: `API (${apiCount})` },
              { value: "llm",   label: `LLM (${llmCount})` },
            ]}/>
          </div>

          {tpMode === "graph" && <BipartiteGraph onOpenApp={onOpenApp} filter={filter} setFilter={setFilter} domain="services"/>}

          {tpMode !== "graph" && (
            <div className="card flush">
              <table className="t">
                <thead><tr><th>Service</th><th>Kind</th><th>Used by</th><th>Behavior</th><th>Key</th><th>Policy</th><th>Cost / mo</th></tr></thead>
                <tbody>
                  {services
                    .filter(s => tpMode === "all" || (tpMode === "llm" ? s.kind === "LLM" : s.kind !== "LLM"))
                    .slice()
                    .sort((a, b) => {
                      // new (≤30d) first, then blocked, then everyone else
                      const order = (s) => {
                        if ((s.firstSeen ?? 9999) <= 30) return 0;
                        if (policyFor(s.name) === "blocked") return 1;
                        return 2;
                      };
                      return order(a) - order(b);
                    })
                    .map((s, i) => {
                    const policy = policyFor(s.name);
                    const isNew = (s.firstSeen ?? 9999) <= 30;
                    return (
                    <tr key={i} style={{ cursor: "pointer", background: isNew ? "var(--amber-bg)" : undefined }} onClick={() => onOpenService && onOpenService(s)}>
                      <td>
                        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                          <span style={{ width: 26, height: 26, borderRadius: 7, background: s.kind === "LLM" ? "color-mix(in srgb, var(--orange) 14%, var(--paper))" : "var(--cream-3)", display: "flex", alignItems: "center", justifyContent: "center", color: s.kind === "LLM" ? "var(--orange-dark)" : "var(--ink-3)" }}>
                            {s.kind === "LLM" ? <Icons.Spark s={12}/> : <Icons.Cloud s={13}/>}
                          </span>
                          <span className="sig" style={{ fontSize: 12.5, color: "var(--ink)" }}>{s.name}</span>
                          {isNew && <Pill kind="warn">first seen {s.firstSeen}d ago</Pill>}
                          {s.sharedKey && s.apps.length > 1 && <Pill kind="risk">shared key</Pill>}
                        </div>
                      </td>
                      <td><Pill kind="neutral">{s.kind}</Pill></td>
                      <td>
                        {s.apps.length === 0
                          ? <span style={{ fontSize: 11.5, color: "var(--muted-2)" }}>—</span>
                          : (
                            <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
                              {s.apps.slice(0, 2).map((a, j) => <Pill key={j} kind="neutral">{a}</Pill>)}
                              {s.apps.length > 2 && <Pill kind="neutral">+{s.apps.length - 2}</Pill>}
                            </div>
                          )}
                      </td>
                      <td>{s.det ? <Pill kind="ok">deterministic</Pill> : <Pill kind="warn">non-det</Pill>}</td>
                      <td>
                        {!s.sharedKey
                          ? <Pill kind="ok">per-app</Pill>
                          : <Pill kind="risk">shared · {s.apps.length} apps</Pill>}
                      </td>
                      <td onClick={(e) => e.stopPropagation()}>
                        <PolicyCell state={policy} posture={tpPosture} onSet={(v) => setPolicy(s.name, v)}/>
                      </td>
                      <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink)" }}>{currency(s.cost)}</td>
                    </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          )}
        </>
      )}

      {tab === "data" && (
        <>
          {/* Shared data summary */}
          <div className="card" style={{ marginBottom: 16, background: "var(--cream-2)" }}>
            <div style={{ display: "flex", gap: 14, alignItems: "flex-start" }}>
              <span style={{ width: 36, height: 36, borderRadius: 10, background: "var(--paper)", border: "1px solid var(--tan-2)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink)" }}>
                <Icons.Database/>
              </span>
              <div style={{ flex: 1 }}>
                <h3 style={{ fontFamily: "var(--serif)", fontSize: 22, fontWeight: 400, letterSpacing: "-0.02em" }}>Schemas + data</h3>
                <p style={{ fontSize: 13, color: "var(--muted)", marginTop: 4, maxWidth: "68ch" }}>
                  Tables and warehouses the fleet reads. A codified contract on shared schemas means a change goes through a single reviewable diff, apps can't break silently.
                  {noContract > 0 && <> <b style={{ color: "var(--red)" }}>{noContract} shared schema{noContract > 1 ? "s" : ""} still missing a typed contract.</b></>}
                </p>
              </div>
              <div style={{ display: "flex", gap: 18, alignItems: "center" }}>
                <Stat label="Schemas" value={schemas.length}/>
                <Stat label="Shared" value={sharedSchemas.length}/>
                <Stat label="No contract" value={noContract} kind={noContract > 0 ? "warn" : "ok"}/>
              </div>
            </div>
          </div>

          <div style={{ display: "flex", gap: 10, marginBottom: 14, alignItems: "center" }}>
            <Tabs value={dataMode} onChange={setDataMode} items={[
              { value: "graph",    label: "Graph" },
              { value: "schemas",  label: `Schemas (${schemas.length})` },
              { value: "unmapped", label: `Unmapped (${unmapped.length})` },
            ]}/>
          </div>

          {dataMode === "graph" && <BipartiteGraph onOpenApp={onOpenApp} filter={filter} setFilter={setFilter} domain="schemas"/>}

          {dataMode === "schemas" && (
            <div className="card flush">
              <table className="t">
                <thead>
                  <tr>
                    <th>Schema</th><th>Kind</th><th>Used by</th><th>Reads / mo</th><th>Contract</th><th>Health</th><th></th>
                  </tr>
                </thead>
                <tbody>
                  {schemas.map(s => (
                    <tr key={s.name} style={{ cursor: "pointer" }} onClick={() => onOpenContract && onOpenContract(s)}>
                      <td>
                        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                          <span style={{ width: 26, height: 26, borderRadius: 7, background: "var(--cream-3)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-3)" }}>
                            <Icons.Database s={13}/>
                          </span>
                          <span className="sig" style={{ fontSize: 12.5, color: "var(--ink)" }}>{s.name}</span>
                        </div>
                      </td>
                      <td style={{ color: "var(--muted)" }}>{s.kind}</td>
                      <td>
                        <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
                          {s.apps.slice(0, 2).map((a, i) => <Pill key={i} kind="neutral">{a}</Pill>)}
                          {s.apps.length > 2 && <Pill kind="neutral">+{s.apps.length - 2}</Pill>}
                        </div>
                      </td>
                      <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink)" }}>{(s.reads/1000).toFixed(1)}k</td>
                      <td>
                        {s.contract === "typed" && <Pill kind="ok">typed</Pill>}
                        {s.contract === "partial" && <Pill kind="warn">partial</Pill>}
                        {s.contract === "none" && <Pill kind="risk">none</Pill>}
                      </td>
                      <td>
                        {s.health === "ok"   && <Pill kind="ok">stable</Pill>}
                        {s.health === "warn" && <Pill kind="warn">drift</Pill>}
                        {s.health === "risk" && <Pill kind="risk">exposed</Pill>}
                      </td>
                      <td style={{ color: "var(--muted)" }}><Icons.Arrow/></td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}

          {dataMode === "unmapped" && (
            <div className="card flush">
              {unmapped.length === 0 ? (
                <div style={{ padding: 28, textAlign: "center", color: "var(--muted)" }}>
                  Every schema referenced by an app is registered. Nothing to map.
                </div>
              ) : (
                <table className="t">
                  <thead>
                    <tr><th>Schema</th><th>Used by</th><th>Status</th><th></th></tr>
                  </thead>
                  <tbody>
                    {unmapped.map(u => (
                      <tr key={u.name}>
                        <td>
                          <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                            <span style={{ width: 26, height: 26, borderRadius: 7, background: "var(--cream-3)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-3)" }}>
                              <Icons.Database s={13}/>
                            </span>
                            <span className="sig" style={{ fontSize: 12.5, color: "var(--ink)" }}>{u.name}</span>
                          </div>
                        </td>
                        <td>
                          <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
                            {u.apps.slice(0, 3).map((a, i) => <Pill key={i} kind="neutral">{a}</Pill>)}
                            {u.apps.length > 3 && <Pill kind="neutral">+{u.apps.length - 3}</Pill>}
                          </div>
                        </td>
                        <td><Pill kind="warn">unmapped</Pill></td>
                        <td><button className="btn sm">Register</button></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              )}
            </div>
          )}
        </>
      )}
    </div>
  );
}

// Compact stat for the summary cards.
function Stat({ label, value, kind }) {
  const color = kind === "warn" ? "var(--red)" : kind === "ok" ? "var(--ink)" : "var(--ink)";
  return (
    <div style={{ textAlign: "right", minWidth: 60 }}>
      <div style={{ fontFamily: "var(--serif)", fontSize: 26, fontWeight: 400, letterSpacing: "-0.02em", color, fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>{value}</div>
      <div style={{ fontSize: 10.5, color: "var(--muted)", letterSpacing: "0.06em", textTransform: "uppercase", marginTop: 4 }}>{label}</div>
    </div>
  );
}

// Policy cell — renders state and (in pending) inline allow/block actions.
// Policy cell — renders state. Inline allow/block on rows that inherit posture.
function PolicyCell({ state, posture, onSet }) {
  if (state === "blocked") return <Pill kind="risk">Blocked</Pill>;
  if (state === "allowed") return <Pill kind="ok">Allowlisted</Pill>;
  // Inherits posture — no explicit policy on this service.
  if (posture === "default-deny") {
    return <span style={{ fontSize: 11.5, color: "var(--muted-2)", fontStyle: "italic" }}>Default-deny</span>;
  }
  return <span style={{ fontSize: 11.5, color: "var(--muted-2)", fontStyle: "italic" }}>Default-allow</span>;
}

// ---------- Bipartite graph ----------
// Apps on the left, resources (schemas + services) on the right.
// Edges thicken on shared resources, dashed on non-deterministic.
function BipartiteGraph({ onOpenApp, filter, setFilter, domain }) {
  const apps = DATA.APPS;
  const schemas = DATA.SCHEMAS;
  const services = DATA.SERVICES;

  // Resources column: filtered by domain prop. "schemas" → only schemas, "services" → only services, default → both.
  const includeSchemas  = !domain || domain === "schemas";
  const includeServices = !domain || domain === "services";
  const resources = [
    ...(includeSchemas ? [...schemas].sort((a,b) => b.apps.length - a.apps.length).map(s => ({
      kind: "schema", name: s.name, apps: s.apps,
      contract: s.contract, health: s.health,
    })) : []),
    ...(includeServices ? [...services].sort((a,b) => b.apps.length - a.apps.length).map(s => ({
      kind: "service", subkind: s.kind, name: s.name, apps: s.apps,
      det: s.det, sharedKey: s.sharedKey,
    })) : []),
  ];

  // Apps sorted by total connections (desc) for cleaner edge bundling.
  // When a domain is set, only show apps that actually connect to a resource in this domain.
  const resourceNames = new Set(resources.map(r => r.name));
  const appsFiltered = domain
    ? apps.filter(a => (a.connections || []).some(c => resourceNames.has(c.name)))
    : apps;
  const appsSorted = [...appsFiltered].sort((a,b) => {
    const ca = (a.connections || []).filter(c => !domain || resourceNames.has(c.name)).length;
    const cb = (b.connections || []).filter(c => !domain || resourceNames.has(c.name)).length;
    return cb - ca;
  });

  // Build edges. Match by name.
  const edges = [];
  appsSorted.forEach((a, ai) => {
    (a.connections || []).forEach(c => {
      const ri = resources.findIndex(r => r.name === c.name);
      if (ri < 0) return;
      const r = resources[ri];
      const isShared = r.apps.length > 1;
      const isNonDet = c.det === false;
      // filter
      if (filter === "shared" && !isShared) return;
      if (filter === "non-det" && !isNonDet) return;
      edges.push({ ai, ri, det: c.det !== false, shared: isShared, type: c.type, kind: r.kind });
    });
  });

  // Hover state
  const [hover, setHover] = useState(null); // {kind: "app"|"resource", index}

  const isEdgeLit = (e) => {
    if (!hover) return false;
    if (hover.kind === "app") return e.ai === hover.index;
    if (hover.kind === "resource") return e.ri === hover.index;
    return false;
  };
  const isAppLit = (i) => {
    if (!hover) return true;
    if (hover.kind === "app") return i === hover.index;
    return edges.some(e => e.ai === i && e.ri === hover.index);
  };
  const isResourceLit = (i) => {
    if (!hover) return true;
    if (hover.kind === "resource") return i === hover.index;
    return edges.some(e => e.ri === i && e.ai === hover.index);
  };

  // Layout sizes
  const W = 1080, rowH = 32;
  const padTop = 36;
  const appsH = appsSorted.length * rowH;
  const resH = resources.length * rowH;
  const H = Math.max(padTop + appsH, padTop + resH) + 20;
  const appX = 220;     // right edge of app column (where edges start)
  const resX = 740;     // left edge of resources column (where edges end)

  const appY = (i) => padTop + i * rowH + rowH/2;
  const resY = (i) => padTop + i * rowH + rowH/2;

  return (
    <div className="card flush" style={{ padding: 0, overflow: "hidden" }}>
      <div className="bp-head">
        <div className="legend">
          <span><span className="sw" style={{ background: "var(--ink-2)" }}/>app</span>
          {includeSchemas && <span><span className="sw" style={{ background: "var(--cream-3)", border: "1px solid var(--tan)" }}/>schema</span>}
          {includeServices && <span><span className="sw" style={{ background: "color-mix(in srgb, var(--orange) 14%, var(--paper))", border: "1px solid var(--orange-soft)" }}/>service</span>}
          <span><span className="sw" style={{ background: "var(--ink-2)", height: 3, borderRadius: 1 }}/>shared (2+ apps)</span>
          <span><span className="sw" style={{ background: "transparent", border: "1px dashed var(--amber)" }}/>non-det</span>
        </div>
        <div style={{ display: "flex", gap: 6 }}>
          <button className={`bp-filter ${filter === "all" ? "on" : ""}`} onClick={() => setFilter("all")}>All edges</button>
          <button className={`bp-filter ${filter === "shared" ? "on" : ""}`} onClick={() => setFilter("shared")}>Shared only</button>
          <button className={`bp-filter ${filter === "non-det" ? "on" : ""}`} onClick={() => setFilter("non-det")}>Non-det only</button>
        </div>
      </div>
      <div className="bp-graph-wrap">
        <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMin meet" style={{ width: "100%", height: H, display: "block" }}>
          {/* column headers */}
          <text x={20} y={20} fontSize="10.5" fontWeight="600" fill="var(--muted)" letterSpacing="0.08em" style={{ textTransform: "uppercase" }}>APPS · {appsSorted.length}</text>
          <text x={resX + 30} y={20} fontSize="10.5" fontWeight="600" fill="var(--muted)" letterSpacing="0.08em" style={{ textTransform: "uppercase" }}>RESOURCES · {resources.length}</text>

          {/* edges */}
          <g>
            {edges.map((e, i) => {
              const lit = isEdgeLit(e);
              const dim = hover && !lit;
              const x1 = appX, y1 = appY(e.ai);
              const x2 = resX, y2 = resY(e.ri);
              const cx1 = x1 + 80, cx2 = x2 - 80;
              const stroke = e.shared ? "var(--ink-2)" : "var(--tan)";
              const sw = e.shared ? 1.6 : 1.0;
              return (
                <path key={i}
                  d={`M${x1},${y1} C${cx1},${y1} ${cx2},${y2} ${x2},${y2}`}
                  fill="none"
                  stroke={lit ? "var(--orange)" : stroke}
                  strokeWidth={lit ? 2 : sw}
                  strokeOpacity={dim ? 0.06 : (lit ? 0.95 : (e.shared ? 0.5 : 0.32))}
                  strokeDasharray={e.det ? "0" : "4 3"}
                />
              );
            })}
          </g>

          {/* app nodes */}
          {appsSorted.map((a, i) => {
            const lit = isAppLit(i);
            const o = DATA.OPERATORS[a.owner];
            return (
              <g key={a.id}
                 onMouseEnter={() => setHover({ kind: "app", index: i })}
                 onMouseLeave={() => setHover(null)}
                 onClick={() => onOpenApp(a)}
                 style={{ cursor: "pointer", opacity: lit ? 1 : 0.35, transition: "opacity 0.15s" }}>
                <rect x={20} y={appY(i) - 12} width={appX - 30} height={24} rx={6}
                  fill="var(--paper)" stroke={hover?.kind === "app" && hover.index === i ? "var(--orange)" : "var(--tan-2)"} strokeWidth={hover?.kind === "app" && hover.index === i ? 1.5 : 1}/>
                <circle cx={32} cy={appY(i)} r={4} fill={o.color}/>
                <text x={42} y={appY(i) + 4} fontSize="11.5" fill="var(--ink)" fontWeight="500">{a.name}</text>
                <text x={appX - 14} y={appY(i) + 4} fontSize="10" fill="var(--muted)" textAnchor="end">{(a.connections||[]).length}</text>
              </g>
            );
          })}

          {/* resource nodes */}
          {resources.map((r, i) => {
            const lit = isResourceLit(i);
            const isShared = r.apps.length > 1;
            const isLLM = r.kind === "service" && r.subkind === "LLM";
            const baseFill = r.kind === "schema" ? "var(--cream-3)" : isLLM ? "color-mix(in srgb, var(--orange) 10%, var(--paper))" : "var(--paper)";
            const baseStroke = isShared ? "var(--ink-3)" : "var(--tan-2)";
            const swText = isShared ? 600 : 400;
            return (
              <g key={r.kind + "-" + r.name}
                 onMouseEnter={() => setHover({ kind: "resource", index: i })}
                 onMouseLeave={() => setHover(null)}
                 style={{ cursor: "pointer", opacity: lit ? 1 : 0.35, transition: "opacity 0.15s" }}>
                <rect x={resX + 10} y={resY(i) - 12} width={W - resX - 30} height={24} rx={6}
                  fill={baseFill} stroke={hover?.kind === "resource" && hover.index === i ? "var(--orange)" : baseStroke} strokeWidth={hover?.kind === "resource" && hover.index === i ? 1.5 : (isShared ? 1.2 : 1)}/>
                <text x={resX + 22} y={resY(i) + 4} fontSize="9.5" fill="var(--muted)" letterSpacing="0.06em">{r.kind === "schema" ? "DB" : r.subkind}</text>
                <text x={resX + 56} y={resY(i) + 4} fontSize="11" fill="var(--ink)" fontWeight={swText} fontFamily="var(--mono)">{r.name}</text>
                {/* badges on right */}
                <text x={W - 22} y={resY(i) + 4} fontSize="10" fill="var(--muted)" textAnchor="end" style={{ fontVariantNumeric: "tabular-nums" }}>{r.apps.length}</text>
                {r.kind === "schema" && r.contract === "none" && (
                  <circle cx={W - 44} cy={resY(i)} r={3.5} fill="var(--red)"/>
                )}
                {r.kind === "schema" && r.contract === "partial" && (
                  <circle cx={W - 44} cy={resY(i)} r={3.5} fill="var(--amber)"/>
                )}
                {r.kind === "service" && r.sharedKey && (
                  <circle cx={W - 44} cy={resY(i)} r={3.5} fill="var(--amber)"/>
                )}
              </g>
            );
          })}
        </svg>
      </div>
      <div className="bp-foot">
        <span style={{ color: "var(--muted)", fontSize: 11.5 }}>
          {edges.length} edges · {edges.filter(e => e.shared).length} shared · {edges.filter(e => !e.det).length} non-deterministic
        </span>
        <span style={{ color: "var(--muted)", fontSize: 11.5 }}>Hover to focus · click an app to open detail</span>
      </div>
    </div>
  );
}

// ---------- Governance page ----------
function GovernancePage({ onAddRule, onInspect }) {
  const rules = DATA.RULES;
  const cats = DATA.RULE_CATEGORIES;
  const [expanded, setExpanded] = useState(() => new Set(cats.map(c => c.id)));
  const [query, setQuery] = useState("");

  const totalRules = rules.length;
  const totalOn = rules.filter(r => r.status === "on").length;
  const totalTests = rules.reduce((s, r) => s + r.builtInTests, 0);
  const totalViolations = rules.reduce((s, r) => s + r.violations, 0);

  const byCat = Object.fromEntries(cats.map(c => [c.id, rules.filter(r => r.category === c.id)]));
  const toggle = (id) => setExpanded(prev => {
    const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n;
  });

  return (
    <div className="fade-in">
      <div style={{ marginBottom: 22 }}>
        <div className="eyebrow">Governance · {cats.length} categories · {totalRules} rules · {totalTests} built-in tests</div>
        <h1 className="page-title" style={{ marginTop: 8 }}>Rules that <em>hold.</em></h1>
        <p className="page-sub">
          Every rule runs a bundle of deterministic tests Stoda ships, maintained by us, applied to your fleet. Toggle by category, inspect violations, export signed evidence.
        </p>
      </div>

      {/* Top stats, simple flat row, not Kpi cards */}
      <div className="gov-topline">
        <div><div className="gov-top-label">Rules active</div><div className="gov-top-value">{totalOn} <span className="gov-top-of">/ {totalRules}</span></div></div>
        <div><div className="gov-top-label">Built-in tests</div><div className="gov-top-value">{totalTests}</div></div>
        <div><div className="gov-top-label">Violations</div><div className="gov-top-value">{totalViolations}<span className="gov-top-of"> · auto-routed</span></div></div>
        <div><div className="gov-top-label">Fleet coverage</div><div className="gov-top-value">90<span className="gov-top-of">%</span></div></div>
        <div style={{ marginLeft: "auto", display: "flex", gap: 8, alignItems: "center" }}>
          <div style={{ position: "relative" }}>
            <input className="gov-search" placeholder="Filter rules…" value={query} onChange={e => setQuery(e.target.value)}/>
            <span style={{ position: "absolute", left: 10, top: 8, color: "var(--muted)" }}><Icons.Search s={12}/></span>
          </div>
          <button className="btn sm" onClick={() => onAddRule && onAddRule()}><Icons.Plus s={12}/> New rule</button>
        </div>
      </div>

      {/* Rule categories */}
      <div className="rule-cats">
        {cats.map(cat => {
          const list = byCat[cat.id] || [];
          const filtered = query
            ? list.filter(r => r.name.toLowerCase().includes(query.toLowerCase()) || r.summary.toLowerCase().includes(query.toLowerCase()))
            : list;
          if (query && filtered.length === 0) return null;
          const catOn = list.filter(r => r.status === "on").length;
          const catTests = list.reduce((s,r) => s + r.builtInTests, 0);
          const catViolations = list.reduce((s,r) => s + r.violations, 0);
          const isOpen = expanded.has(cat.id) || !!query;
          const Ic = Icons[cat.icon] || Icons.Shield;

          return (
            <div key={cat.id} className={`rule-cat ${isOpen ? "open" : ""}`}>
              <button className="rule-cat-head" onClick={() => toggle(cat.id)}>
                <span className="rule-cat-icon"><Ic s={15}/></span>
                <div className="rule-cat-title-block">
                  <div className="rule-cat-title">{cat.name}</div>
                  <div className="rule-cat-sub">{cat.description}</div>
                </div>
                <div className="rule-cat-meta">
                  <span className="rule-cat-stat"><b>{catOn}<span style={{ fontFamily: "var(--serif)", fontSize: 18, color: "var(--muted)", fontWeight: 400 }}>/{list.length}</span></b><em>on</em></span>
                  <span className="rule-cat-stat"><b>{catTests}</b><em>tests</em></span>
                  <span className="rule-cat-stat">
                    {catViolations > 0
                      ? <><b style={{ color: "var(--red)" }}>{catViolations}</b><em>violations</em></>
                      : <><b>0</b><em>violations</em></>}
                  </span>
                </div>
                <span className="rule-cat-chev" style={{ transform: isOpen ? "rotate(90deg)" : "rotate(0)" }}>
                  <Icons.Arrow s={12}/>
                </span>
              </button>

              {isOpen && (
                <div className="rule-list">
                  {filtered.map(r => (
                    <div key={r.id} className="rule-row">
                      <div className="rule-row-main">
                        <div className="rule-row-head">
                          <span className="rule-row-name">{r.name}</span>
                          {r.status === "on" && <Pill kind="ok">on</Pill>}
                          {r.status === "partial" && <Pill kind="warn">partial</Pill>}
                          {r.status === "off" && <Pill kind="neutral">off</Pill>}
                        </div>
                        <div className="rule-row-summary">{r.summary}</div>
                        <div className="rule-row-chips">
                          <span className="rule-chip"><Icons.Flask s={11}/> {r.builtInTests} built-in tests</span>
                          <span className="rule-chip">{r.coverage} / 24 apps</span>
                          {r.violations > 0 && <span className="rule-chip violation">{r.violations} violation{r.violations > 1 ? "s" : ""}</span>}
                        </div>
                      </div>
                      <div className="rule-row-actions">
                        <button className="btn sm ghost" onClick={() => onInspect && onInspect(r.id)}>Inspect <Icons.Arrow s={11}/></button>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ---------- Costs page ----------
function CostsPage({ onOpenApp, onOpenService, onOpenBudget }) {
  const budgets = DATA.TEAM_BUDGETS;
  const lineItems = DATA.TOP_LINE_ITEMS;
  const apps = [...DATA.APPS].sort((a,b) => b.cost - a.cost);
  const trend = DATA.COST_TREND_30D;

  const totalBudget = budgets.reduce((s, b) => s + b.budget, 0);
  const totalSpent = budgets.reduce((s, b) => s + b.spent, 0);
  const totalApps = apps.length + 15; // extrapolated for "24 apps"
  const unattributedPool = lineItems.filter(l => l.unattributed).reduce((s,l) => s+l.cost, 0);
  const forecast = Math.round(totalSpent * 30 / 27); // day 27 of month, project to 30
  const pct = Math.min(100, Math.round((totalSpent / totalBudget) * 100));

  const maxTrend = Math.max(...trend);
  const minTrend = Math.min(...trend);
  const maxBudget = Math.max(...budgets.map(b => b.budget));

  return (
    <div className="fade-in">
      <div style={{ marginBottom: 22 }}>
        <div className="eyebrow">Cost attribution · April 2026 · Month-to-date</div>
        <h1 className="page-title" style={{ marginTop: 8 }}>Every dollar <em>has an owner.</em></h1>
        <p className="page-sub">Compute, APIs, and LLM spend, attributed to the team using it. Shared keys surface as an unattributed pool you can split on demand.</p>
      </div>

      {/* Top stats */}
      <div className="cost-topline">
        <div className="cost-top-card">
          <div className="cost-top-label">Month-to-date spend</div>
          <div className="cost-top-value">${(totalSpent/1000).toFixed(1)}k</div>
          <div className="cost-top-foot">of ${(totalBudget/1000).toFixed(1)}k budgeted ({pct}%)</div>
          <div className="cost-bar">
            <div className="cost-bar-fill" style={{ width: `${pct}%` }}/>
          </div>
        </div>
        <div className="cost-top-card">
          <div className="cost-top-label">Forecast · end of month</div>
          <div className="cost-top-value">${(forecast/1000).toFixed(1)}k</div>
          <div className="cost-top-foot pos">on track · ~${((totalBudget-forecast)/1000).toFixed(1)}k under budget</div>
          <div className="cost-bar">
            <div className="cost-bar-fill proj" style={{ width: `${Math.round(forecast/totalBudget*100)}%` }}/>
          </div>
        </div>
        <div className="cost-top-card unattrib">
          <div className="cost-top-label">Unattributed</div>
          <div className="cost-top-value">${unattributedPool.toLocaleString()}</div>
          <div className="cost-top-foot warn">1 shared key · 3 apps</div>
          <button className="btn sm primary" style={{ marginTop: 8 }} onClick={() => {
            const svc = DATA.SERVICES.find(s => s.sharedKey);
            onOpenService && onOpenService(svc);
          }}>Split into per-app keys <Icons.Arrow s={11}/></button>
        </div>
        <div className="cost-top-card">
          <div className="cost-top-label">Cost per user</div>
          <div className="cost-top-value">${(totalSpent / DATA.FLEET_TOTALS.monthlyUsers).toFixed(0)}</div>
          <div className="cost-top-foot">across {DATA.FLEET_TOTALS.monthlyUsers} monthly users</div>
        </div>
      </div>

      {/* 30-day trend */}
      <div className="card" style={{ marginBottom: 20 }}>
        <div className="card-head">
          <div>
            <h3>30-day spend</h3>
            <div className="subtitle">Daily cost across the fleet. Dashed line = projected.</div>
          </div>
          <div style={{ display: "flex", gap: 12, fontSize: 12, color: "var(--muted)" }}>
            <span><span className="sw" style={{ background: "var(--ink-2)" }}/> actual</span>
            <span><span className="sw" style={{ background: "var(--accent)" }}/> projected</span>
            <span><span className="sw" style={{ background: "var(--green)", opacity: 0.4 }}/> daily budget</span>
          </div>
        </div>
        <CostTrendChart values={trend} dailyBudget={totalBudget/30}/>
      </div>

      {/* Team budgets */}
      <div className="card" style={{ marginBottom: 20 }}>
        <div className="card-head">
          <div>
            <h3>By team</h3>
            <div className="subtitle">Budget vs. month-to-date spend. Click to open the team's apps.</div>
          </div>
        </div>
        <div className="budget-list">
          {budgets.map(b => {
            const used = Math.round((b.spent / b.budget) * 100);
            const over = used > 100;
            return (
              <div key={b.team} className="budget-row">
                <div className="budget-row-head">
                  <div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 220 }}>
                    <span className="budget-swatch" style={{ background: b.color }}/>
                    <div>
                      <div style={{ fontWeight: 500, color: "var(--ink)", fontSize: 13.5 }}>{b.team}</div>
                      <div style={{ fontSize: 11.5, color: "var(--muted)" }}>{b.apps} app{b.apps > 1 ? "s" : ""} · owned by {DATA.OPERATORS[b.owner].name}</div>
                    </div>
                  </div>
                  <div className="budget-bar-wrap">
                    <div className="budget-bar">
                      <div className="budget-bar-fill" style={{ width: `${Math.min(100, used)}%`, background: b.color }}/>
                      <div className="budget-bar-budget" style={{ left: `${Math.min(100, (b.budget/maxBudget)*100)}%` }}/>
                    </div>
                  </div>
                  <div style={{ display: "flex", gap: 16, fontVariantNumeric: "tabular-nums", minWidth: 200, justifyContent: "flex-end" }}>
                    <div style={{ textAlign: "right" }}>
                      <div style={{ fontSize: 13, color: "var(--ink)", fontWeight: 500 }}>${b.spent.toLocaleString()}</div>
                      <div style={{ fontSize: 11, color: "var(--muted)" }}>of ${b.budget.toLocaleString()}</div>
                    </div>
                    <div style={{ textAlign: "right", minWidth: 44 }}>
                      <div style={{ fontSize: 13, color: over ? "var(--red)" : "var(--ink-3)", fontWeight: 500 }}>{used}%</div>
                      <div style={{ fontSize: 11, color: "var(--muted)" }}>used</div>
                    </div>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Side-by-side: by app + top line items */}
      <div className="grid g-12" style={{ gap: 16, marginBottom: 20 }}>
        <div className="col-7 card flush">
          <div className="card-head" style={{ padding: "16px 20px 14px", marginBottom: 0 }}>
            <div>
              <h3>By app</h3>
              <div className="subtitle">Compute / APIs / LLM breakdown per app.</div>
            </div>
          </div>
          <table className="t">
            <thead>
              <tr><th>App</th><th>Owner</th><th>Compute</th><th>APIs</th><th>LLM</th><th>Total</th></tr>
            </thead>
            <tbody>
              {apps.map(a => {
                const o = DATA.OPERATORS[a.owner];
                const cb = a.costBreakdown || { compute: Math.round(a.cost * 0.2), apis: Math.round(a.cost * 0.45), llm: Math.round(a.cost * 0.35) };
                return (
                  <tr key={a.id} onClick={() => onOpenBudget ? onOpenBudget(a) : onOpenApp(a)} style={{ cursor: "pointer" }}>
                    <td style={{ fontWeight: 500, color: "var(--ink)" }}>{a.name}</td>
                    <td>
                      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                        <span className="avatar xs" style={{ background: o.color }}>{o.initials}</span>
                        <span style={{ fontSize: 12, color: "var(--muted)" }}>{o.team}</span>
                      </div>
                    </td>
                    <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink-3)" }}>${cb.compute}</td>
                    <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink-3)" }}>${cb.apis}</td>
                    <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink-3)" }}>${cb.llm}</td>
                    <td style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink)", fontWeight: 500 }}>${a.cost.toLocaleString()}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        <div className="col-5 card flush">
          <div className="card-head" style={{ padding: "16px 20px 14px", marginBottom: 0 }}>
            <div>
              <h3>Top line items</h3>
              <div className="subtitle">Services by monthly spend.</div>
            </div>
          </div>
          <div className="line-items">
            {lineItems.slice(0, 8).map((l, i) => {
              const svc = DATA.SERVICES.find(s => s.name === l.name);
              return (
              <div key={i} className="line-item" style={{ cursor: svc ? "pointer" : "default" }} onClick={() => svc && onOpenService && onOpenService(svc)}>
                <div className="line-item-main">
                  <div className="line-item-head">
                    <span className="line-item-kind" data-kind={l.kind.toLowerCase()}>{l.kind}</span>
                    <span className="line-item-name">{l.name}</span>
                    {l.unattributed && <span className="tag warn" title="Shared key, costs not attributed">shared key</span>}
                  </div>
                  <div className="line-item-sub">
                    {l.apps} app{l.apps > 1 ? "s" : ""} · {l.trend}
                  </div>
                </div>
                <div className="line-item-cost">${l.cost.toLocaleString()}</div>
              </div>
              );
            })}
          </div>
        </div>
      </div>

      {/* Attribution quality */}
      <div className="card attrib-card">
        <div style={{ display: "flex", gap: 18, alignItems: "flex-start" }}>
          <span style={{ width: 40, height: 40, borderRadius: 10, background: "var(--amber-bg)", border: "1px solid var(--amber-border)", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--amber)" }}>
            <Icons.Alert/>
          </span>
          <div style={{ flex: 1 }}>
            <h3 style={{ fontFamily: "var(--serif)", fontSize: 22, fontWeight: 400, letterSpacing: "-0.02em" }}>${unattributedPool.toLocaleString()} sits in a shared OPENAI_API_KEY</h3>
            <p style={{ fontSize: 13.5, color: "var(--muted)", marginTop: 6, maxWidth: "72ch" }}>
              Quote Copilot, Churn Early Warning, and NPS Pulse all bill against one key. Splitting into per-app keys restores team-level cost attribution and limits blast radius if a key leaks.
            </p>
            <div style={{ display: "flex", gap: 8, marginTop: 14 }}>
              <button className="btn primary sm" onClick={() => {
                const svc = DATA.SERVICES.find(s => s.sharedKey);
                onOpenService && onOpenService(svc);
              }}>Split keys <Icons.Arrow s={11}/></button>
              <button className="btn sm">Dismiss</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function CostTrendChart({ values, dailyBudget }) {
  const W = 1000, H = 220;
  const pad = { t: 20, r: 20, b: 30, l: 48 };
  const iw = W - pad.l - pad.r;
  const ih = H - pad.t - pad.b;
  const actualCount = 27;
  const projCount = values.length - actualCount;
  const max = Math.max(...values, dailyBudget) * 1.15;
  const x = (i) => pad.l + (i / (values.length - 1)) * iw;
  const y = (v) => pad.t + ih - (v / max) * ih;

  const actualPath = values.slice(0, actualCount).map((v, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(v)}`).join(" ");
  const projPath = `M${x(actualCount - 1)},${y(values[actualCount - 1])} ` + values.slice(actualCount).map((v, i) => `L${x(actualCount + i)},${y(v)}`).join(" ");
  const budgetY = y(dailyBudget);

  return (
    <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ width: "100%", height: H, display: "block" }}>
      {/* grid lines */}
      {[0.25, 0.5, 0.75, 1].map((g, i) => (
        <line key={i} x1={pad.l} x2={W-pad.r} y1={pad.t + ih - g*ih} y2={pad.t + ih - g*ih} stroke="var(--tan-2)" strokeOpacity="0.6" strokeWidth="1"/>
      ))}
      {/* y labels */}
      {[0.25, 0.5, 0.75, 1].map((g, i) => (
        <text key={i} x={pad.l - 8} y={pad.t + ih - g*ih + 4} fontSize="10" textAnchor="end" fill="var(--muted)" style={{ fontVariantNumeric: "tabular-nums" }}>${Math.round(max*g)}</text>
      ))}
      {/* budget line */}
      <line x1={pad.l} x2={W-pad.r} y1={budgetY} y2={budgetY} stroke="var(--green)" strokeOpacity="0.5" strokeWidth="1" strokeDasharray="2 3"/>
      <text x={W-pad.r} y={budgetY - 5} fontSize="10" textAnchor="end" fill="var(--green)">daily budget ${Math.round(dailyBudget)}</text>
      {/* actual fill */}
      <path d={`${actualPath} L${x(actualCount - 1)},${pad.t + ih} L${x(0)},${pad.t + ih} Z`} fill="var(--ink-2)" fillOpacity="0.08"/>
      {/* actual line */}
      <path d={actualPath} fill="none" stroke="var(--ink-2)" strokeWidth="1.8"/>
      {/* projected line */}
      <path d={projPath} fill="none" stroke="var(--accent)" strokeWidth="1.8" strokeDasharray="3 3"/>
      {/* day labels */}
      {[0, 7, 14, 21, 29].map(d => (
        <text key={d} x={x(d)} y={H - 10} fontSize="10" textAnchor="middle" fill="var(--muted)">{d === 29 ? "today" : `d${d+1}`}</text>
      ))}
      {/* today marker */}
      <line x1={x(actualCount-1)} x2={x(actualCount-1)} y1={pad.t} y2={pad.t+ih} stroke="var(--accent)" strokeOpacity="0.4" strokeWidth="1" strokeDasharray="2 2"/>
    </svg>
  );
}

// ---------- Recommendations page ----------
function RecommendationsPage({ onOpenApp, onOpenRec }) {
  const all = DATA.SIGNALS;
  const sevRank = { risk: 0, warn: 1, info: 2, ok: 3 };
  const sorted = [...all].sort((a, b) => (sevRank[a.sev] ?? 9) - (sevRank[b.sev] ?? 9));
  return (
    <div className="fade-in">
      <div style={{ marginBottom: 22 }}>
        <div className="eyebrow">Recommendations · Decisions awaiting you</div>
        <h1 className="page-title" style={{ marginTop: 8 }}>Calls only <em>you</em> can make.</h1>
        <p className="page-sub">Each one is a moment where Stoda needs a human decision, exception, allowlist, budget, or rule.</p>
      </div>

      <div className="signals-list">
        {sorted.map(s => <SignalItem key={s.id} item={s} onOpen={onOpenRec}/>)}
      </div>
    </div>
  );
}

// ---------- Coming soon scaffold ----------
function ComingSoonPage({ title, eyebrow, description, items, icon }) {
  const Ic = Icons[icon || "Clock"];
  return (
    <div className="fade-in">
      <div style={{ marginBottom: 28 }}>
        <div className="eyebrow">{eyebrow}</div>
        <h1 className="page-title" style={{ marginTop: 8 }}>{title}</h1>
        <p className="page-sub">{description}</p>
      </div>
      <div className="card" style={{ background: "var(--cream-2)", padding: 40, textAlign: "center" }}>
        <div style={{ width: 56, height: 56, borderRadius: 14, background: "var(--paper)", border: "1px solid var(--tan-2)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--ink)", marginBottom: 18 }}>
          <Ic s={24}/>
        </div>
        <div style={{ fontFamily: "var(--serif)", fontSize: 26, letterSpacing: "-0.02em", color: "var(--ink)" }}>Coming soon</div>
        <p style={{ color: "var(--muted)", fontSize: 14, marginTop: 6, maxWidth: "52ch", margin: "6px auto 0" }}>
          This surface is in private beta. A preview of what will live here:
        </p>
        <div style={{ display: "flex", gap: 10, justifyContent: "center", flexWrap: "wrap", marginTop: 20, maxWidth: 700, marginLeft: "auto", marginRight: "auto" }}>
          {items.map((it, i) => (
            <span key={i} style={{ padding: "8px 14px", borderRadius: 999, background: "var(--paper)", border: "1px solid var(--tan-2)", fontSize: 13, color: "var(--ink-3)" }}>
              {it}
            </span>
          ))}
        </div>
        <button className="btn primary sm" style={{ marginTop: 22 }}>Request early access <Icons.Arrow s={12}/></button>
      </div>
    </div>
  );
}

Object.assign(window, {
  SignalItem, RecommendationCard,
  OverviewPage, FleetPage, ConnectionsPage, GovernancePage, RecommendationsPage, CostsPage, ComingSoonPage,
});
