/* global React, apiFetch, useApiResource, useAuth, useToast, Select */

// MCP Analytics v2 — the "Session Analytics" standalone app.
// Renders its own sidebar (with an Armature app switcher) and its
// four primary views (Overview, Topics, Searches, Sessions) plus a
// thinking-trace drill-down for any session.
//
// Mounts at /mcp-analytics. Gated by `auth.me.featureFlags.mcpAnalytics`.
// app.jsx renders this page in a layoutMode="standalone" branch so the
// global Armature sidebar is suppressed and we replace it with the SA
// sidebar end-to-end.
//
// Data: GET /api/mcp-analytics/v2/overview|topics|searches|sessions etc.
// See lib/mcp-analytics/queries-v2.js + docs/superpowers/specs/2026-05-27.

const {
  useEffect: useEffectMa,
  useMemo: useMemoMa,
  useState: useStateMa,
  useCallback: useCallbackMa,
} = React;

const SA_RANGES = ['24h', '7d', '30d', '90d'];
// Sessions table pages by growing the request limit. We start at one page
// and add a page per "Load more" click, up to the server-side cap in
// listSessionsV2 (1000). When fewer than the limit come back, we've hit the
// end of the feed for this range/filter.
const SA_SESSIONS_PAGE_SIZE = 100;
const SA_SESSIONS_MAX = 1000;
const SA_NAV = [
  { key: 'overview', label: 'Overview', path: 'overview' },
  { key: 'topics', label: 'Topics', path: 'topics' },
  { key: 'searches', label: 'Searches', path: 'searches' },
  { key: 'sessions', label: 'Sessions', path: 'sessions' },
];

function saNormaliseTab(tab) {
  return SA_NAV.find((n) => n.key === tab)?.key || 'overview';
}

// ────────────────────────────────────────────────────────────────────────
// SIGIL — deterministic 28×28 SVG keyed off the session id hash. The
// same id always renders the same shape; gives anonymous sessions a
// distinct visual identity in tables and rails. Pure function; no API.
// ────────────────────────────────────────────────────────────────────────

function saFnv1aHash(str) {
  let h = 0x811c9dc5;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 0x01000193) >>> 0;
  }
  return h;
}

const SIGIL_TEMPLATES = [
  // 8 templates. Each is a list of <rect|circle|polygon> tuples on a
  // 0–28 grid; the picked palette colours map to slot 0 and slot 1.
  function tpl0(c0, c1) { // split-square
    return [
      { tag: 'rect', x: 0, y: 0, w: 14, h: 14, fill: c0 },
      { tag: 'rect', x: 14, y: 14, w: 14, h: 14, fill: c1 },
    ];
  },
  function tpl1(c0, c1) { // ring
    return [
      { tag: 'circle', cx: 14, cy: 14, r: 10, fill: c0 },
      { tag: 'circle', cx: 14, cy: 14, r: 4, fill: '#F6F2EF' },
    ];
  },
  function tpl2(c0, c1) { // triangle
    return [{ tag: 'polygon', points: '0,28 28,28 14,0', fill: c0 }];
  },
  function tpl3(c0, c1) { // bordered with center square
    return [
      { tag: 'rect', x: 2, y: 2, w: 24, h: 24, fill: 'none', stroke: '#090808', sw: 2 },
      { tag: 'rect', x: 10, y: 10, w: 8, h: 8, fill: c1 },
    ];
  },
  function tpl4(c0, c1) { // two horizontal bars
    return [
      { tag: 'rect', x: 0, y: 0, w: 28, h: 9, fill: c0 },
      { tag: 'rect', x: 0, y: 19, w: 28, h: 9, fill: c0 },
    ];
  },
  function tpl5(c0, c1) { // diamond
    return [
      { tag: 'polygon', points: '14,0 28,14 14,28 0,14', fill: 'none', stroke: '#090808', sw: 2 },
      { tag: 'circle', cx: 14, cy: 14, r: 4, fill: c1 },
    ];
  },
  function tpl6(c0, c1) { // quadrants
    return [
      { tag: 'rect', x: 0, y: 0, w: 9, h: 9, fill: c0 },
      { tag: 'rect', x: 19, y: 0, w: 9, h: 9, fill: c1 },
      { tag: 'rect', x: 0, y: 19, w: 9, h: 9, fill: c1 },
      { tag: 'rect', x: 19, y: 19, w: 9, h: 9, fill: c0 },
    ];
  },
  function tpl7(c0, c1) { // vertical half
    return [{ tag: 'rect', x: 0, y: 0, w: 14, h: 28, fill: c0 }];
  },
];

const SIGIL_PALETTE = [
  ['#090808', '#c8410d'], // ink + brick
  ['#090808', '#090808'], // ink + ink
  ['#c8410d', '#090808'], // brick + ink
  ['#8A5A00', '#090808'], // warn + ink
];

function Sigil({ seed, size = 28 }) {
  const safeSeed = String(seed || '');
  const h = saFnv1aHash(safeSeed);
  const tplIdx = h % SIGIL_TEMPLATES.length;
  let palIdx = (h >>> 3) % SIGIL_PALETTE.length;
  const [c0, c1] = SIGIL_PALETTE[palIdx];
  const shapes = SIGIL_TEMPLATES[tplIdx](c0, c1);
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 28 28"
      style={{ display: 'block', border: '1px solid var(--ink)' }}>
      <rect width="28" height="28" fill="#F6F2EF" />
      {shapes.map((s, i) => {
        if (s.tag === 'rect') {
          return <rect key={i} x={s.x} y={s.y} width={s.w} height={s.h}
            fill={s.fill} stroke={s.stroke} strokeWidth={s.sw} />;
        }
        if (s.tag === 'circle') {
          return <circle key={i} cx={s.cx} cy={s.cy} r={s.r} fill={s.fill} />;
        }
        return <polygon key={i} points={s.points}
          fill={s.fill} stroke={s.stroke} strokeWidth={s.sw} />;
      })}
    </svg>
  );
}

function saShortId(sessionId) {
  if (!sessionId) return 'sess_…';
  const clean = String(sessionId).replace(/-/g, '');
  if (clean.length <= 8) return `sess_${clean}`;
  return `sess_${clean.slice(0, 4)}…${clean.slice(-3)}`;
}

// Friendly label for the MCP client ("harness") behind a session. The
// gateway learns the raw clientInfo.name from the `initialize` handshake
// (e.g. "claude-ai", "cursor-vscode", "Visual Studio Code"); we map the
// ones we recognise to a tidy display name and otherwise show the raw
// value. NULL client_name (legacy/unknown sessions) returns null so the
// caller can omit the chip rather than render "Unknown" noise inline.
const SA_HARNESS_LABELS = {
  'claude-ai': 'Claude',
  'claude-code': 'Claude Code',
  claude: 'Claude',
  cursor: 'Cursor',
  'cursor-vscode': 'Cursor',
  windsurf: 'Windsurf',
  cline: 'Cline',
  continue: 'Continue',
  'visual studio code': 'VS Code',
  vscode: 'VS Code',
  zed: 'Zed',
  goose: 'Goose',
  'mcp-inspector': 'MCP Inspector',
};

function saHarnessLabel(clientName) {
  if (!clientName) return null;
  const raw = String(clientName).trim();
  if (!raw) return null;
  return SA_HARNESS_LABELS[raw.toLowerCase()] || raw;
}

// Bookmark glyph for the Save / Saved toggle in the trace top bar.
// Outline when idle, solid when saved (filled brick via CSS color).
function SaBookmarkIcon({ filled = false, size = 14 }) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 16 16"
      fill={filled ? 'currentColor' : 'none'}
      stroke="currentColor"
      strokeWidth="1.6"
      strokeLinejoin="miter"
      aria-hidden="true">
      <path d="M3 1 H13 V15 L8 11 L3 15 Z" />
    </svg>
  );
}

function saFormatRelative(iso) {
  if (!iso) return '';
  const t = new Date(iso).getTime();
  if (Number.isNaN(t)) return '';
  const diffMs = Date.now() - t;
  const m = Math.floor(diffMs / 60000);
  if (m < 1) return 'just now';
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  const d = Math.floor(h / 24);
  if (d < 30) return `${d}d ago`;
  return new Date(iso).toLocaleDateString();
}

function saFormatNumber(n) {
  if (n == null) return '–';
  return new Intl.NumberFormat('en-US').format(n);
}

// One-line narrative summary of a topic/use-case row. Reads as
// a sentence so a PM doesn't have to interpret a row of numbers.
function saTopicNarrative(topic) {
  const total = (topic.member_count || 0);
  if (total === 0) return '';
  const sessions = total === 1 ? '1 session' : `${saFormatNumber(total)} sessions`;
  const bits = [sessions];
  if (topic.peak_frustration === 'high') bits.push('high frustration');
  else if (topic.peak_frustration === 'medium') bits.push('some frustration');
  if (topic.last_seen_at && Date.now() - new Date(topic.last_seen_at).getTime() < 24 * 60 * 60 * 1000) {
    bits.push('seen today');
  }
  return bits.join(' · ');
}

function saFormatDelta(pct) {
  if (pct == null) return '—';
  if (pct === 0) return '— flat';
  const sign = pct > 0 ? '↑' : '↓';
  return `${sign} ${Math.abs(pct)}%`;
}

function saFormatDurationMs(ms) {
  const n = Number(ms) || 0;
  if (n <= 0) return '0s';
  if (n < 1000) return `${n} ms`;
  const totalSec = Math.round(n / 1000);
  if (totalSec < 60) return `${totalSec}s`;
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  if (m < 60) return s ? `${m}m ${s}s` : `${m}m`;
  const h = Math.floor(m / 60);
  return `${h}h ${m % 60}m`;
}

function saFormatPercent(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return '—';
  return `${Math.round(n * 100)}%`;
}

function saHumanLabel(value) {
  return String(value || '')
    .replace(/_/g, ' ')
    .replace(/\b\w/g, (ch) => ch.toUpperCase());
}

function saConfidenceLabel(value) {
  const n = Number(value);
  if (!Number.isFinite(n)) return 'Unknown';
  if (n >= 0.75) return 'High';
  if (n >= 0.45) return 'Medium';
  return 'Low';
}

// ────────────────────────────────────────────────────────────────────────
// SHELL — sidebar with app switcher + Session Analytics nav
// ────────────────────────────────────────────────────────────────────────

