// Entity drawer + Observability (Retrievals/Usage) + Home

const { useState: useGx } = React;

// Entity drawer — resolves the clicked id against the live /skill/graph
// snapshot and renders the REAL node (aliases, properties, columns,
// connections, raw DTO). An id the snapshot doesn't contain gets a minimal
// "not in the graph" card; there is no fixture to fall back to.
const ENTITY_ICON_BY_TYPE = { table: 'Table', column: 'Table', document: 'Doc', chunk: 'Doc', concept: 'Sparkle' };
const EDGE_VERB = { mapping: 'maps to', dependency: 'depends on', relation: 'related', category: 'category', structure: 'links' };

function DrawerShell({ onClose, children }) {
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,.25)', zIndex: 90, display: 'flex', justifyContent: 'flex-end' }}>
      <div onClick={e => e.stopPropagation()} style={{
        width: 640, height: '100%', background: 'var(--surface)',
        borderLeft: '1px solid var(--border)', boxShadow: 'var(--shadow-1)',
        display: 'flex', flexDirection: 'column',
        animation: 'slideInR 240ms cubic-bezier(.2,.8,.2,1)'
      }}>
        {children}
      </div>
    </div>
  );
}

function EntityDrawer({ entity, onClose }) {
  const [snap, setSnap] = useGx(null);
  const [loadErr, setLoadErr] = useGx(false);
  React.useEffect(() => {
    if (typeof fetchGraphSnapshotCached !== 'function') { setLoadErr(true); return; }
    let cancelled = false;
    fetchGraphSnapshotCached().then(s => { if (!cancelled) setSnap(s); }).catch(() => { if (!cancelled) setLoadErr(true); });
    const unsub = typeof subscribeGraphSnapshot === 'function'
      ? subscribeGraphSnapshot(s => { if (!cancelled) setSnap(s); })
      : null;
    return () => { cancelled = true; if (unsub) unsub(); };
  }, []);

  const resolved = React.useMemo(() => {
    if (!entity || !entity.id || !snap) return null;
    const nodes = allGraphNodes(snap);
    const idx = buildGraphNodeIndex(nodes);
    const node = idx.get(entity.id);
    if (!node) return null;
    const { outByNode, inByNode } = buildGraphAdjacency(allGraphEdges(snap));
    return { node, idx, outE: outByNode.get(node.id) || [], inE: inByNode.get(node.id) || [] };
  }, [entity, snap]);

  if (!entity) return null;

  // Resolved to a real knowledge-graph node → show its live data.
  if (resolved) {
    return <DrawerShell onClose={onClose}><GraphEntityBody resolved={resolved} onClose={onClose}/></DrawerShell>;
  }
  // Has a graph-style id and the snapshot is still loading → wait (not failed).
  if (entity.id && !snap && !loadErr) {
    return <DrawerShell onClose={onClose}><UnknownEntityBody entity={entity} loading={true} onClose={onClose}/></DrawerShell>;
  }
  // The snapshot FAILED to load, so we cannot know whether this id is a graph
  // node. Say the graph is unreachable rather than guessing at the entity.
  if (loadErr) {
    return <DrawerShell onClose={onClose}><UnknownEntityBody entity={entity} loading={false} failed={true} onClose={onClose}/></DrawerShell>;
  }
  // Snapshot loaded fine and the id simply isn't in it. There is no fallback
  // dataset to fill the gap — every entity this console shows comes from the
  // graph — so report the miss instead of inventing sources for it.
  return <DrawerShell onClose={onClose}><UnknownEntityBody entity={entity} loading={false} onClose={onClose}/></DrawerShell>;
}