function SaAppSwitcher({ navigate, onClose }) {
  return (
    <div className="sa-app-popover" role="menu" onClick={(e) => e.stopPropagation()}>
      <div
        className="sa-app-opt"
        role="menuitem"
        tabIndex={0}
        onClick={() => { onClose(); navigate('/'); }}>
        <div className="sa-app-opt-icon ink">
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
            <rect x="2" y="2" width="10" height="10" fill="none" stroke="#F6F2EF" strokeWidth="1.4" />
            <path d="M4 7 L6 9 L10 5" stroke="#F6F2EF" strokeWidth="1.4" fill="none" />
          </svg>
        </div>
        <div>
          <div className="sa-app-opt-name">Testing &amp; Benchmarks</div>
          <div className="sa-app-opt-desc">RUN WORKFLOWS · COMPARE MCPS</div>
        </div>
      </div>
      <div className="sa-app-opt is-current" role="menuitem" aria-current="page">
        <div className="sa-app-opt-icon brick">
          <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
            <rect x="1" y="1" width="6" height="6" fill="#F6F2EF" />
            <rect x="7" y="7" width="6" height="6" fill="#F6F2EF" />
          </svg>
        </div>
        <div>
          <div className="sa-app-opt-name">Session Analytics</div>
          <div className="sa-app-opt-desc">CODE-MODE MCP TELEMETRY</div>
        </div>
      </div>
    </div>
  );
}

function SaSidebar({ activeKey, navigate, counts = null, mcpServerName = null, profile = null }) {
  const [switcherOpen, setSwitcherOpen] = useStateMa(false);
  useEffectMa(() => {
    if (!switcherOpen) return undefined;
    function onDoc() { setSwitcherOpen(false); }
    document.addEventListener('click', onDoc);
    return () => document.removeEventListener('click', onDoc);
  }, [switcherOpen]);
  return (
    <aside className="sa-sidebar">
      <div className="sa-app-switcher">
        <div
          className="sa-app-current"
          role="button"
          tabIndex={0}
          onClick={(e) => { e.stopPropagation(); setSwitcherOpen((o) => !o); }}>
          <div className="sa-app-logo">
            <svg viewBox="0 0 18 18" fill="none">
              <rect x="1" y="1" width="16" height="16" fill="#F6F2EF" />
              <rect x="1" y="1" width="7" height="7" fill="#090808" />
              <rect x="10" y="10" width="7" height="7" fill="#090808" />
            </svg>
          </div>
          <div className="sa-app-copy">
            <div className="sa-app-eyebrow">ARMATURE</div>
            <div className="sa-app-name">Session Analytics</div>
          </div>
          <span className="sa-app-caret">▾</span>
        </div>
        {switcherOpen && <SaAppSwitcher navigate={navigate} onClose={() => setSwitcherOpen(false)} />}
      </div>
      <div className="sa-ctx">
        <div className="sa-ctx-eyebrow">MCP SOURCE</div>
        <div className="sa-ctx-row">
          <span className="sa-ctx-name">{mcpServerName || 'all servers'}</span>
        </div>
      </div>
      <nav className="sa-nav">
        {SA_NAV.map((item) => (
          <a
            key={item.key}
            className={`sa-nav-link ${activeKey === item.key ? 'is-active' : ''}`}
            onClick={() => navigate(`/mcp-analytics/${item.path}`)}>
            <span className="sa-nav-icon">
              <SaNavIcon name={item.key} />
            </span>
            <span className="sa-nav-label">{item.label}</span>
            {counts && counts[item.key] != null && (
              <span className="sa-nav-badge">{saFormatNumber(counts[item.key])}</span>
            )}
          </a>
        ))}
      </nav>
      <div className="sa-spacer"></div>
      <div className="sa-sidebar-footer">
        <div className="sa-footer-row">
          <div className="sa-footer-avatar">{(profile?.displayName || profile?.email || '?').slice(0, 2).toUpperCase()}</div>
          <div className="sa-footer-copy">
            <div className="sa-footer-name">{profile?.displayName || profile?.email || 'User'}</div>
            <div className="sa-footer-org">{profile?.organization?.name?.toUpperCase() || ''}</div>
          </div>
        </div>
      </div>
    </aside>
  );
}

function SaNavIcon({ name }) {
  switch (name) {
    case 'overview':
      return (
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
          <rect x="2" y="2" width="6" height="6" fill="currentColor" />
          <rect x="10" y="2" width="6" height="6" fill="none" stroke="currentColor" strokeWidth="1.4" />
          <rect x="2" y="10" width="6" height="6" fill="none" stroke="currentColor" strokeWidth="1.4" />
          <rect x="10" y="10" width="6" height="6" fill="currentColor" />
        </svg>
      );
    case 'topics':
      return (
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
          <circle cx="9" cy="9" r="7" stroke="currentColor" strokeWidth="1.4" fill="none" />
          <circle cx="9" cy="9" r="3.5" stroke="currentColor" strokeWidth="1.4" fill="none" />
          <circle cx="9" cy="9" r="1" fill="currentColor" />
        </svg>
      );
    case 'searches':
      return (
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
          <circle cx="8" cy="8" r="5" stroke="currentColor" strokeWidth="1.4" fill="none" />
          <line x1="12" y1="12" x2="15" y2="15" stroke="currentColor" strokeWidth="1.4" />
        </svg>
      );
    case 'sessions':
      return (
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
          <rect x="2" y="3" width="14" height="3" fill="currentColor" />
          <rect x="2" y="8" width="10" height="3" fill="currentColor" />
          <rect x="2" y="13" width="12" height="3" fill="currentColor" />
        </svg>
      );
    default: return null;
  }
}

function SaRangePicker({ range, onChange }) {
  return (
    <div className="sa-range">
      {SA_RANGES.map((r) => (
        <button
          key={r}
          className={r === range ? 'is-active' : ''}
          onClick={() => onChange(r)}>
          {r}
        </button>
      ))}
    </div>
  );
}

function SaToolbar({ title, range, onRangeChange, right = null }) {
  return (
    <div className="sa-toolbar">
      <h1>{title}</h1>
      <div className="sa-toolbar-spacer" />
      {right}
      {range && (
        <SaRangePicker range={range} onChange={onRangeChange} />
      )}
    </div>
  );
}

function saBuildQuery(baseQuery = '', additions = {}) {
  const params = new URLSearchParams(String(baseQuery || '').replace(/^\?/, ''));
  for (const [key, value] of Object.entries(additions)) {
    if (value == null || value === '') params.delete(key);
    else params.set(key, String(value));
  }
  const qs = params.toString();
  return qs ? `?${qs}` : '';
}

function SaServerFilter({ servers = [], value, onChange, loading = false }) {
  const serverOptions = servers.map((server) => ({
    value: server.id,
    label: server.name || server.base_url || server.id,
  }));
  const selectedServer = value && value !== 'all' && !serverOptions.some((option) => option.value === value)
    ? [{ value, label: value }]
    : [];
  const options = [
    { value: 'all', label: 'All MCP servers' },
    ...selectedServer,
    ...serverOptions,
  ];
  return (
    <div className="sa-server-filter">
      <span className="sa-server-filter-label">MCP server</span>
      <Select
        value={value || 'all'}
        onChange={(next) => onChange(next || 'all')}
        options={options}
        disabled={loading || options.length <= 1}
        searchable={options.length > 8}
        ariaLabel="Filter by MCP server" />
    </div>
  );
}

function SaLoading() {
  return <div className="sa-loading">Loading…</div>;
}

function SaError({ error }) {
  return (
    <div className="sa-error">
      <strong>Couldn't load data.</strong>
      <span>{error?.message || 'Unknown error'}</span>
    </div>
  );
}

function SaEmpty({ title, hint }) {
  return (
    <div className="sa-empty">
      <div className="sa-empty-title">{title}</div>
      {hint && <div className="sa-empty-hint">{hint}</div>}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────
// OVERVIEW PAGE
// ────────────────────────────────────────────────────────────────────────

function SaOverviewPage({
  range, onRangeChange, navigate, mcpServerId = null, serverControl = null, analyticsQuery = '',
}) {
  const serverParam = mcpServerId ? `&server=${encodeURIComponent(mcpServerId)}` : '';
  const { data, loading, error } = useApiResource(
    `/api/mcp-analytics/v2/overview?range=${range}${serverParam}`,
    [range, mcpServerId],
  );
  return (
    <>
      <SaToolbar title="Overview" range={range} onRangeChange={onRangeChange} right={serverControl} />
      <div className="sa-purpose-strip">
        <span className="sa-purpose-eyebrow">What is this?</span>
        <span className="sa-purpose-body">
          A quick read on whether your MCP is doing its job this week —
          volume, success, and the friction points worth poking at.
        </span>
      </div>
      {loading && !data ? <SaLoading /> : error ? <SaError error={error} /> : data && (
        <SaOverviewBody data={data} navigate={navigate} analyticsQuery={analyticsQuery} />
      )}
    </>
  );
}

function SaOverviewBody({ data, navigate, analyticsQuery = '' }) {
  const kpis = data.kpis || {};
  const buckets = data.by_bucket || [];
  const clients = data.client_distribution || [];
  return (
    <>
      <div className="sa-kpis">
        <SaKpi label="Conversations" value={kpis.conversations?.value} delta={kpis.conversations?.delta_pct} prior={kpis.conversations?.prior_value} tone="neutral" />
        <SaKpi label="Tool calls" value={kpis.tool_calls?.value} delta={kpis.tool_calls?.delta_pct} prior={kpis.tool_calls?.prior_value} tone="neutral" />
        <SaKpi label="Failed searches" value={kpis.failed_searches?.value} delta={kpis.failed_searches?.delta_pct} prior={kpis.failed_searches?.prior_value} tone="bad-on-up" />
        <SaKpi label="High-frustration sessions" value={kpis.high_frustration_sessions?.value} delta={kpis.high_frustration_sessions?.delta_pct} prior={kpis.high_frustration_sessions?.prior_value} tone="bad-on-up" />
      </div>

      <SaToolCallChart buckets={buckets} />

      <SaClientChart clients={clients} />

      <div className="sa-two-col">
        <SaPanel
          title={data.top_topics_fallback ? 'Top intents · provisional' : 'Top topics'}
          linkText="VIEW ALL →"
          onLink={() => navigate(`/mcp-analytics/topics${analyticsQuery}`)}>
          {data.top_topics?.length > 0 ? (
            <div className="sa-topic-list-mini">
              {data.top_topics.map((t, i) => (
                <SaTopicRowMini
                  key={t.id || `raw-${i}`}
                  rank={i + 1}
                  topic={t}
                  onClick={() => navigate(`/mcp-analytics/topics${analyticsQuery}`)} />
              ))}
            </div>
          ) : (
            <SaEmpty title="No topics yet" hint="Classification cron is still building clusters." />
          )}
        </SaPanel>
        <SaPanel
          title={data.top_failed_search_clusters_fallback ? 'Failed searches · provisional' : 'Failed searches'}
          linkText="VIEW ALL →"
          onLink={() => navigate(`/mcp-analytics/searches${analyticsQuery}`)}>
          {data.top_failed_search_clusters?.length > 0 ? (
            <div className="sa-miss-list">
              {data.top_failed_search_clusters.map((c, i) => (
                <SaMissRow
                  key={c.id || `raw-${i}`}
                  cluster={c}
                  onClick={() => navigate(`/mcp-analytics/searches${analyticsQuery}`)} />
              ))}
            </div>
          ) : (
            <SaEmpty title="No failed searches yet" hint="They emerge as users try things your API doesn't expose." />
          )}
        </SaPanel>
      </div>
    </>
  );
}

function SaKpi({ label, value, delta, prior = null, tone }) {
  let deltaCls = 'sa-kpi-delta flat';
  let deltaText = '— flat';
  if (delta == null) {
    // null delta == prior period was empty (first-period data).
    // "↑ 100%" reads as doubled — show NEW instead so the user knows
    // there's no baseline to compare against yet.
    deltaText = prior === 0 ? 'NEW' : '—';
  } else if (delta !== 0) {
    if (tone === 'bad-on-up') deltaCls = delta > 0 ? 'sa-kpi-delta down' : 'sa-kpi-delta up';
    else deltaCls = delta > 0 ? 'sa-kpi-delta up' : 'sa-kpi-delta down';
    deltaText = saFormatDelta(delta);
  }
  return (
    <div className="sa-kpi">
      <div className="sa-kpi-label">{label}</div>
      <div className="sa-kpi-row">
        <div className="sa-kpi-value">{saFormatNumber(value)}</div>
        <div className={deltaCls}>{deltaText}</div>
      </div>
    </div>
  );
}

function SaToolCallChart({ buckets }) {
  if (!buckets || buckets.length === 0) {
    return (
      <div className="sa-chart-card">
        <div className="sa-chart-head">
          <div className="sa-chart-title">Tool calls over time</div>
        </div>
        <div className="sa-chart-empty">No data in this range yet.</div>
      </div>
    );
  }
  // Layout in CSS px (uniform scale — text must not stretch, so no
  // preserveAspectRatio="none"). Margins reserve room for the axes.
  const VW = 960;
  const VH = 280;
  const ML = 52; // left gutter for y-axis value labels
  const MR = 20;
  const MT = 18;
  const MB = 44; // bottom gutter for x-axis date labels
  const plotW = VW - ML - MR;
  const plotH = VH - MT - MB;
  const baseY = MT + plotH;
  const maxStack = Math.max(1, ...buckets.map((b) => (b.exec_count || 0) + (b.search_count || 0)));
  // Round the top up to a clean number and place ~3 evenly-spaced ticks.
  const rawStep = maxStack / 3;
  const pow = Math.pow(10, Math.floor(Math.log10(rawStep)));
  const norm = rawStep / pow;
  // Counts are integers, so never let the tick step drop below 1 — otherwise
  // low-volume ranges (maxStack < 3) would render fractional labels (0.5, 1.5…).
  const step = Math.max(1, (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * pow);
  const niceMax = Math.max(step, Math.ceil(maxStack / step) * step);
  const ticks = [];
  for (let t = 0; t <= niceMax + 1e-9; t += step) ticks.push(t);
  const yFor = (v) => baseY - (v / niceMax) * plotH;
  const slot = plotW / buckets.length;
  const barW = Math.max(3, Math.min(54, slot * 0.6));
  const xCenter = (i) => ML + slot * (i + 0.5);
  // Thin out x-axis labels so they never collide on dense ranges.
  const labelEvery = Math.max(1, Math.ceil(buckets.length / Math.max(1, Math.floor(plotW / 72))));
  const fmtDay = (iso) => {
    try { return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }); } catch (_e) { return ''; }
  };
  const searchTotal = buckets.reduce((n, b) => n + (b.search_count || 0), 0);
  const execTotal = buckets.reduce((n, b) => n + (b.exec_count || 0), 0);
  const legendCls = (total) => `${total > 0 ? '' : 'is-empty'}`;
  // Stacked bars: execute_script (brand rust) over search_sdk (charcoal).
  const EXEC_FILL = '#c8410d'; // --brand
  const SEARCH_FILL = '#2b2a28'; // charcoal
  return (
    <div className="sa-chart-card">
      <div className="sa-chart-head">
        <div className="sa-chart-title">Tool calls over time</div>
        <div className="sa-legend">
          <span className={legendCls(searchTotal)} title={searchTotal > 0 ? undefined : 'No volume in this range'}><span className="sa-legend-sw search" />search_sdk</span>
          <span className={legendCls(execTotal)} title={execTotal > 0 ? undefined : 'No volume in this range'}><span className="sa-legend-sw exec" />execute_script</span>
        </div>
      </div>
      <div className="sa-chart-body">
        <svg className="sa-chart-svg" viewBox={`0 0 ${VW} ${VH}`} role="img" aria-label="Tool calls over time">
          <g className="sa-chart-axis" fontSize="11" fontFamily="ui-monospace, SFMono-Regular, monospace" fill="#8a857f">
            {ticks.map((t) => (
              <g key={`y${t}`}>
                <line x1={ML} y1={yFor(t)} x2={VW - MR} y2={yFor(t)} stroke="rgba(9,8,8,0.08)" strokeWidth="1" />
                <text x={ML - 8} y={yFor(t) + 4} textAnchor="end">{saFormatNumber(t)}</text>
              </g>
            ))}
            <line x1={ML} y1={baseY} x2={VW - MR} y2={baseY} stroke="rgba(9,8,8,0.25)" strokeWidth="1" />
            {buckets.map((b, i) => (
              i % labelEvery === 0 ? (
                <text key={`x${i}`} x={xCenter(i)} y={baseY + 22} textAnchor="middle">{fmtDay(b.bucket)}</text>
              ) : null
            ))}
          </g>
          {buckets.map((b, i) => {
            const s = b.search_count || 0;
            const e = b.exec_count || 0;
            const x = xCenter(i) - barW / 2;
            return (
              <g key={`bar${i}`}>
                {s > 0 ? <rect x={x} y={yFor(s)} width={barW} height={baseY - yFor(s)} fill={SEARCH_FILL} /> : null}
                {e > 0 ? <rect x={x} y={yFor(s + e)} width={barW} height={yFor(s) - yFor(s + e)} fill={EXEC_FILL} /> : null}
              </g>
            );
          })}
        </svg>
      </div>
    </div>
  );
}

function SaPanel({ title, linkText, onLink, children }) {
  return (
    <div className="sa-panel">
      <div className="sa-panel-head">
        <div className="sa-panel-title">{title}</div>
        {linkText && <span className="sa-panel-link" onClick={onLink}>{linkText}</span>}
      </div>
      {children}
    </div>
  );
}

// Brand marks for the known MCP clients — the same harness logo assets the
// Test & Benchmark app uses, so a client reads identically across the product.
// Matching is on a lower-cased substring of the reported clientInfo.name.
// Order matters: the more specific test (e.g. "claude code") must precede the
// broader one ("claude").
const SA_CLIENT_LOGO_BASE = '/frontend/assets/logos/';
const SA_CLIENT_BRANDS = [
  { test: (n) => n.includes('claude code') || n.includes('claude-code') || n.includes('claude_code'), label: 'Claude Code', src: 'claude-code.png' },
  { test: (n) => n.includes('codex'), label: 'Codex', src: 'codex.png' },
  { test: (n) => n.includes('openclaw'), label: 'OpenClaw', src: 'openclaw.svg' },
  { test: (n) => n.includes('opencode'), label: 'OpenCode', src: 'opencode.svg' },
  { test: (n) => n.includes('cursor'), label: 'Cursor', src: 'cursor.png' },
  { test: (n) => n.includes('vscode') || n.includes('vs code') || n.includes('visual studio'), label: 'VS Code', src: 'vscode.png' },
  { test: (n) => n.includes('gemini') || n.includes('google'), label: 'Gemini', src: 'gemini.png' },
  { test: (n) => n.includes('chatgpt') || n.includes('openai') || n.includes('gpt'), label: 'ChatGPT', src: 'chatgpt.png' },
  { test: (n) => n.includes('claude') || n.includes('anthropic'), label: 'Claude', src: 'anthropic.svg.png' },
];

// Catch-all buckets. UNKNOWN = no client recorded on the handshake (legacy /
// non-conformant sessions); OTHER = a real client string we don't have a brand
// for (a harness or third-party MCP client outside our known lineup). Neither
// has a logo asset, so both render a neutral monogram chip.
const SA_CLIENT_UNKNOWN = { key: '__unknown', label: 'Unknown', mono: '?' };
const SA_CLIENT_OTHER = { key: '__other', label: 'Other', mono: '⋯' };

// Resolve a raw client_name to a brand. Unrecognised real names collapse into
// the single OTHER bucket rather than rendering one monogram row each.
function saClientBrand(rawName) {
  const name = String(rawName || '').toLowerCase();
  if (!name || name === 'unknown') return SA_CLIENT_UNKNOWN;
  return SA_CLIENT_BRANDS.find((b) => b.test(name)) || SA_CLIENT_OTHER;
}

function SaClientLogo({ brand }) {
  return (
    <span className="sa-client-logo" title={brand.label}>
      {brand.src ? (
        <img src={`${SA_CLIENT_LOGO_BASE}${brand.src}`} alt="" width="16" height="16" />
      ) : (
        <span className="sa-client-mono">{brand.mono}</span>
      )}
    </span>
  );
}

// Overview "Clients" chart: which MCP client drives the gateway. Ranked
// horizontal bars with a brand logo, count and share. Reads visible sessions
// only (handshake-only + hidden sessions are already excluded server-side).
// Distinct client_names that resolve to the same brand (e.g. several Claude
// proxies) are summed; unrecognised names fold into "Other". The Other and
// Unknown buckets always sort last regardless of size.
function SaClientChart({ clients }) {
  const input = (clients || []).filter((c) => (c.session_count || 0) > 0);
  if (input.length === 0) {
    return (
      <div className="sa-chart-card">
        <div className="sa-chart-head"><div className="sa-chart-title">Clients</div></div>
        <div className="sa-chart-empty">No client data in this range yet.</div>
      </div>
    );
  }
  const byBrand = new Map();
  for (const c of input) {
    const brand = saClientBrand(c.client_name);
    const existing = byBrand.get(brand.label);
    if (existing) existing.count += c.session_count || 0;
    else byBrand.set(brand.label, { brand, count: c.session_count || 0 });
  }
  const rank = (label) => (label === SA_CLIENT_OTHER.label ? 2 : label === SA_CLIENT_UNKNOWN.label ? 3 : 1);
  const rows = Array.from(byBrand.values()).sort((a, b) => {
    const ra = rank(a.brand.label);
    const rb = rank(b.brand.label);
    return ra !== rb ? ra - rb : b.count - a.count;
  });
  const total = rows.reduce((n, r) => n + r.count, 0);
  const max = Math.max(...rows.map((r) => r.count));
  return (
    <div className="sa-chart-card">
      <div className="sa-chart-head">
        <div className="sa-chart-title">Clients</div>
        <div className="sa-chart-sub">{saFormatNumber(total)} sessions</div>
      </div>
      <div className="sa-client-list">
        {rows.map(({ brand, count }) => {
          const pct = total > 0 ? Math.round((count / total) * 100) : 0;
          return (
            <div className="sa-client-row" key={brand.label}>
              <SaClientLogo brand={brand} />
              <div className="sa-client-name">{brand.label}</div>
              <div className="sa-client-bar-wrap">
                <span className="sa-client-bar" style={{ width: `${max > 0 ? (count / max) * 100 : 0}%` }} />
              </div>
              <div className="sa-client-count">{saFormatNumber(count)}</div>
              <div className="sa-client-pct">{pct}%</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function SaTopicRowMini({ rank, topic, onClick }) {
  return (
    <div className="sa-topic-row-mini" onClick={onClick}>
      <div className="sa-topic-rank">{String(rank).padStart(2, '0')}</div>
      <div>
        <div className="sa-topic-label">
          {topic.label}
          {topic.is_unmet_demand && <span className="sa-badge-demand">UNMET</span>}
        </div>
      </div>
      <div className="sa-topic-vol">{saFormatNumber(topic.member_count)}</div>
    </div>
  );
}

function SaMissRow({ cluster, onClick }) {
  return (
    <div className="sa-miss-row" onClick={onClick}>
      <div>
        <span className="sa-miss-tag">{(cluster.label || '').toUpperCase().slice(0, 14)}</span>
        <span className="sa-miss-q">{cluster.label}</span>
      </div>
      <div className="sa-miss-vol">{saFormatNumber(cluster.member_count)}</div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────
// TOPICS PAGE
// ────────────────────────────────────────────────────────────────────────

function SaTopicsPage({
  range, onRangeChange, navigate, mcpServerId = null, serverControl = null, analyticsQuery = '',
}) {
  const [sort, setSort] = useStateMa('volume');
  const [showAll, setShowAll] = useStateMa(false);
  const serverParam = mcpServerId ? `&server=${encodeURIComponent(mcpServerId)}` : '';
  const { data, loading, error } = useApiResource(
    `/api/mcp-analytics/v2/topics?range=${range}&sort=${sort}${serverParam}`,
    [range, sort, mcpServerId],
  );
  const allTopics = data?.topics || [];
  const visible = showAll ? allTopics : allTopics.slice(0, 10);
  return (
    <>
      <SaToolbar
        title="Use cases"
        range={range}
        onRangeChange={onRangeChange}
        right={(
          <>
            {serverControl}
            <SaSortDropdown value={sort} onChange={setSort} options={[
              { value: 'volume', label: 'Most common' },
              { value: 'frustration', label: 'Most frustrating' },
              { value: 'trend', label: 'Most recent' },
            ]} />
          </>
        )}
      />
      <SaTopicsPurpose />
      {data?.fallback === 'raw_intents' && (
        <SaProvisionalBanner
          label="Provisional view"
          body="Real agent intents, grouped by exact @intent string. The classify cron will merge near-duplicates into named topics within the hour. Until then, click any row to read the sessions behind it." />
      )}
      {loading && !data ? <SaLoading /> : error ? <SaError error={error} /> : (
        <>
          <SaTopicsBody
            topics={visible}
            navigate={navigate}
            mcpServerId={mcpServerId}
            analyticsQuery={analyticsQuery} />
          {!showAll && allTopics.length > 10 && (
            <button type="button" className="sa-show-more" onClick={() => setShowAll(true)}>
              Show all {allTopics.length} →
            </button>
          )}
        </>
      )}
    </>
  );
}

// A one-line purpose-stripe under the page header so the customer
// knows what to do with the page. Designed to read as instruction,
// not decoration — it shouldn't compete with the data below.
function SaTopicsPurpose() {
  return (
    <div className="sa-purpose-strip">
      <span className="sa-purpose-eyebrow">What is this?</span>
      <span className="sa-purpose-body">
        Jobs users are asking your agent to do. Pick what matters —
        <strong> most common</strong>, <strong>most frustrating</strong> —
        then click a row to see the sessions where it happened.
      </span>
    </div>
  );
}

// Single labeled dropdown for "Sort by:". Replaces the pill row, which
// read as data columns rather than controls. Uses the design-system
// Select so the menu styling, keyboard handling, and theming stay
// consistent with the rest of the app.
function SaSortDropdown({ value, onChange, options }) {
  return (
    <div className="sa-sortdrop">
      <span className="sa-sortdrop-label">Sort by</span>
      <Select
        value={value}
        onChange={(next) => onChange(next)}
        options={options}
        ariaLabel="Sort topics" />
    </div>
  );
}

function SaProvisionalBanner({ label, body }) {
  return (
    <div style={{
      background: 'rgba(255,184,77,0.10)',
      border: '1px solid var(--warn, #8A5A00)',
      padding: '10px 14px',
      marginBottom: 16,
      fontSize: 12.5,
      color: 'var(--ink-2, #2A2724)',
    }}>
      <span style={{
        fontFamily: 'var(--font-mono)',
        fontSize: 10,
        letterSpacing: '0.1em',
        textTransform: 'uppercase',
        color: 'var(--warn, #8A5A00)',
        fontWeight: 600,
        marginRight: 10,
      }}>{label}</span>
      {body}
    </div>
  );
}

function SaSortPills({ value, onChange, options }) {
  return (
    <div className="sa-sort">
      {options.map((o) => (
        <button key={o.value} className={value === o.value ? 'is-active' : ''} onClick={() => onChange(o.value)}>
          {o.label}
        </button>
      ))}
    </div>
  );
}

function SaTopicsBody({ topics, navigate, mcpServerId = null, analyticsQuery = '' }) {
  const [expandedId, setExpandedId] = useStateMa(null);
  if (topics.length === 0) {
    return <SaEmpty title="No topics yet" hint="The classification cron promotes clusters once at least 5 similar events accumulate." />;
  }
  return (
    <div className="sa-topic-list">
      {topics.map((t, i) => (
        <SaTopicRow
          key={t.id}
          rank={i + 1}
          topic={t}
          expanded={t.id === expandedId}
          onToggle={() => setExpandedId(t.id === expandedId ? null : t.id)}
          navigate={navigate}
          mcpServerId={mcpServerId}
          analyticsQuery={analyticsQuery} />
      ))}
    </div>
  );
}

function SaTopicRow({
  rank, topic, expanded, onToggle, navigate, mcpServerId = null, analyticsQuery = '',
}) {
  // Promoted topics get an inline detail panel (sample sessions +
  // stats + related searches). Raw rows have no detail
  // endpoint, so for them clicking the row navigates straight to the
  // Sessions table filtered by the exact intent — that's the
  // actionable thing a PM wants: "show me who's asking this and what
  // happened in their session". Either way clicking a row gets you
  // closer to the work, not into a dead end.
  const isPromoted = !!topic.id && !topic.is_raw;
  function handleRowClick() {
    if (isPromoted) {
      onToggle();
    } else {
      navigate(`/mcp-analytics/sessions${saBuildQuery(analyticsQuery, { intent: topic.label })}`);
    }
  }
  return (
    <div className={`sa-topic ${expanded ? 'expanded' : ''}`}>
      <div className="sa-topic-row" onClick={handleRowClick}>
        <div className="sa-topic-rank">{String(rank).padStart(2, '0')}</div>
        <div>
          <div className="sa-topic-title-row">
            <span className="sa-topic-title">{topic.label}</span>
            {topic.is_unmet_demand && <span className="sa-badge-demand">UNMET DEMAND</span>}
          </div>
          {/* Narrative sub-line: human-readable summary of what the
           * numbers mean. Reads like a sentence so a PM doesn't have
           * to translate the column data. */}
          <div className="sa-topic-narrative">{saTopicNarrative(topic)}</div>
          {topic.description && <div className="sa-topic-sample">{topic.description}</div>}
        </div>
        <SaFrustCell value={topic.peak_frustration} />
        <div className="sa-vol-cell">
          <div className="sa-vol-v">{saFormatNumber(topic.member_count)}</div>
        </div>
        <div className="sa-trend">{/* trend sparkline reserved */}</div>
        <div className="sa-caret">{isPromoted ? '›' : '→'}</div>
      </div>
      {expanded && isPromoted && (
        <SaTopicDetailInline
          topicId={topic.id}
          mcpServerId={mcpServerId}
          navigate={navigate} />
      )}
    </div>
  );
}

function SaFrustCell({ value }) {
  const v = value || 'low';
  return (
    <div className="sa-frust-cell">
      <span className={`sa-frust-bar ${v}`} />
      <span className={`sa-frust-text ${v}`}>{v.toUpperCase()}</span>
    </div>
  );
}

function SaTopicDetailInline({ topicId, mcpServerId = null, navigate }) {
  const serverParam = mcpServerId ? `?server=${encodeURIComponent(mcpServerId)}` : '';
  const { data, loading, error } = useApiResource(
    `/api/mcp-analytics/v2/topics/${topicId}${serverParam}`,
    [topicId, mcpServerId],
  );
  if (loading) return <div className="sa-topic-panel"><SaLoading /></div>;
  if (error) return <div className="sa-topic-panel"><SaError error={error} /></div>;
  if (!data) return null;
  return (
    <div className="sa-topic-panel">
      <div className="sa-panel-grid">
        <div className="sa-panel-card">
          <h4>Sample intents</h4>
          {(data.samples || []).map((s) => (
            <div key={s.event_id} className="sa-sample-intent" onClick={() => s.session_id && navigate(`/mcp-analytics/sessions/${s.session_id}`)}>
              {s.intent || '(no intent)'}
              <span className="sa-sess-ref">{saShortId(s.session_id)} · {saFormatRelative(s.started_at)}</span>
            </div>
          ))}
        </div>
        <div className="sa-panel-card">
          <h4>Stats</h4>
          <SaKv k="AVG CALLS" v={data.stats.avg_calls_per_session} />
          <SaKv k="AVG TIME" v={`${Math.round((data.stats.avg_duration_ms || 0) / 1000)}s`} />
          <SaKv k="SESSIONS" v={data.stats.unique_sessions} />
        </div>
        <div className="sa-panel-card">
          <h4>Related searches</h4>
          {(data.related_searches || []).map((r, i) => (
            <div key={i} className={`sa-related-search-row ${r.has_miss ? 'miss' : ''}`}>
              <span>&quot;{r.query}&quot;</span><span className="vol">{saFormatNumber(r.freq)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function SaKv({ k, v }) {
  return <div className="sa-kv"><span className="k">{k}</span><span className="v">{v}</span></div>;
}

// ────────────────────────────────────────────────────────────────────────
// SEARCHES PAGE
// ────────────────────────────────────────────────────────────────────────

function SaSearchesPage({
  range, onRangeChange, navigate, mcpServerId = null, serverControl = null, analyticsQuery = '',
}) {
  const serverParam = mcpServerId ? `&server=${encodeURIComponent(mcpServerId)}` : '';
  const { data, loading, error } = useApiResource(
    `/api/mcp-analytics/v2/searches?range=${range}${serverParam}`,
    [range, mcpServerId],
  );
  const [selectedId, setSelectedId] = useStateMa(null);
  useEffectMa(() => {
    const clusters = data?.clusters || [];
    if (!clusters.length) {
      if (selectedId !== null) setSelectedId(null);
      return;
    }
    if (!selectedId || !clusters.some((cluster) => cluster.id === selectedId)) {
      setSelectedId(clusters[0].id || null);
    }
  }, [data, selectedId]);
  return (
    <>
      <SaToolbar title="Misses" range={range} onRangeChange={onRangeChange} right={serverControl} />
      <div className="sa-purpose-strip">
        <span className="sa-purpose-eyebrow">What is this?</span>
        <span className="sa-purpose-body">
          Searches your agents ran that came back empty. Some point to a
          capability you haven't built; others are a tool you already have
          that the agent couldn't find by the name it searched. Either way
          it's a fix that makes your MCP easier to use.
        </span>
      </div>
      {loading && !data ? <SaLoading /> : error ? <SaError error={error} /> : (
        <SaSearchesBody
          data={data}
          range={range}
          mcpServerId={mcpServerId}
          selectedId={selectedId}
          setSelectedId={setSelectedId}
          navigate={navigate}
          analyticsQuery={analyticsQuery} />
      )}
    </>
  );
}

function SaSearchesBody({
  data, range, mcpServerId = null, selectedId, setSelectedId, navigate, analyticsQuery = '',
}) {
  const clusters = data?.clusters || [];
  const unclustered = data?.unclustered || [];
  if (clusters.length === 0 && unclustered.length === 0) {
    return <SaEmpty title="No search misses yet" hint="They appear when an agent searches your SDK and nothing comes back." />;
  }
  return (
    <>
      {clusters.length > 0 && (
        <div className="sa-twopane">
          <div className="sa-cluster-list">
            {clusters.map((c, i) => (
              <SaClusterRow
                key={c.id}
                num={i + 1}
                cluster={c}
                isSelected={c.id === selectedId}
                onClick={() => setSelectedId(c.id)} />
            ))}
          </div>
          {selectedId && (
            <SaClusterDetail
              clusterId={selectedId}
              range={range}
              mcpServerId={mcpServerId}
              navigate={navigate} />
          )}
        </div>
      )}
      {unclustered.length > 0 && (
        <div className="sa-ungrouped">
          <div className="sa-ungrouped-head">
            <span className="sa-ungrouped-title">Ungrouped misses</span>
            <span className="sa-ungrouped-hint">
              One row per exact query. The clustering job rolls similar terms
              together as volume builds; click any row to see the sessions
              that ran it.
            </span>
          </div>
          <div className="sa-cluster-list">
            {unclustered.map((c, i) => (
              <SaClusterRow
                key={`raw-${i}`}
                num={clusters.length + i + 1}
                cluster={c}
                isSelected={false}
                onClick={() => navigate(`/mcp-analytics/sessions${saBuildQuery(analyticsQuery, { search_miss: c.label })}`)} />
            ))}
          </div>
        </div>
      )}
    </>
  );
}

function SaClusterRow({ num, cluster, isSelected, onClick = undefined, interactive = true }) {
  return (
    <div
      className={`sa-cluster-row ${isSelected ? 'is-selected' : ''} ${interactive ? '' : 'is-static'}`}
      onClick={interactive ? onClick : undefined}>
      <div className="sa-cluster-num">{String(num).padStart(2, '0')}</div>
      <div className="sa-cluster-mark">⚠</div>
      <div>
        <div className="sa-cluster-name">{cluster.label}</div>
        {cluster.description && <div className="sa-cluster-sample">{cluster.description}</div>}
      </div>
      {/* Always render this cell (empty when ≤1) so the 6-column grid stays aligned. */}
      <div className="sa-cluster-meta">{cluster.unique_queries > 1 ? `${cluster.unique_queries} QUERIES` : ''}</div>
      <div className="sa-cluster-meta">{cluster.affected_events} SESS</div>
      <div className="sa-cluster-vol">
        <div className="v">{saFormatNumber(cluster.member_count)}</div>
      </div>
    </div>
  );
}

function SaClusterDetail({ clusterId, range, mcpServerId = null, navigate }) {
  const serverParam = mcpServerId ? `&server=${encodeURIComponent(mcpServerId)}` : '';
  const { data, loading, error } = useApiResource(
    `/api/mcp-analytics/v2/searches/${clusterId}?range=${range}${serverParam}`,
    [clusterId, range, mcpServerId],
  );
  if (loading) return <div className="sa-detail"><SaLoading /></div>;
  if (error) return <div className="sa-detail"><SaError error={error} /></div>;
  if (!data) return null;
  const cl = data.cluster;
  return (
    <div className="sa-detail">
      <div className="sa-detail-head">
        <div className="sa-detail-badge">SEARCH MISS</div>
        <h2>{cl.label}</h2>
        <div className="sa-detail-meta">{cl.member_count} EMPTY SEARCHES</div>
      </div>
      <div className="sa-detail-section">
        <h4>Searched for</h4>
        {(data.member_queries || []).map((m, i) => (
          <div key={i} className="sa-member-q">
            <span>&quot;{m.query}&quot;</span><span className="c">{m.count}</span>
          </div>
        ))}
      </div>
      <div className="sa-detail-section">
        <h4>Suggested fix</h4>
        {(cl.suggested_apis || []).length > 0 ? (
          (cl.suggested_apis || []).map((s, i) => (
            <div key={i} className="sa-suggestion">
              <div className="lbl">▌ SUGGESTED ENDPOINT</div>
              <div className="api">{s.verb} {s.path}</div>
              <div className="desc">{s.rationale}</div>
              {s.body_shape && <div className="desc"><code>{s.body_shape}</code></div>}
            </div>
          ))
        ) : (
          <SaEmpty title="No suggestion yet" hint="May be a missing capability — or a tool you have under a different name. Refreshes daily." />
        )}
      </div>
      <div className="sa-detail-section">
        <h4>Sample sessions</h4>
        {(data.sample_sessions || []).map((s) => (
          <div key={s.event_id} className="sa-sample-session" onClick={() => s.session_id && navigate(`/mcp-analytics/sessions/${s.session_id}`)}>
            <div>
              {s.missed_query && <div className="ss-query">searched &quot;{s.missed_query}&quot;</div>}
              <div className="ss-name">{s.intent ? `"${s.intent}"` : 'No declared intent'}</div>
              <div className="ss-meta-line">
                <span>{saFormatRelative(s.started_at).toUpperCase()}</span>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────
// SESSIONS PAGE
// ────────────────────────────────────────────────────────────────────────

function SaSessionsPage({
  range, onRangeChange, navigate, queryString = '', mcpServerId = null, serverControl = null,
}) {
  const [view, setView] = useStateMa('all');
  // The Topics page deep-links here with `?intent=<exact text>` to
  // narrow the table to one prompt's sessions. We surface the filter
  // as a visible chip the user can clear (then the URL also drops the
  // param so back/forward stays consistent).
  const intent = useMemoMa(() => {
    const sp = new URLSearchParams(queryString || '');
    return sp.get('intent');
  }, [queryString]);
  // The Gaps page deep-links here with `?search_miss=<query>` to show
  // the sessions that ran one failed search. Same clearable-chip
  // treatment as the intent filter.
  const searchMiss = useMemoMa(() => {
    const sp = new URLSearchParams(queryString || '');
    return sp.get('search_miss');
  }, [queryString]);
  function clearParam(key) {
    const sp = new URLSearchParams(queryString || '');
    sp.delete(key);
    const qs = sp.toString();
    navigate(`/mcp-analytics/sessions${qs ? `?${qs}` : ''}`);
  }
  const intentParam = intent ? `&intent=${encodeURIComponent(intent)}` : '';
  const searchMissParam = searchMiss ? `&search_miss=${encodeURIComponent(searchMiss)}` : '';
  const serverParam = mcpServerId ? `&server=${encodeURIComponent(mcpServerId)}` : '';
  // "Load more" grows the limit; useApiResource keeps the prior rows on
  // screen while the larger page loads, so the feed extends in place. Any
  // change of range/view/filter is a fresh query — collapse back to page one.
  const [limit, setLimit] = useStateMa(SA_SESSIONS_PAGE_SIZE);
  useEffectMa(() => {
    setLimit(SA_SESSIONS_PAGE_SIZE);
  }, [range, view, intent, searchMiss, mcpServerId]);
  const { data, loading, error } = useApiResource(
    `/api/mcp-analytics/v2/sessions?range=${range}&view=${view}${intentParam}${searchMissParam}${serverParam}&limit=${limit}`,
    [range, view, intent, searchMiss, mcpServerId, limit],
  );
  const loadedCount = data?.sessions?.length || 0;
  // A full page back means there may be more; once a short page returns (or
  // we reach the cap) the feed is exhausted for this view.
  const canLoadMore = loadedCount >= limit && limit < SA_SESSIONS_MAX;
  // Clicking "Load more" bumps `limit` past the still-stale `loadedCount`, so
  // `canLoadMore` flips false for the duration of the fetch and would unmount
  // the button. Track the in-flight click explicitly so the button stays put
  // showing "Loading…" until the larger page lands and clears on settle.
  const [loadingMore, setLoadingMore] = useStateMa(false);
  useEffectMa(() => {
    if (!loading) setLoadingMore(false);
  }, [loading]);
  return (
    <>
      <SaToolbar title="Activity" range={range} onRangeChange={onRangeChange} right={serverControl} />
      <div className="sa-purpose-strip">
        <span className="sa-purpose-eyebrow">What is this?</span>
        <span className="sa-purpose-body">
          Every conversation your MCP saw, newest first. Click into one
          to read the agent's reasoning step by step — search, script,
          upstream calls, results.
        </span>
      </div>
      {intent && (
        <div className="sa-intent-filter-chip">
          <span className="sa-intent-filter-label">Filtered by intent</span>
          <span className="sa-intent-filter-value">"{intent}"</span>
          <button type="button" className="sa-intent-filter-clear" onClick={() => clearParam('intent')} aria-label="Clear intent filter">×</button>
        </div>
      )}
      {searchMiss && (
        <div className="sa-intent-filter-chip">
          <span className="sa-intent-filter-label">Sessions that searched</span>
          <span className="sa-intent-filter-value">"{searchMiss}"</span>
          <button type="button" className="sa-intent-filter-clear" onClick={() => clearParam('search_miss')} aria-label="Clear search filter">×</button>
        </div>
      )}
      <div className="sa-layout-sessions">
        <SaSessionsSideRail counts={data?.counts} view={view} onView={setView} />
        <div className="sa-main-content">
          {loading && !data ? <SaLoading /> : error ? <SaError error={error} /> : (
            <>
              <SaSessionsTable sessions={data?.sessions || []} navigate={navigate} />
              {(canLoadMore || loadingMore) && (
                <div className="sa-load-more-row">
                  <button
                    type="button"
                    className="sa-load-more-btn"
                    disabled={loadingMore}
                    onClick={() => {
                      setLoadingMore(true);
                      setLimit((n) => Math.min(n + SA_SESSIONS_PAGE_SIZE, SA_SESSIONS_MAX));
                    }}
                  >
                    {loadingMore ? 'Loading…' : 'Load more'}
                  </button>
                </div>
              )}
            </>
          )}
        </div>
      </div>
    </>
  );
}

function SaSessionsSideRail({ counts, view, onView }) {
  const c = counts || {};
  function row(key, label, dot) {
    return (
      <div className={`sa-qf ${view === key ? 'is-active' : ''}`} onClick={() => onView(key)}>
        {dot && <span className={`sa-dot ${dot}`} />}<span>{label}</span>
        <span className="sa-count">{saFormatNumber(c[key] || 0)}</span>
      </div>
    );
  }
  // The "Saved" row is the user's personal list — not just another
  // filter. Pin it to its own section above the auto-filters with
  // the bookmark glyph and a brick-tinted card so it reads as "your
  // stuff", not "another query option".
  const savedCount = c.flagged || 0;
  return (
    <aside className="sa-secondary-rail">
      <div className={`sa-saved-card ${view === 'flagged' ? 'is-active' : ''}`} onClick={() => onView('flagged')}>
        <span className="sa-saved-icon"><SaBookmarkIcon filled size={14} /></span>
        <span className="sa-saved-label">Saved</span>
        <span className="sa-saved-count">{saFormatNumber(savedCount)}</span>
      </div>
      <div className="sa-secondary-group">
        <h4>Views</h4>
        {row('all', 'All', 'ok')}
        {row('high_frustration', 'High frust.', 'bad')}
        {row('repeat', 'Repeat', 'warn')}
      </div>
    </aside>
  );
}

function SaSessionsTable({ sessions, navigate }) {
  if (sessions.length === 0) {
    return <SaEmpty title="No sessions" hint="Try widening the range or switching views." />;
  }
  // Story-row layout. The intent is the headline; the sigil and short
  // hash are demoted to a corner. The PM scans by JOB, not by id.
  return (
    <div className="sa-storyfeed">
      {sessions.map((s) => (
        <SaSessionStory key={s.id} session={s} onClick={() => navigate(`/mcp-analytics/sessions/${s.id}`)} />
      ))}
    </div>
  );
}

// One-line narrative for a session row. Reads as story, not data.
function saSessionStoryLine(session) {
  const calls = session.event_count || 0;
  const searches = session.search_count || 0;
  const scripts = Math.max(0, calls - searches);
  const errs = session.error_count || 0;
  const bits = [];
  if (searches > 0) bits.push(`${searches} ${searches === 1 ? 'search' : 'searches'}`);
  if (scripts > 0) bits.push(`${scripts} ${scripts === 1 ? 'script' : 'scripts'}`);
  if (errs > 0) bits.push(`${errs} failed`);
  if (session.peak_frustration === 'high') bits.push('high frustration');
  return bits.join(' · ');
}

function SaSessionStory({ session, onClick }) {
  const intent = session.primary_intent?.label
    || session.inferred_user_goal
    || session.raw_intent
    || session.intent_summary
    || null;
  const harnessLabel = saHarnessLabel(session.client_name);
  const startedAt = new Date(session.started_at);
  const duration = session.last_event_at
    ? Math.round((new Date(session.last_event_at).getTime() - startedAt.getTime()) / 1000)
    : null;
  // No per-session sigil: the intent headline is the row's identity.
  // The session id is kept as a small sub-label for support/sharing.
  return (
    <div className="sa-story" onClick={onClick}>
      <div className="sa-story-body">
        <div className="sa-story-headline">
          {intent || <span className="sa-story-headline-empty">Session with no declared intent</span>}
        </div>
        <div className="sa-story-meta">
          <span>{saFormatRelative(session.last_event_at)}</span>
          {duration != null && <span>· {duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`}</span>}
          <span>· {saSessionStoryLine(session)}</span>
          {harnessLabel && <span className="sa-harness-chip">{harnessLabel}</span>}
        </div>
        <div className="sa-story-id">{saShortId(session.id)}</div>
      </div>
      <div className="sa-arrow">›</div>
    </div>
  );
}

function SaCallPills({ session }) {
  const total = session.event_count || 0;
  // We don't have per-event details in the row; approximate by mixing
  // search_count and ok/error counts.
  const searches = session.search_count || 0;
  const scripts = Math.max(0, total - searches);
  const errors = session.error_count || 0;
  const pills = [];
  for (let i = 0; i < searches && pills.length < 8; i++) {
    pills.push('search');
  }
  for (let i = 0; i < scripts && pills.length < 8; i++) {
    pills.push(i < errors ? 'exec-fail' : 'exec');
  }
  return (
    <div className="sa-col-calls">
      {pills.map((p, i) => (
        <span key={i} className={`sa-pill-tool sa-${p}`} />
      ))}
      <span className="total">{saFormatNumber(total)}</span>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────
// THINKING TRACE
// ────────────────────────────────────────────────────────────────────────

function SaThinkingTracePage({ sessionId, navigate }) {
  const { data, loading, error, reload } = useApiResource(
    `/api/mcp-analytics/v2/sessions/${sessionId}/trace`,
    [sessionId],
  );
  const toast = useToast?.();

  async function onFlag() {
    try {
      await apiFetch(`/api/mcp-analytics/v2/sessions/${sessionId}/flag`, {
        method: 'POST',
        body: JSON.stringify({ flagged: !(data?.session?.is_flagged) }),
        headers: { 'content-type': 'application/json' },
      });
      reload();
    } catch (err) {
      if (toast?.show) toast.show({ tone: 'error', message: 'Could not flag session' });
    }
  }

  return (
    <>
      <div className="sa-crumbs-bar">
        {/* Back arrow first — it's the dominant "get me out of here"
         * affordance. Replaces the old ✕ CLOSE on the right which
         * read as a destructive close-modal action. */}
        <button
          type="button"
          className="sa-back-btn"
          onClick={() => navigate('/mcp-analytics/sessions')}
          title="Back to Activity"
          aria-label="Back to Activity">
          ← Back
        </button>
        <div className="sa-crumbs">
          {/* Show the inferred intent so the trail describes what
           * you're looking at, not an opaque hash. */}
          <span className="current">{(() => {
            const intent = (data?.events || []).find((e) => e.metadata?.intent)?.metadata?.intent
              || data?.session?.inferred_user_goal
              || data?.session?.primary_intent?.label
              || data?.session?.raw_intent
              || data?.session?.intent_summary;
            const text = intent ? `"${intent}"` : saShortId(sessionId);
            return text.length > 80 ? `${text.slice(0, 78)}…` : text;
          })()}</span>
        </div>
        {/* Save toggle — writes `flagged_at` on the session row,
         * which powers the "Saved" filter on the Activity page side
         * rail. Bookmark glyph fills brick when active so the
         * current state is unambiguous. */}
        <button
          type="button"
          className={`sa-save-btn ${data?.session?.is_flagged ? 'is-saved' : ''}`}
          onClick={onFlag}
          title={data?.session?.is_flagged ? 'Remove from Saved (find under Activity → Saved)' : 'Save — find under Activity → Saved'}
          aria-pressed={!!data?.session?.is_flagged}>
          <SaBookmarkIcon filled={!!data?.session?.is_flagged} />
          {data?.session?.is_flagged ? 'Saved' : 'Save'}
        </button>
      </div>
      {loading && !data ? <SaLoading /> : error ? <SaError error={error} /> : data && (
        <SaThinkingTraceBody data={data} navigate={navigate} />
      )}
    </>
  );
}

function SaThinkingTraceBody({ data, navigate }) {
  const s = data.session;
  const events = data.events || [];
  const judgement = data.judgement || null;
  const peakLabel = (s.peak_frustration || '').toUpperCase();
  // Session duration: prefer the stored value, but fall back to
  // last_event_at - started_at because the stored column is updated
  // only when a session is explicitly ended (`ended_at`).
  const computedDurationMs = Number(s.duration_ms) > 0
    ? Number(s.duration_ms)
    : (s.last_event_at && s.started_at)
      ? Math.max(0, new Date(s.last_event_at).getTime() - new Date(s.started_at).getTime())
      : 0;
  // Lead with what the user was trying to do, not the database id.
  // The first @intent the agent declared in this session — walk
  // events in order and pick the first non-empty one. (Searches
  // don't carry @intent, so the title might come from the second
  // or third event, which is fine.)
  const firstIntent = (events.find((e) => e.metadata?.intent)?.metadata?.intent) || null;
  const sessionIntent = firstIntent
    || s.inferred_user_goal
    || s.primary_intent?.label
    || s.raw_intent
    || s.intent_summary
    || null;
  return (
    <div className="sa-trace-shell">
      <div className="sa-trace-main">
        <div className="sa-sess-header">
          <div>
            <div className="sa-sess-h-title">SESSION</div>
            <div className="sa-sess-h-id">
              {sessionIntent || <span style={{ color: 'var(--text-4, #8A857D)', fontStyle: 'italic' }}>No declared intent</span>}
            </div>
            {/* Started / duration / events / id live in the right-rail
             * Session card now — repeating them here was just noise. */}
          </div>
          <div className="sa-sess-h-pills">
            {judgement && <SaOutcomePill judgement={judgement} />}
            {peakLabel && <span className={`sa-pill ${peakLabel === 'HIGH' ? 'bad' : peakLabel === 'MEDIUM' ? 'warn' : 'ok'}`}>PEAK: {peakLabel}</span>}
          </div>
        </div>

        {s.inferred_user_goal && (
          <SaThoughtEvent
            label="USER GOAL · INFERRED"
            text={s.inferred_user_goal}
            timestamp={s.started_at} />
        )}

        {events.map((ev, idx) => (
          <SaTraceEvent key={ev.id} event={ev} prev={idx > 0 ? events[idx - 1] : null} sessionStart={s.started_at} />
        ))}
      </div>

      <div className="sa-trace-side">
        <SaTraceSidebar session={s} judgement={judgement} events={events} navigate={navigate} sessionId={s.id} />
      </div>
    </div>
  );
}

function SaThoughtEvent({ label, meta = null, text, timestamp, contextStr = null, frustration = null }) {
  // Context is the narrative ("WHY the agent is doing this") and
  // changes per step. Intent ("WHAT") is typically a short label
  // that repeats. Lead with context — fall back to intent only
  // when no context was declared.
  const headline = contextStr || text;
  const showIntentLine = !!contextStr && !!text && text !== contextStr;
  return (
    <div className="sa-event thought">
      <div className="sa-event-rail">
        <div className="sa-marker thought" />
        <div className="sa-ts">{timestamp ? new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }) : ''}</div>
      </div>
      <div className="sa-event-card">
        <div className="sa-thought-head">
          <span className="label">{label}</span>
          {meta && <span className="meta">{meta}</span>}
        </div>
        <div className="sa-thought-body">
          <div className="sa-thought-text">&quot;{headline}&quot;</div>
          {(showIntentLine || frustration) && (
            <div className="sa-thought-kv">
              {showIntentLine && (<><span className="k">Intent</span><span className="v">{text}</span></>)}
              {frustration && (<>
                <span className="k">Frustration</span>
                <span className="v"><span className={`sa-frust-tag ${frustration}`}>{frustration.toUpperCase()}</span></span>
              </>)}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function SaTraceEvent({ event, prev, sessionStart }) {
  if (event.kind === 'search_sdk_standalone') {
    return <SaSearchEvent event={event} sessionStart={sessionStart} prev={prev} />;
  }
  // Work events emit any embedded search_calls first, then the declared context,
  // then the concrete tool card.
  return (
    <>
      {(event.search_calls || []).map((sc, i) => (
        <SaSearchInline key={`${event.id}-sc-${i}`} sc={sc} when={event.started_at} />
      ))}
      {(event.metadata?.intent || event.metadata?.context) && (
        <SaThoughtEvent
          label={event.metadata?.context ? 'AGENT CONTEXT' : 'AGENT INTENT'}
          text={event.metadata.intent}
          contextStr={event.metadata.context}
          frustration={event.metadata.frustration_level || event.classification?.inferred_frustration}
          timestamp={event.started_at} />
      )}
      {event.kind === 'tool_call'
        ? <SaToolCallEvent event={event} />
        : <SaExecuteEvent event={event} />}
    </>
  );
}

function SaSearchEvent({ event, sessionStart = null, prev = null }) {
  void sessionStart; void prev;
  const sc = (event.search_calls || [])[0] || {};
  const isMiss = !!sc.is_miss || (event.metadata?.is_miss === true);
  return (
    <div className="sa-event search" id={`sa-event-${event.id}`} data-event-id={event.id}>
      <div className="sa-event-rail">
        <div className="sa-marker search" />
        <div className="sa-ts">{new Date(event.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}</div>
      </div>
      <div className="sa-event-card">
        <div className={`sa-search-head ${isMiss ? 'is-miss' : ''}`}>
          <span className="sa-tool-chip">SEARCH_SDK</span>
          <span className="sa-search-q">&quot;{sc.query || event.metadata?.query || '(unknown)'}&quot;</span>
          <span className="sa-search-status">{isMiss ? '⚠ 0 MATCHES' : `${sc.match_count || 0} MATCHES`}</span>
        </div>
      </div>
    </div>
  );
}

function SaSearchInline({ sc, when = null }) {
  void when;
  const isMiss = !!sc.is_miss;
  return (
    <div className="sa-event search">
      <div className="sa-event-rail"><div className="sa-marker search" /></div>
      <div className="sa-event-card">
        <div className={`sa-search-head ${isMiss ? 'is-miss' : ''}`}>
          <span className="sa-tool-chip">SEARCH_SDK</span>
          <span className="sa-search-q">&quot;{sc.query}&quot;</span>
          <span className="sa-search-status">{isMiss ? '⚠ 0 MATCHES' : `${sc.match_count || 0} MATCHES`}</span>
        </div>
      </div>
    </div>
  );
}

// Single collapsible section inside an execute_script card. All four
// detail blocks (script source, upstream calls, result, error) use
// the same toggle pattern so the card reads as one stack of "click
// to see" rows. Collapsed by default — the @context above already
// tells you WHY; these blocks are for when you want the receipts.
function SaTraceCollapse({ openLabel, closedLabel, meta = null, tone = 'default', defaultOpen = false, children }) {
  const [open, setOpen] = useStateMa(defaultOpen);
  return (
    <div className={`sa-trace-collapse ${tone !== 'default' ? `tone-${tone}` : ''}`}>
      <button
        type="button"
        className="sa-script-toggle"
        onClick={() => setOpen((v) => !v)}
        aria-expanded={open}>
        {open ? '▾' : '▸'} {open ? openLabel : closedLabel}
        {meta && <span className="sa-script-toggle-meta">{meta}</span>}
      </button>
      {open && <div className="sa-trace-collapse-body">{children}</div>}
    </div>
  );
}

function SaExecuteEvent({ event }) {
  const ok = event.ok && !event.error_text;
  const calls = event.calls || [];
  const maxDur = Math.max(1, ...calls.map((c) => c.duration_ms || 0));
  const scriptLineCount = event.script_source
    ? event.script_source.split('\n').length
    : 0;
  const resultSize = event.result_preview ? event.result_preview.length : 0;
  const okCalls = calls.filter((c) => c.status >= 200 && c.status < 400).length;
  const errCalls = calls.length - okCalls;
  return (
    <div className="sa-event execute" id={`sa-event-${event.id}`} data-event-id={event.id}>
      <div className="sa-event-rail">
        <div className="sa-marker execute" />
        <div className="sa-ts">{new Date(event.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}</div>
      </div>
      <div className="sa-event-card">
        <div className="sa-exec-head">
          <span className="sa-tool-chip">EXECUTE_SCRIPT</span>
          <span className="sa-exec-summary">
            <span>{calls.length} API CALLS</span>
            <span>{event.duration_ms ?? 0} MS</span>
            <span><span className={`sa-dot-inline ${ok ? 'ok' : 'fail'}`} />{ok ? 'OK' : 'FAILED'}</span>
          </span>
        </div>

        {event.script_source && (
          <SaTraceCollapse
            openLabel="Hide script"
            closedLabel="Show script"
            meta={`${scriptLineCount} ${scriptLineCount === 1 ? 'line' : 'lines'}`}>
            <pre className="sa-code-block">{event.script_source}</pre>
          </SaTraceCollapse>
        )}

        {calls.length > 0 && (
          <SaTraceCollapse
            openLabel="Hide upstream API calls"
            closedLabel="Show upstream API calls"
            meta={errCalls > 0 ? `${calls.length} · ${errCalls} failed` : `${calls.length}`}>
            <div className="sa-subcalls">
              {calls.map((c, i) => (
                <div key={i} className="sa-subcall">
                  <span className="method">{(c.shape || '').split(' ')[0] || c.method || '?'}</span>
                  <span className="path">{(c.shape || '').split(' ').slice(1).join(' ') || c.operation_id || '?'}</span>
                  <span className="bar"><span style={{ width: `${((c.duration_ms || 0) / maxDur) * 100}%` }} /></span>
                  <span className="ms">{c.duration_ms || 0} ms</span>
                  <span className={`status ${(c.status >= 200 && c.status < 400) ? 'ok' : 'bad'}`}>{c.status || '—'}</span>
                </div>
              ))}
            </div>
          </SaTraceCollapse>
        )}

        {event.result_preview && (
          <SaTraceCollapse
            openLabel="Hide result"
            closedLabel={!ok ? 'Show result (failed)' : 'Show result'}
            meta={`${resultSize.toLocaleString()} ${resultSize === 1 ? 'char' : 'chars'}`}
            tone={!ok ? 'bad' : 'default'}>
            <div className={`sa-result-block ${!ok ? 'fail' : ''}`}>
              <div className="body">{event.result_preview.slice(0, 600)}</div>
            </div>
          </SaTraceCollapse>
        )}

        {event.error_text && (
          <SaTraceCollapse
            openLabel="Hide error"
            closedLabel="Show error"
            tone="bad">
            <div className="sa-result-block fail">
              <div className="body">{event.error_text}</div>
            </div>
          </SaTraceCollapse>
        )}
      </div>
    </div>
  );
}

function saToolCallLabel(event) {
  const raw = event.metadata?.tool_name || 'tool_call';
  const label = String(raw)
    .trim()
    .replace(/[^a-zA-Z0-9]+/g, '_')
    .replace(/^_+|_+$/g, '')
    .toUpperCase();
  return label || 'TOOL_CALL';
}

function SaToolCallEvent({ event }) {
  const ok = event.ok && !event.error_text;
  const calls = event.calls || [];
  const maxDur = Math.max(1, ...calls.map((c) => c.duration_ms || 0));
  const inputPreview = event.metadata?.input_preview || event.script_source || null;
  const inputSize = inputPreview ? String(inputPreview).length : 0;
  const resultSize = event.result_preview ? event.result_preview.length : 0;
  const okCalls = calls.filter((c) => c.status >= 200 && c.status < 400).length;
  const errCalls = calls.length - okCalls;
  return (
    <div className="sa-event execute" id={`sa-event-${event.id}`} data-event-id={event.id}>
      <div className="sa-event-rail">
        <div className="sa-marker execute" />
        <div className="sa-ts">{new Date(event.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })}</div>
      </div>
      <div className="sa-event-card">
        <div className="sa-exec-head">
          <span className="sa-tool-chip">{saToolCallLabel(event)}</span>
          <span className="sa-exec-summary">
            {calls.length > 0 && <span>{calls.length} API CALLS</span>}
            <span>{event.duration_ms ?? 0} MS</span>
            <span><span className={`sa-dot-inline ${ok ? 'ok' : 'fail'}`} />{ok ? 'OK' : 'FAILED'}</span>
          </span>
        </div>

        {inputPreview && (
          <SaTraceCollapse
            openLabel="Hide input"
            closedLabel="Show input"
            meta={`${inputSize.toLocaleString()} ${inputSize === 1 ? 'char' : 'chars'}`}>
            <pre className="sa-code-block">{String(inputPreview)}</pre>
          </SaTraceCollapse>
        )}

        {calls.length > 0 && (
          <SaTraceCollapse
            openLabel="Hide upstream API calls"
            closedLabel="Show upstream API calls"
            meta={errCalls > 0 ? `${calls.length} · ${errCalls} failed` : `${calls.length}`}>
            <div className="sa-subcalls">
              {calls.map((c, i) => (
                <div key={i} className="sa-subcall">
                  <span className="method">{(c.shape || '').split(' ')[0] || c.method || '?'}</span>
                  <span className="path">{(c.shape || '').split(' ').slice(1).join(' ') || c.operation_id || '?'}</span>
                  <span className="bar"><span style={{ width: `${((c.duration_ms || 0) / maxDur) * 100}%` }} /></span>
                  <span className="ms">{c.duration_ms || 0} ms</span>
                  <span className={`status ${(c.status >= 200 && c.status < 400) ? 'ok' : 'bad'}`}>{c.status || '-'}</span>
                </div>
              ))}
            </div>
          </SaTraceCollapse>
        )}

        {event.result_preview && (
          <SaTraceCollapse
            openLabel="Hide result"
            closedLabel={!ok ? 'Show result (failed)' : 'Show result'}
            meta={`${resultSize.toLocaleString()} ${resultSize === 1 ? 'char' : 'chars'}`}
            tone={!ok ? 'bad' : 'default'}>
            <div className={`sa-result-block ${!ok ? 'fail' : ''}`}>
              <div className="body">{event.result_preview.slice(0, 600)}</div>
            </div>
          </SaTraceCollapse>
        )}

        {event.error_text && (
          <SaTraceCollapse
            openLabel="Hide error"
            closedLabel="Show error"
            tone="bad">
            <div className="sa-result-block fail">
              <div className="body">{event.error_text}</div>
            </div>
          </SaTraceCollapse>
        )}
      </div>
    </div>
  );
}

function SaOutcomePill({ judgement }) {
  const outcome = String(judgement?.outcome_label || '').toUpperCase();
  const tone = judgement?.outcome_label === 'success'
    ? 'ok'
    : judgement?.outcome_label === 'partial'
      ? 'warn'
      : 'bad';
  return (
    <span className={`sa-pill sa-outcome-pill ${tone}`}>
      {outcome} {saFormatPercent(judgement?.success_score)}
    </span>
  );
}

function SaTraceSidebar({ session, judgement = null, events = [], sessionId, navigate }) {
  const { data: similar } = useApiResource(`/api/mcp-analytics/v2/sessions/${sessionId}/similar`, [sessionId]);
  const harnessLabel = saHarnessLabel(session.client_name);
  const clientValue = harnessLabel
    ? `${harnessLabel}${session.client_version ? ` ${session.client_version}` : ''}`
    : 'Unknown';
  return (
    <>
      {judgement ? <SaOutcomeCard judgement={judgement} events={events} /> : <SaOutcomePendingCard />}
      <div className="sa-side-card">
        <div className="head"><span className="ttl">Session</span></div>
        <div className="body">
          <SaKv k="Started" v={new Date(session.started_at).toLocaleString()} />
          <SaKv k="Duration" v={saFormatDurationMs(
            Number(session.duration_ms) > 0
              ? Number(session.duration_ms)
              : (session.last_event_at && session.started_at)
                ? Math.max(0, new Date(session.last_event_at).getTime() - new Date(session.started_at).getTime())
                : 0
          )} />
          <SaKv k="Events" v={session.event_count} />
          <SaKv k="Searches" v={session.search_count} />
          <SaKv k="API calls" v={Math.max(0, (session.event_count || 0) - (session.search_count || 0))} />
          <SaKv k="Client" v={clientValue} />
        </div>
      </div>
      {similar?.sessions?.length > 0 && (
        <div className="sa-side-card">
          <div className="head"><span className="ttl">Similar sessions</span></div>
          <div className="body">
            {similar.sessions.map((rel) => (
              <div key={rel.id} className="sa-related-row" onClick={() => navigate(`/mcp-analytics/sessions/${rel.id}`)}>
                <Sigil seed={rel.id} size={24} />
                <div>
                  <div className="when">{saFormatRelative(rel.started_at).toUpperCase()} · {rel.event_count} EVENTS</div>
                  <div className="what">&quot;{rel.first_intent || '(no intent)'}&quot;</div>
                  <div className="res">{(rel.error_count || 0) > 0 ? '⚠ ERRORS' : 'OK'}</div>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </>
  );
}

function SaOutcomePendingCard() {
  return (
    <div className="sa-side-card sa-outcome-card">
      <div className="head"><span className="ttl">Outcome</span></div>
      <div className="body">
        <SaKv k="Status" v="Not judged" />
        <div className="sa-outcome-summary">
          No completed session judgement exists for this trace yet.
        </div>
      </div>
    </div>
  );
}

function saEventEvidenceLabel(event) {
  if (!event) return 'Event';
  const time = event.started_at
    ? new Date(event.started_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })
    : '';
  let label = '';
  if (event.kind === 'search_sdk_standalone') {
    const search = (event.search_calls || [])[0] || {};
    label = search.query || event.metadata?.query || 'Search SDK';
  } else if (event.kind === 'tool_call') {
    label = saToolCallLabel(event).replace(/_/g, ' ');
  } else {
    label = 'Execute script';
  }
  return [time, saHumanLabel(label)].filter(Boolean).join(' ');
}

function saEventEvidenceTitle(event) {
  if (!event) return 'Scroll to evidence event';
  const context = event.metadata?.context || event.metadata?.intent || '';
  if (context) return context;
  if (event.error_text) return event.error_text;
  return 'Scroll to evidence event';
}

function SaOutcomeCard({ judgement, events }) {
  const eventById = new Map((events || []).map((event) => [event.id, event]));
  const evidence = (judgement.evidence_event_ids || [])
    .map((id) => eventById.get(id))
    .filter(Boolean);
  const tools = judgement.tool_involvement || {};
  const toolSummary = `${tools.work_event_count ?? 0} events · ${tools.upstream_api_call_count ?? 0} API calls · ${tools.upstream_api_failure_count ?? 0} failed`;
  const failureLabels = judgement.failure_reason_labels || [];
  const frictionLabels = judgement.friction_labels || [];
  function scrollToEvent(id) {
    const el = document.getElementById(`sa-event-${id}`);
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
  }
  return (
    <div className="sa-side-card sa-outcome-card">
      <div className="head"><span className="ttl">Outcome</span></div>
      <div className="body">
        <SaKv k="Outcome" v={saHumanLabel(judgement.outcome_label)} />
        <SaKv k="Success" v={saFormatPercent(judgement.success_score)} />
        <SaKv k="Quality" v={saFormatPercent(judgement.quality_score)} />
        <SaKv k="Confidence" v={saConfidenceLabel(judgement.confidence)} />
        <SaKv k="Tools" v={toolSummary} />
        {failureLabels.length > 0 && (
          <div className="sa-label-group">
            <div className="k">Failures</div>
            <div className="v">
              {failureLabels.map((label) => <span key={label} className="sa-judge-label bad">{saHumanLabel(label)}</span>)}
            </div>
          </div>
        )}
        {frictionLabels.length > 0 && (
          <div className="sa-label-group">
            <div className="k">Friction</div>
            <div className="v">
              {frictionLabels.map((label) => <span key={label} className="sa-judge-label">{saHumanLabel(label)}</span>)}
            </div>
          </div>
        )}
        {judgement.summary && <div className="sa-outcome-summary">{judgement.summary}</div>}
        {evidence.length > 0 && (
          <div className="sa-evidence-links">
            <div className="k">Evidence</div>
            <div className="v">
              {evidence.map((event) => (
                <button
                  key={event.id}
                  type="button"
                  title={saEventEvidenceTitle(event)}
                  onClick={() => scrollToEvent(event.id)}>
                  {saEventEvidenceLabel(event)}
                </button>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────────
// ROOT — McpAnalyticsPage (rendered by app.jsx)
// ────────────────────────────────────────────────────────────────────────

function McpAnalyticsPage({ navigate, queryString = '', tab, sessionId }) {
  const auth = useAuth?.() || {};
  const query = useMemoMa(() => new URLSearchParams(queryString || ''), [queryString]);
  const range = useMemoMa(() => {
    const r = query.get('range');
    return SA_RANGES.includes(r) ? r : '7d';
  }, [query]);
  const selectedServerId = useMemoMa(() => {
    const server = query.get('server') || query.get('mcp_server_id');
    return server && server !== 'all' ? server : 'all';
  }, [query]);

  const activeKey = sessionId ? 'sessions' : saNormaliseTab(tab);
  const serversResource = useApiResource('/api/mcp-analytics/servers');
  const serverControl = (
    <SaServerFilter
      servers={serversResource.data?.servers || []}
      value={selectedServerId}
      loading={serversResource.loading}
      onChange={(nextServer) => {
        const params = new URLSearchParams(queryString);
        params.delete('mcp_server_id');
        if (nextServer && nextServer !== 'all') params.set('server', nextServer);
        else params.delete('server');
        const qs = params.toString();
        navigate(`/mcp-analytics/${activeKey}${qs ? `?${qs}` : ''}`);
      }} />
  );
  const analyticsQuery = useMemoMa(() => {
    const params = new URLSearchParams();
    const explicitRange = query.get('range');
    if (SA_RANGES.includes(explicitRange)) params.set('range', explicitRange);
    if (selectedServerId !== 'all') params.set('server', selectedServerId);
    const qs = params.toString();
    return qs ? `?${qs}` : '';
  }, [query, selectedServerId]);
  const mcpServerId = selectedServerId === 'all' ? null : selectedServerId;

  const setRange = useCallbackMa((nextRange) => {
    const params = new URLSearchParams(queryString);
    params.set('range', nextRange);
    navigate(`/mcp-analytics/${activeKey}${params.toString() ? `?${params.toString()}` : ''}`);
  }, [navigate, queryString, activeKey]);

  let page;
  if (sessionId) {
    page = <SaThinkingTracePage sessionId={sessionId} navigate={navigate} />;
  } else if (activeKey === 'topics') {
    page = (
      <SaTopicsPage
        range={range}
        onRangeChange={setRange}
        navigate={navigate}
        mcpServerId={mcpServerId}
        serverControl={serverControl}
        analyticsQuery={analyticsQuery} />
    );
  } else if (activeKey === 'searches') {
    page = (
      <SaSearchesPage
        range={range}
        onRangeChange={setRange}
        navigate={navigate}
        mcpServerId={mcpServerId}
        serverControl={serverControl}
        analyticsQuery={analyticsQuery} />
    );
  } else if (activeKey === 'sessions') {
    page = (
      <SaSessionsPage
        range={range}
        onRangeChange={setRange}
        navigate={navigate}
        queryString={queryString}
        mcpServerId={mcpServerId}
        serverControl={serverControl} />
    );
  } else {
    page = (
      <SaOverviewPage
        range={range}
        onRangeChange={setRange}
        navigate={navigate}
        mcpServerId={mcpServerId}
        serverControl={serverControl}
        analyticsQuery={analyticsQuery} />
    );
  }

  // Session Analytics shares the global Armature shell (sidebar lives
  // in shell.jsx, app-grouped). The page renders straight into the
  // shell's .main > .page slot — the `sa-page` class scopes the v2
  // brutalist treatment to this surface only.
  return <div className="sa-page">{page}</div>;
}

// Expose globally for the Babel/script-tag loader.
window.McpAnalyticsPage = McpAnalyticsPage;
window.Sigil = Sigil;