function GraphEntityBody({ resolved, onClose }) {
  const { node, idx, outE, inE } = resolved;
  const props = node.properties || {};
  const isConcept = node.category === 'concept';
  const typeLabel = node.type || node.category;
  const I = Icons[ENTITY_ICON_BY_TYPE[node.type] || ENTITY_ICON_BY_TYPE[node.category] || 'Box'] || Icons.Box;
  const synonyms = (node.synonyms || []).filter(s => s && s.state !== 'undo');
  const columnRows = tableColumnsList(props);
  const columns = columnRows.length > 0 ? columnRows : null;
  const created = (typeof formatRelativeTime === 'function' && node.created_at) ? formatRelativeTime(node.created_at) : node.created_at;

  const neighbors = [
    ...outE.map(e => ({ e, other: idx.get(e.target) })),
    ...inE.map(e => ({ e, other: idx.get(e.source) })),
  ].filter(x => x.other && x.other.state !== 'undo');

  return (
    <>
      <div style={{ padding: '20px 24px', borderBottom: '1px solid var(--border-subtle)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span className="chip">{typeLabel}</span>
          {node.status === 'merged' && <span className="chip" style={{ color: 'var(--text-tertiary)' }}>merged</span>}
          {node.state === 'undo' && <span className="chip" style={{ color: 'var(--error)' }}>undone</span>}
          <div style={{ flex: 1 }}/>
          <button className="btn sm ghost" onClick={onClose}><Icons.X size={14}/></button>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 12 }}>
          <span style={{
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            width: 40, height: 40, borderRadius: 10, flexShrink: 0,
            background: 'var(--accent-muted)', color: 'var(--accent-text)',
          }}><I size={20}/></span>
          <div style={{ minWidth: 0 }}>
            <div className="t-h2" style={{ margin: 0 }}>{node.name}</div>
            <div className="mono-sm mono text-tertiary" style={{ marginTop: 2 }}>
              {node.id}{node.origin ? `  ·  ${node.origin}` : ''}{created ? `  ·  ${created}` : ''}
            </div>
          </div>
        </div>
      </div>

      <div style={{ flex: 1, overflowY: 'auto', padding: 24 }}>
        {props.description && (
          <div style={{ marginBottom: 20 }}>
            <div className="t-h3">Description</div>
            <div className="t-body text-secondary" style={{ marginTop: 6 }}>{props.description}</div>
          </div>
        )}
        {props.formula && (
          <div style={{ marginBottom: 20 }}>
            <div className="t-h3">Formula</div>
            <div className="mono-sm mono" style={{ marginTop: 6, background: 'var(--bg)', border: '1px solid var(--border-subtle)', borderRadius: 8, padding: '8px 10px' }}>{props.formula}</div>
          </div>
        )}

        {isConcept && (
          <div style={{ marginBottom: 20 }}>
            <div className="t-h3">Aliases <span className="mono-sm mono text-tertiary">· {synonyms.length}</span></div>
            <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
              {synonyms.length === 0
                ? <span className="t-small text-tertiary">No synonyms recorded.</span>
                : synonyms.map((s, i) => (
                    <span key={i} className="chip" title={`${s.origin || ''}${s.at ? ' · ' + s.at : ''}`}>{s.name}</span>
                  ))}
            </div>
          </div>
        )}

        {columns && (
          <div style={{ marginBottom: 20 }}>
            <div className="t-h3">Columns <span className="mono-sm mono text-tertiary">· {columns.length}</span></div>
            <div style={{ marginTop: 8, fontSize: 13 }}>
              {columns.slice(0, 60).map((c, i) => (
                <div key={i} style={{ display: 'flex', gap: 10, padding: '6px 0', borderBottom: '1px solid var(--border-subtle)' }}>
                  <span style={{ fontWeight: 500, minWidth: 0 }}>{c.name}</span>
                  {c.data_type && <span className="mono-sm mono text-tertiary">{c.data_type}</span>}
                  <div style={{ flex: 1 }}/>
                  {c.description && <span className="t-small text-secondary" style={{ maxWidth: 280, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.description}</span>}
                </div>
              ))}
              {columns.length > 60 && <div className="t-small text-tertiary" style={{ paddingTop: 8 }}>+{columns.length - 60} more columns</div>}
            </div>
          </div>
        )}

        <div style={{ marginBottom: 20 }}>
          <div className="t-h3">Connections <span className="mono-sm mono text-tertiary">· {neighbors.length}</span></div>
          <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
            {neighbors.length === 0
              ? <span className="t-small text-tertiary">No connected nodes.</span>
              : neighbors.slice(0, 50).map(({ e, other }, i) => (
                  <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 10px', border: '1px solid var(--border-subtle)', borderRadius: 7 }}>
                    <span className="t-micro text-tertiary" style={{ minWidth: 64 }}>{e.relation || EDGE_VERB[e.type] || e.type}</span>
                    <span style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{other.name}</span>
                    {e.value && <span className="mono-sm mono text-tertiary">= {e.value}</span>}
                    <div style={{ flex: 1 }}/>
                    {typeof e.confidence === 'number' && <span className="mono-sm mono text-tertiary">{Math.round(e.confidence * 100)}%</span>}
                  </div>
                ))}
            {neighbors.length > 50 && <span className="t-small text-tertiary">+{neighbors.length - 50} more</span>}
          </div>
        </div>

        <div>
          <div className="t-h3">Raw node</div>
          <div style={{ marginTop: 8 }}>
            <CodeBlock language="json">{JSON.stringify(node, null, 2)}</CodeBlock>
          </div>
        </div>
      </div>
    </>
  );
}

function UnknownEntityBody({ entity, loading, failed, onClose }) {
  const name = entity.name || entity.label || entity.id || 'Entity';
  const type = entity.type || entity.sub || 'Entity';
  return (
    <>
      <div style={{ padding: '20px 24px', borderBottom: '1px solid var(--border-subtle)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <span className="chip">{type}</span>
          <div style={{ flex: 1 }}/>
          <button className="btn sm ghost" onClick={onClose}><Icons.X size={14}/></button>
        </div>
        <div className="t-h1" style={{ marginTop: 10 }}>{name}</div>
        {entity.sub && <div className="text-secondary t-small mt-4">{entity.sub}</div>}
      </div>
      <div style={{ flex: 1, overflowY: 'auto', padding: 24 }}>
        <div className="t-small text-tertiary">
          {loading ? 'Loading graph…'
            : failed ? "Couldn't load the knowledge graph, so this entity can't be resolved. Try again once the backend is reachable."
            : 'This entity is not in the current knowledge-graph snapshot.'}
        </div>
      </div>
    </>
  );
}
function KV2({ k, v }) {
  return <div style={{ display: 'flex', padding: '6px 0', borderBottom: '1px solid var(--border-subtle)' }}>
    <span className="text-tertiary" style={{ width: 120 }}>{k}</span>
    <span>{v}</span>
  </div>;
}

// Observability page with Retrievals/Usage tabs
function ObservabilityPage({ openDetail }) {
  const [tab, setTab] = useGx('retrievals');
  return (
    <div style={{ padding: '32px 32px 80px', maxWidth: 1280, margin: '0 auto' }}>
      <div>
        <h1 className="t-h1" style={{ margin: 0 }}>Observability</h1>
        <div className="text-secondary t-body mt-4">Retrieval audit trail and usage.</div>
      </div>
      <div style={{ marginTop: 24, display: 'flex', borderBottom: '1px solid var(--border-subtle)' }}>
        {[['retrievals','Retrievals'],['usage','Usage']].map(([id, label]) => (
          <button key={id} onClick={() => setTab(id)} style={{
            padding: '10px 16px', marginBottom: -1,
            borderBottom: `2px solid ${tab === id ? 'var(--text-primary)' : 'transparent'}`,
            fontSize: 13, fontWeight: tab === id ? 500 : 400,
            color: tab === id ? 'var(--text-primary)' : 'var(--text-secondary)',
          }}>{label}</button>
        ))}
      </div>
      <div style={{ marginTop: 24 }}>
        {tab === 'retrievals' && <RetrievalsTab openDetail={openDetail}/>}
        {tab === 'usage' && <UsageTab/>}
      </div>
    </div>
  );
}

// success/no_data/clarify/failure (backend statuses) → StatusGlyph keys.
function rtvStatusKey(status) {
  if (status === 'success') return 'ok';
  if (status === 'no_data') return 'noresult';
  if (status === 'failure') return 'error';
  return status; // 'clarify' has its own glyph branch
}

function rtvLatency(ms) {
  if (ms == null) return '—';
  return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
}

function rtvTime(iso) {
  const d = new Date(iso);
  if (isNaN(d.getTime())) return '—';
  const hm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
  const sameDay = d.toDateString() === new Date().toDateString();
  return sameDay ? hm : `${d.getMonth() + 1}/${d.getDate()} ${hm}`;
}

const RTV_PAGE_SIZE = 20;

// Real audit trail from GET /skill/retrievals — the only mode; there is no mock.
function RetrievalsTab({ openDetail }) {
  const [page, setPage] = useGx(1);
  const [data, setData] = useGx(null);      // last good list response
  const [loading, setLoading] = useGx(true);
  const [error, setError] = useGx(null);
  const [q, setQ] = useGx('');

  React.useEffect(() => {
    let alive = true;
    setLoading(true);
    ParcleRetrievalsAPI.list(page, RTV_PAGE_SIZE)
      .then(r => { if (!alive) return; setData(r); setError(null); })
      .catch(e => { if (alive) setError((e && e.message) || 'request failed'); })
      .finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, [page]);

  const needle = q.trim().toLowerCase();
  const rows = (data ? data.retrievals : []).filter(r => !needle
    || (r.query || '').toLowerCase().includes(needle)
    || (r.answer || '').toLowerCase().includes(needle));
  const totalPages = data ? data.total_pages : 0;

  return (
    <>
      <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
        <div style={{ position: 'relative', flex: 1, maxWidth: 380 }}>
          <Icons.Search size={13} style={{ position: 'absolute', left: 12, top: 10, color: 'var(--text-tertiary)' }}/>
          <input className="input" placeholder="Filter this page by query or answer…" style={{ paddingLeft: 34 }}
            value={q} onChange={e => setQ(e.target.value)}/>
        </div>
        <div style={{ flex: 1 }}/>
        <span className="mono-sm mono text-tertiary">
          {data ? `${data.total} ask${data.total === 1 ? '' : 's'}` : (loading ? 'Loading…' : '')}
        </span>
      </div>

      {error && !data && (
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="t-small" style={{ color: 'var(--error)' }}>Couldn't load retrievals — {error}</div>
        </div>
      )}
      {error && data && (
        <div className="t-small" style={{ marginTop: 12, color: 'var(--error)' }}>
          Couldn't refresh — {error}. Showing the last loaded page.
        </div>
      )}
      {data && data.total === 0 && (
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="t-small text-secondary">
            No retrievals yet. Ask something in Chat — or through the ask-parcle skill — and the
            full audit trail shows up here.
          </div>
        </div>
      )}
      {data && data.total > 0 && rows.length === 0 && (
        <div className="card" style={{ marginTop: 16, padding: 24 }}>
          <div className="t-small text-secondary">
            {needle
              ? `No match for “${q.trim()}” on this page.`
              : 'Nothing to show on this page — these records predate the current audit format.'}
          </div>
        </div>
      )}

      {rows.length > 0 && (
        <div className="card" style={{ marginTop: 16, overflow: 'hidden' }}>
          <table className="parcle">
            <thead>
              <tr>
                <th style={{ width: 40 }}></th>
                <th>Query</th>
                <th style={{ width: 220 }}>Sources</th>
                <th style={{ width: 80 }}>Conf.</th>
                <th style={{ width: 90 }}>Latency</th>
                <th style={{ width: 110 }}>Time</th>
              </tr>
            </thead>
            <tbody>
              {rows.map(r => (
                <tr key={r.turn_id} onClick={() => openDetail(r)}>
                  <td><StatusGlyph status={rtvStatusKey(r.status)}/></td>
                  <td><span style={{ fontSize: 13 }}>{r.query}</span></td>
                  <td>
                    {r.sources && r.sources.length > 0 ? (
                      <div style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
                        {r.sources.slice(0, 2).map((s, i) => (
                          <span key={i} className="chip" style={{ fontSize: 10, height: 20 }}>{s.name || s.id}</span>
                        ))}
                        {r.sources.length > 2 && <span className="mono-sm mono text-tertiary">+{r.sources.length - 2}</span>}
                      </div>
                    ) : <span className="mono-sm mono text-tertiary">—</span>}
                  </td>
                  <td><span className="mono-sm mono text-secondary">{r.confidence != null ? r.confidence.toFixed(2) : '—'}</span></td>
                  <td><span className="mono-sm mono text-secondary">{rtvLatency(r.latency_ms)}</span></td>
                  <td><span className="mono-sm mono text-tertiary">{rtvTime(r.created_at)}</span></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {totalPages > 1 && (
        <div style={{ display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'flex-end', marginTop: 12 }}>
          <button className="btn sm" disabled={page <= 1 || loading} onClick={() => setPage(page - 1)}>← Prev</button>
          <span className="mono-sm mono text-tertiary">Page {page} of {totalPages}</span>
          <button className="btn sm" disabled={page >= totalPages || loading} onClick={() => setPage(page + 1)}>Next →</button>
        </div>
      )}
    </>
  );
}

// Real per-account aggregates from GET /skill/retrievals/stats — no mock mode.
function UsageTab() {
  const [stats, setStats] = useGx(null);
  const [loading, setLoading] = useGx(true);
  const [error, setError] = useGx(null);

  React.useEffect(() => {
    let alive = true;
    setLoading(true);
    ParcleRetrievalsAPI.stats(30)
      .then(r => { if (!alive) return; setStats(r); setError(null); })
      .catch(e => { if (alive) setError((e && e.message) || 'request failed'); })
      .finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, []);

  if (error && !stats) {
    return (
      <div className="card" style={{ padding: 24 }}>
        <div className="t-small" style={{ color: 'var(--error)' }}>Couldn't load usage — {error}</div>
      </div>
    );
  }
  if (!stats) return <div className="t-small text-tertiary" style={{ padding: 24 }}>{loading ? 'Loading…' : ''}</div>;
  if (stats.total === 0) {
    return (
      <div className="card" style={{ padding: 24 }}>
        <div className="t-small text-secondary">
          No asks in the last {stats.days} days. Ask something in Chat — or through the
          ask-parcle skill — and usage shows up here.
        </div>
      </div>
    );
  }
  return <UsageStats stats={stats}/>;
}

const USAGE_STATUS_ROWS = [
  ['success', 'Answered', 'var(--success)'],
  ['clarify', 'Needs clarification', 'var(--warning)'],
  ['no_data', 'No data found', 'var(--text-tertiary)'],
  ['failure', 'Failed', 'var(--error)'],
];

function UsageStats({ stats }) {
  const spark = stats.daily.map(d => d.count);
  const classified = USAGE_STATUS_ROWS.reduce((s, [k]) => s + (stats.by_status[k] || 0), 0);
  const successRate = classified ? Math.round((stats.by_status.success || 0) * 100 / classified) : null;
  return (
    <div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
        <UsageMetricCard label={`Asks · last ${stats.days} days`} value={stats.total.toLocaleString()} spark={spark}
          sub="all knowledge-service asks for this org"/>
        <UsageMetricCard label="Answered" value={successRate == null ? '—' : `${successRate}%`}
          sub={classified ? `${(stats.by_status.success || 0).toLocaleString()} of ${classified.toLocaleString()} asks` : 'no classified asks yet'}/>
        <UsageMetricCard label="Latency · avg / p50"
          value={classified ? `${rtvLatency(stats.latency_avg_ms)} · ${rtvLatency(stats.latency_p50_ms)}` : '—'}
          sub="per ask, LLM + tool time" subtle/>
      </div>

      <div className="card" style={{ padding: 20, marginTop: 24, maxWidth: 560 }}>
        <div className="t-h3" style={{ marginBottom: 12 }}>By outcome</div>
        {USAGE_STATUS_ROWS.map(([key, label, color]) => {
          const n = stats.by_status[key] || 0;
          const pct = classified ? Math.round(n * 100 / classified) : 0;
          return (
            <div key={key} style={{ marginBottom: 10 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
                <span className="t-small" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  <span style={{ width: 7, height: 7, borderRadius: '50%', background: color, display: 'inline-block' }}/>
                  {label}
                </span>
                <span className="mono-sm mono text-secondary">{n.toLocaleString()} · {pct}%</span>
              </div>
              <Progress value={pct} height={3}/>
            </div>
          );
        })}
        {stats.total > classified && (
          <div className="t-small text-tertiary" style={{ marginTop: 12 }}>
            {(stats.total - classified).toLocaleString()} older ask{stats.total - classified === 1 ? '' : 's'} predate the current audit format and aren't classified.
          </div>
        )}
      </div>
    </div>
  );
}

// Named UsageMetricCard (not MetricCard): every top-level function here is a
// global under babel-standalone, and chat_inspector.jsx already owns MetricCard.
function UsageMetricCard({ label, value, spark, delta, sub, subtle }) {
  return (
    <div className="card" style={{ padding: 20 }}>
      <div className="mono-sm mono text-tertiary">{label}</div>
      <div className="t-display" style={{ marginTop: 6 }}>{value}</div>
      {(delta || sub) && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
          {delta && <span className="mono-sm mono" style={{ color: subtle ? 'var(--text-secondary)' : 'var(--success)' }}>{delta}</span>}
          {sub && <span className="mono-sm mono text-tertiary">{sub}</span>}
        </div>
      )}
      {spark && spark.length > 1 && (
        <div style={{ marginTop: 14 }}>
          <Sparkline data={spark} filled/>
        </div>
      )}
    </div>
  );
}

// Home — every figure is read from the backend:
//
//   the graph snapshot (GET /skill/graph, shared app-wide cache) → what the org
//     knows and what it learned in the last 24h
//   GET /skill/retrievals/stats?days=30 → ask volume and latency
//   GET /skill/retrievals?page=1&limit=5 → the newest asks
//
// There is no demo dataset behind this page. A block whose source hasn't
// answered yet shows '—' / a skeleton, and a block whose source failed says so
// — neither is filled in with a plausible-looking number.

const HOME_RECENT_LIMIT = 5;
const HOME_STATS_DAYS = 30;
const HOME_CLOCK_MS = 60000;
// The retrieval figures are re-read on this cadence (and immediately whenever
// the tab comes back to the foreground). Without it "N today (UTC)" would keep
// reporting yesterday's count after UTC midnight, next to a header clock that
// does tick — and an ask made in Chat would not show up until a route change.
const HOME_REFRESH_MS = 300000;

function HomePage({ setRoute }) {
  const [snapshot, setSnapshot] = useGx(null);
  const [snapError, setSnapError] = useGx(null);
  const [stats, setStats] = useGx(null);
  const [statsError, setStatsError] = useGx(null);
  const [recent, setRecent] = useGx(null);
  const [recentError, setRecentError] = useGx(null);
  const [now, setNow] = useGx(() => new Date());

  // The header prints a wall clock, so it has to keep moving — a timestamp
  // frozen at mount is wrong for every minute the tab stays open. The same tick
  // re-evaluates the rolling "last 24 hours" digest below.
  React.useEffect(() => {
    const t = setInterval(() => setNow(new Date()), HOME_CLOCK_MS);
    return () => clearInterval(t);
  }, []);

  // Graph snapshot — served from the app-wide cache primed at mount, and kept
  // current by the same subscription Fabric / Learning use.
  React.useEffect(() => {
    let cancelled = false;
    fetchGraphSnapshotCached()
      .then(s => { if (!cancelled) { setSnapshot(s); setSnapError(null); } })
      .catch(e => { if (!cancelled) setSnapError((e && e.message) || 'request failed'); });
    const unsub = typeof subscribeGraphSnapshot === 'function'
      ? subscribeGraphSnapshot(s => { if (!cancelled) setSnapshot(s); })
      : null;
    return () => { cancelled = true; if (unsub) unsub(); };
  }, []);

  // Retrieval stats + the recent list, refreshed on a slow cadence and on
  // foreground return. A failed read keeps the last good value on screen and
  // reports itself; it never falls back to invented numbers.
  React.useEffect(() => {
    let alive = true;
    const load = () => {
      ParcleRetrievalsAPI.stats(HOME_STATS_DAYS)
        .then(r => { if (alive) { setStats(r); setStatsError(null); } })
        .catch(e => { if (alive) setStatsError((e && e.message) || 'request failed'); });
      ParcleRetrievalsAPI.list(1, HOME_RECENT_LIMIT)
        .then(r => { if (alive) { setRecent(r); setRecentError(null); } })
        .catch(e => { if (alive) setRecentError((e && e.message) || 'request failed'); });
    };
    load();
    let last = Date.now();
    const tick = () => {
      if (document.hidden) return;      // don't poll a page nobody is looking at
      const t = Date.now();
      if (t - last < HOME_REFRESH_MS) return;
      last = t;
      load();
    };
    const onVisible = () => { if (!document.hidden) { last = Date.now(); load(); } };
    const timer = setInterval(tick, HOME_CLOCK_MS);
    document.addEventListener('visibilitychange', onVisible);
    return () => {
      alive = false;
      clearInterval(timer);
      document.removeEventListener('visibilitychange', onVisible);
    };
  }, []);

  const events = React.useMemo(
    () => (snapshot ? deriveLearningEvents(snapshot) : []),
    [snapshot]
  );
  const graphStats = React.useMemo(
    () => deriveFabricStats(snapshot, events),
    [snapshot, events]
  );
  // Recomputed on every `now` tick so "last 24h" stays a rolling window rather
  // than 24 hours measured from whenever the page happened to mount.
  const digest = React.useMemo(
    () => fabRecentLearningDigest(events),
    [events, now]
  );

  // Read off the same figure Fabric's "Entities indexed" card shows, rather
  // than re-adding the composition buckets: `FactualNode.type` is a free string
  // on the backend, so a producer emitting a new type would silently drop out
  // of a hand-rolled sum while still counting on Fabric.
  const nodeTotal = graphStats ? graphStats.entities.total : null;

  // `daily` is zero-filled and today-last (UTC days) per the endpoint contract.
  const todayAsks = stats && stats.daily && stats.daily.length
    ? stats.daily[stats.daily.length - 1].count : null;

  return (
    <div style={{ padding: '32px 32px 80px', maxWidth: 1080, margin: '0 auto' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
        <div>
          <h1 className="t-h1" style={{ margin: 0 }}>Welcome back, {fabOrgName()}</h1>
          <div className="text-secondary t-body mt-4">Here's what's happening across your org knowledge.</div>
        </div>
        <div className="mono-sm mono text-tertiary">
          {now.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })} · {now.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}
        </div>
      </div>

      {/* Learnings digest — derived from the graph snapshot */}
      <HomeDigest snapshot={snapshot} stats={graphStats} error={snapError} digest={digest} setRoute={setRoute}/>

      {/* Status cards */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginTop: 24 }}>
        <StatusCard label="Knowledge nodes"
          value={snapError ? '—' : nodeTotal == null ? '…' : nodeTotal.toLocaleString()}
          footer={snapError ? `Couldn't load — ${snapError}`
            : graphStats ? `${graphStats.composition.concepts.toLocaleString()} concepts · ${graphStats.composition.tables.toLocaleString()} tables · ${graphStats.composition.documents.toLocaleString()} documents`
            : 'Loading the graph…'}
          onClick={() => setRoute('fabric')}/>
        <StatusCard label={`Asks · last ${HOME_STATS_DAYS} days`}
          value={statsError ? '—' : stats == null ? '…' : stats.total.toLocaleString()}
          footer={statsError ? `Couldn't load — ${statsError}`
            : stats ? (todayAsks == null ? 'across every client' : `${todayAsks.toLocaleString()} today (UTC)`)
            : 'Loading usage…'}
          onClick={() => setRoute('observability')}/>
        <StatusCard label="Latency p50"
          value={statsError ? '—' : stats == null ? '…' : (stats.total ? rtvLatency(stats.latency_p50_ms) : '—')}
          footer={statsError ? `Couldn't load — ${statsError}`
            : stats ? (stats.total ? `avg ${rtvLatency(stats.latency_avg_ms)} · LLM + tool time` : 'no asks to measure yet')
            : 'Loading usage…'}
          onClick={() => setRoute('observability')}/>
      </div>

      {/* Recent retrievals */}
      <div style={{ marginTop: 32 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 12 }}>
          <h2 className="t-h2" style={{ margin: 0 }}>Recent retrievals</h2>
          <button className="btn sm ghost" onClick={() => setRoute('observability')}>See all →</button>
        </div>
        <HomeRecentRetrievals data={recent} error={recentError} setRoute={setRoute}/>
      </div>

      {/* Shortcuts */}
      <div style={{ marginTop: 32 }}>
        <h2 className="t-h2" style={{ margin: 0, marginBottom: 12 }}>Shortcuts</h2>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
          <ShortcutCard icon="Plus" title="Add a connector" desc="Databricks, Slack or file upload" onClick={() => setRoute('connectors')}/>
          <ShortcutCard icon="Code" title="Call the API" desc="Auth, upload, graph and ask endpoints" onClick={() => setRoute('api')}/>
          <ShortcutCard icon="Activity" title="Inspect retrievals" desc="Live queries, citations and latency" onClick={() => setRoute('observability')}/>
        </div>
      </div>
    </div>
  );
}

// "What the graph learned in the last 24 hours", counted off the snapshot. A
// quiet day says so — it does not round up to a headline.
function HomeDigest({ snapshot, stats, error, digest, setRoute }) {
  const box = (accent) => ({
    marginTop: 24, padding: '16px 20px',
    background: accent ? 'var(--accent-muted)' : 'var(--surface)',
    border: accent ? '1px solid transparent' : '1px solid var(--border-subtle)',
    borderRadius: 10,
    display: 'flex', alignItems: 'center', gap: 20,
  });

  if (error) {
    return (
      <div style={box(false)}>
        <Icons.AlertTri size={16} style={{ color: 'var(--error)' }}/>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 14, fontWeight: 500, color: 'var(--error)' }}>Couldn't load the knowledge graph</div>
          <div className="mono-sm mono text-tertiary" style={{ marginTop: 2 }}>{error}</div>
        </div>
      </div>
    );
  }
  if (!snapshot) {
    return (
      <div style={box(false)}>
        <div className="skeleton" style={{ width: 240, height: 16 }}/>
      </div>
    );
  }
  // A brand-new org has an empty graph, so "nothing new" would be a dead end —
  // point it at the thing that would actually give it something to learn.
  if (stats && stats.isEmpty) {
    return (
      <div style={box(false)}>
        <Icons.Sparkle size={16} style={{ color: 'var(--text-tertiary)' }}/>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 14, fontWeight: 500 }}>No knowledge indexed yet</div>
          <div className="mono-sm mono text-tertiary" style={{ marginTop: 2 }}>
            Connect Databricks or Slack, or upload a file, and the graph starts learning.
          </div>
        </div>
        <button className="btn sm accent" onClick={() => setRoute('connectors')}>Connect a source →</button>
      </div>
    );
  }
  if (digest.total === 0) {
    return (
      <div style={box(false)}>
        <Icons.Sparkle size={16} style={{ color: 'var(--text-tertiary)' }}/>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 14, fontWeight: 500 }}>Nothing new in the last 24 hours</div>
          <div className="mono-sm mono text-tertiary" style={{ marginTop: 2 }}>
            The graph learns as sources sync and questions get asked.
          </div>
        </div>
        <button className="btn sm" onClick={() => setRoute('learning')}>Learning history →</button>
      </div>
    );
  }

  const parts = [
    digest.entities  ? `◆ ${digest.entities} new ${digest.entities === 1 ? 'entity' : 'entities'}` : null,
    digest.relations ? `→ ${digest.relations} new relationship${digest.relations === 1 ? '' : 's'}` : null,
    digest.merges    ? `≈ ${digest.merges} merge${digest.merges === 1 ? '' : 's'}` : null,
  ].filter(Boolean);

  return (
    <div style={box(true)}>
      <Icons.Sparkle size={16} style={{ color: 'var(--accent-text)' }}/>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 14, fontWeight: 500, color: 'var(--accent-text)' }}>
          {parcleBrand().name} learned {digest.total.toLocaleString()} new thing{digest.total === 1 ? '' : 's'} in the last 24 hours
        </div>
        <div className="mono-sm mono" style={{ color: 'var(--accent-text)', opacity: 0.8, marginTop: 2 }}>
          {parts.length ? parts.join('  ·  ') : 'across re-ranking and schema inference'}
        </div>
      </div>
      <button className="btn sm" style={{ background: 'var(--surface)', borderColor: 'transparent' }}
        onClick={() => setRoute('learning')}>Review all →</button>
    </div>
  );
}

// The newest asks from the audit trail, same shape the Observability tab
// renders. Clicking a row hands off to that tab, which owns the detail drawer.
function HomeRecentRetrievals({ data, error, setRoute }) {
  if (error) {
    return (
      <div className="card" style={{ padding: 20 }}>
        <div className="t-small" style={{ color: 'var(--error)' }}>Couldn't load retrievals — {error}</div>
      </div>
    );
  }
  if (!data) {
    return (
      <div className="card" style={{ padding: 20 }}>
        <div className="skeleton" style={{ width: 260, height: 16 }}/>
      </div>
    );
  }
  const rows = data.retrievals || [];
  if (rows.length === 0) {
    return (
      <div className="card" style={{ padding: 20 }}>
        <div className="t-small text-secondary">
          No retrievals yet. Ask something in Chat — or through the ask-parcle skill — and
          the audit trail starts here.
        </div>
      </div>
    );
  }
  return (
    <div className="card" style={{ overflow: 'hidden' }}>
      <table className="parcle">
        <tbody>
          {rows.map(r => (
            <tr key={r.turn_id} onClick={() => setRoute('observability')}>
              <td style={{ width: 36 }}><StatusGlyph status={rtvStatusKey(r.status)}/></td>
              <td>{r.query}</td>
              <td style={{ width: 150 }}>
                {r.sources && r.sources.length > 0
                  ? <span className="mono-sm mono text-secondary">
                      {r.sources.length} source{r.sources.length === 1 ? '' : 's'}
                    </span>
                  : <span className="mono-sm mono text-tertiary">—</span>}
              </td>
              <td style={{ width: 80 }}>
                <span className="mono-sm mono text-secondary">
                  {r.confidence != null ? r.confidence.toFixed(2) : '—'}
                </span>
              </td>
              <td style={{ width: 80 }}><span className="mono-sm mono text-secondary">{rtvLatency(r.latency_ms)}</span></td>
              <td style={{ width: 90 }}><span className="mono-sm mono text-tertiary">{rtvTime(r.created_at)}</span></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
function StatusCard({ label, value, footer, onClick }) {
  return (
    <button onClick={onClick} className="card" style={{ padding: 20, textAlign: 'left' }}>
      <div className="mono-sm mono text-tertiary">{label}</div>
      <div className="t-display" style={{ marginTop: 6 }}>{value}</div>
      <div className="t-small text-secondary mt-4">{footer}</div>
    </button>
  );
}
function ShortcutCard({ icon, title, desc, onClick }) {
  const I = Icons[icon];
  return (
    <button onClick={onClick} className="card" style={{ padding: 18, textAlign: 'left', display: 'flex', gap: 14, alignItems: 'center' }}>
      <div style={{ width: 36, height: 36, borderRadius: 8, background: 'var(--accent-muted)', color: 'var(--accent-text)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><I size={16}/></div>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 14, fontWeight: 500 }}>{title}</div>
        <div className="t-small text-tertiary mt-4">{desc}</div>
      </div>
      <Icons.ArrowR size={14} style={{ color: 'var(--text-tertiary)' }}/>
    </button>
  );
}

// APIPage is not exported here — api_docs.jsx owns the `api` route.
Object.assign(window, { EntityDrawer, ObservabilityPage, HomePage });
