/* global React, Button, Icon, useApiResource, apiFetch, useAuth, useToast */

// ============================================================
// /armature-mcp — the install + connect + manage hub for Armature's
// own remote MCP server. Ported from the Connect-your-editor
// design bundle (docs/superpowers/specs/2026-05-15-…).
// ============================================================

const {
  useEffect: useEffectOMCP,
  useMemo: useMemoOMCP,
  useRef: useRefOMCP,
  useState: useStateOMCP,
} = React;

const MCP_URL = 'https://mcp.armature.tech/mcp';

// Inline Eyebrow component — the production repo uses .eyebrow as a class
// rather than a component. The /armature-mcp page reads better with a component
// wrapper because the eyebrow appears in many places.
function Eyebrow({ children }) {
  return <span className="eyebrow">{children}</span>;
}

// ---- Client matrix --------------------------------------------------------
// kind drives the install-snippet shape:
//   'deep' — primary "Install in X" deep-link button + secondary CLI fallback
//   'cli'  — single shell command
//   'json' — JSON config snippet (optionally with config-file paths)
//   'web'  — URL + Authorization header as separate copyable fields
const CLIENTS = [
  { id: 'claude-code',    label: 'Claude Code',    kind: 'cli',  logo: '/frontend/assets/logos/claude-code.png' },
  { id: 'cursor',         label: 'Cursor',         kind: 'deep', logo: '/frontend/assets/logos/cursor.png' },
  { id: 'vscode',         label: 'VS Code',        kind: 'deep', logo: '/frontend/assets/logos/vscode.png' },
  { id: 'claude-desktop', label: 'Claude Desktop', kind: 'json', logo: '/frontend/assets/logos/anthropic.svg.png' },
  { id: 'codex',          label: 'Codex',          kind: 'cli',  logo: '/frontend/assets/logos/codex.png' },
  { id: 'chatgpt',        label: 'ChatGPT',        kind: 'web',  logo: '/frontend/assets/logos/chatgpt.png' },
  { id: 'opencode',       label: 'opencode',       kind: 'cli',  logo: '/frontend/assets/logos/opencode.svg' },
  { id: 'openclaw',       label: 'openclaw',       kind: 'cli',  logo: '/frontend/assets/logos/openclaw.svg' },
  { id: 'other',          label: 'Other',          kind: 'json', logo: null, monogram: '{ }', isOther: true },
];

const HERO_CLIENT_LOGOS = CLIENTS.filter((client) => client.logo && !client.isOther);

// ---- Sticky prefs ---------------------------------------------------------
const LS_KEY = 'armature.ourmcp.prefs.v1';
function loadPrefs() {
  try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch { return {}; }
}
function savePrefs(p) {
  try { localStorage.setItem(LS_KEY, JSON.stringify(p)); } catch { /* noop */ }
}

// ---- Use-case cards (capabilities tab #1) --------------------------------
const USE_CASES = [
  {
    id: 'triage',
    eyebrow: 'Triage',
    title: 'Find out why a workflow is failing.',
    body: 'Pull the trace, isolate the failing span, and compare it to the last green run from your MCP client.',
    tools: ['search_runs', 'inspect_run', 'diagnose_run', 'compare_runs', 'get_org_health'],
    prompt: 'The insforge auth-redirect workflow started failing in the last hour. Pull the most recent failed run, diagnose it, and compare it to yesterday’s baseline.',
  },
  {
    id: 'repair',
    eyebrow: 'Repair',
    title: 'Patch a workflow and ship the fix.',
    body: 'Stage a patch from a failing run, review the diff, apply with optimistic concurrency, and re-run — all approval-gated.',
    tools: ['propose_workflow_patch', 'draft_regression_workflow_from_run', 'apply_workflow_change', 'run_workflow_now'],
    prompt: 'Patch the workflow for that failing run so it refreshes the session before calling users.get, draft a regression for the failure mode, then re-run.',
  },
  {
    id: 'design',
    eyebrow: 'Design',
    title: 'Add coverage for a new MCP server.',
    body: 'Read a target’s tool catalog, see what isn’t tested, and stage workflow proposals you can review in the dashboard.',
    tools: ['list_mcp_servers', 'list_cli_targets', 'get_workflows', 'sync_mcp_server_capabilities', 'propose_workflow_create'],
    prompt: 'We just added the Linear MCP server. Sync its capabilities, then propose three smoke workflows that cover its most-called tools.',
  },
  {
    id: 'investigate',
    eyebrow: 'Investigate',
    title: 'See yesterday in one digest.',
    body: 'Pull today’s Insights digest — or any day’s — so your agent can summarise pass-rate, top regressions, and noisy tools.',
    tools: ['get_insight_digest', 'ensure_insight_digest'],
    prompt: 'Summarise yesterday’s Insights digest for our standup. Lead with anything that regressed by more than 5 points.',
  },
];

// ============================================================
// Sub-components
// ============================================================

function ConnectionStatus({ connected, count, loading }) {
  const statusText = loading
    ? 'Loading...'
    : connected
      ? `Connected · ${count} ${count === 1 ? 'credential' : 'credentials'}`
      : 'Not connected';
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center', gap: 10,
      padding: '8px 12px',
      border: '1px solid var(--ink)',
      background: connected ? 'var(--surface)' : 'var(--surface-2)',
      minHeight: 36,
    }}>
      <span style={{
        width: 8, height: 8, borderRadius: '50%',
        background: loading ? 'var(--text-4)' : connected ? 'var(--green)' : 'var(--text-4)',
        animation: !loading && connected ? 'pulse 1.4s ease-in-out infinite' : 'none',
      }} />
      <div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.15 }}>
        <span style={{
          fontFamily: 'var(--font-mono)', fontSize: 10, textTransform: 'uppercase',
          letterSpacing: '0.06em', color: 'var(--text-3)',
        }}>Status</span>
        <span style={{ fontSize: 12.5, fontWeight: 500, whiteSpace: 'nowrap' }}>
          {statusText}
        </span>
      </div>
    </div>
  );
}

function HeroLede({ status }) {
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto',
      gap: 24, alignItems: 'flex-end', marginBottom: 22,
    }}>
      <div style={{ minWidth: 0 }}>
        <h1 className="page-title" style={{ marginTop: 0, fontSize: 36, lineHeight: 1.02 }}>
          Connect Armature MCP - let your agent fix all your findings
        </h1>
        <div style={{
          fontSize: 14.5, color: 'var(--text-2)', maxWidth: 720, marginTop: 10, lineHeight: 1.5,
          display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
        }}>
          <span>Connect your MCP client to Armature so your agent can fix findings in place.</span>
          <span aria-label="Supported clients" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
            {HERO_CLIENT_LOGOS.map((client) => (
              <img
                key={client.id}
                src={client.logo}
                alt={client.label}
                title={client.label}
                style={{ width: 15, height: 15, objectFit: 'contain', display: 'block' }} />
            ))}
          </span>
        </div>
      </div>
      <ConnectionStatus connected={status.connected} count={status.activeKeyCount} loading={status.loading} />
    </div>
  );
}

// ---- Auth mode segmented selector ----------------------------------------
function AuthModeToggle({ value, onChange, oauthEnabled }) {
  const opts = [
    { id: 'oauth', label: 'OAuth',
      sublabel: oauthEnabled ? 'recommended' : 'rolling out',
      preview: !oauthEnabled },
    { id: 'apikey', label: 'API key', sublabel: 'works today', preview: false },
  ];
  return (
    <div style={{ display: 'inline-flex', border: '1px solid var(--ink)', background: 'var(--surface)' }}>
      {opts.map((o, i) => {
        const active = value === o.id;
        return (
          <button
            key={o.id}
            onClick={() => onChange(o.id)}
            style={{
              padding: '8px 14px', display: 'flex', flexDirection: 'column', alignItems: 'flex-start',
              gap: 1, border: 0, background: active ? 'var(--ink)' : 'transparent',
              color: active ? 'var(--on-ink)' : 'var(--text-2)',
              borderLeft: i > 0 ? '1px solid var(--ink)' : 0,
              minWidth: 132, textAlign: 'left', cursor: 'pointer',
              fontFamily: 'var(--font-sans)', whiteSpace: 'nowrap',
            }}>
            <span style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 500, whiteSpace: 'nowrap' }}>
              {o.label}
              {o.preview && (
                <span style={{
                  fontFamily: 'var(--font-mono)', fontSize: 9, padding: '1px 5px',
                  border: '1px solid currentColor', letterSpacing: '0.06em',
                  textTransform: 'uppercase', opacity: 0.7,
                }}>preview</span>
              )}
            </span>
            <span style={{
              fontFamily: 'var(--font-mono)', fontSize: 10, textTransform: 'uppercase',
              letterSpacing: '0.06em', opacity: active ? 0.7 : 0.6, whiteSpace: 'nowrap',
            }}>{o.sublabel}</span>
          </button>
        );
      })}
    </div>
  );
}

// ---- Client mosaic --------------------------------------------------------
function ClientMark({ c, active }) {
  return (
    <div style={{
      width: 44, height: 44,
      border: '1px solid ' + (active ? 'var(--ink)' : 'var(--border)'),
      background: active ? 'var(--ink)' : 'var(--surface)',
      display: 'grid', placeItems: 'center', flexShrink: 0,
      transition: 'background 160ms var(--ease-out), border-color 160ms var(--ease-out)',
    }}>
      {c.logo ? (
        <img src={c.logo} alt="" style={{
          width: 26, height: 26, objectFit: 'contain',
          filter: active ? 'invert(1) brightness(1.2)' : 'none',
        }} />
      ) : (
        <span style={{
          fontFamily: 'var(--font-display)',
          fontSize: c.monogram && c.monogram.length > 2 ? 13 : 14,
          fontWeight: 500, letterSpacing: '-0.01em',
          color: active ? 'var(--on-ink)' : 'var(--text-2)',
        }}>{c.monogram}</span>
      )}
    </div>
  );
}

function ClientPicker({ value, onChange }) {
  const rows = Math.ceil(CLIENTS.length / 3);
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
      gap: 0, border: '1px solid var(--ink)', background: 'var(--surface)',
    }}>
      {CLIENTS.map((c, i) => {
        const active = value === c.id;
        const col = i % 3, row = Math.floor(i / 3);
        return (
          <button
            key={c.id}
            onClick={() => onChange(c.id)}
            style={{
              display: 'grid', gridTemplateColumns: '44px minmax(0, 1fr)', alignItems: 'center',
              gap: 12, padding: '14px 14px',
              border: 0,
              borderRight:  col < 2 ? '1px solid var(--border)' : 0,
              borderBottom: row < rows - 1 ? '1px solid var(--border)' : 0,
              background: active ? 'var(--brand-soft)' : 'transparent',
              cursor: 'pointer', textAlign: 'left',
              transition: 'background 160ms var(--ease-out)',
            }}>
            <ClientMark c={c} active={active} />
            <div style={{ minWidth: 0 }}>
              <div style={{
                fontSize: 13.5, fontWeight: 500, color: 'var(--text)',
                display: 'flex', alignItems: 'center', gap: 6,
              }}>
                {c.label}
                {active && <span style={{ width: 5, height: 5, background: 'var(--brand)', display: 'inline-block' }} />}
              </div>
              <div style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-3)', marginTop: 1 }}>
                {c.kind === 'deep' ? 'one-click install'
                 : c.kind === 'cli' ? 'shell command'
                 : c.kind === 'json' ? 'config snippet'
                 : 'web connector'}
              </div>
            </div>
          </button>
        );
      })}
    </div>
  );
}

// ---- Install snippet builders --------------------------------------------
function buildSnippet({ client, authMode, oauthEnabled, apiKey }) {
  // When the org has OAuth enabled AND the user picked the OAuth tab, drop
  // the Authorization header from every snippet. MCP-capable clients negotiate
  // OAuth themselves against /api/mcp/oauth/authorize after their first call
  // to /api/mcp returns 401 with a WWW-Authenticate challenge.
  const useOauth = authMode === 'oauth' && oauthEnabled;
  const token = apiKey || '<YOUR_TOKEN>';
  const concreteAuthHeaderObj = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
  const authHeaderValue = useOauth ? null : `Bearer ${token}`;
  const authHeaderObj = useOauth ? {} : { Authorization: `Bearer ${token}` };
  const authHeaderFlag = useOauth ? '' : ` --header "Authorization: Bearer ${token}"`;

  switch (client) {
    case 'claude-code':
      return {
        shape: 'cli',
        cmd: useOauth
          ? `claude mcp add \\
  --transport http \\
  armature ${MCP_URL}`
          : `claude mcp add \\
  --transport http \\
  --header "Authorization: Bearer ${token}" \\
  armature ${MCP_URL}`,
      };
    case 'cursor': {
      // Cursor's MCP install deep link expects a base64-encoded JSON config
      // matching the mcp.json server entry, NOT a bare ?url= parameter.
      // See https://docs.cursor.com/en/context/mcp#install-mcp-server
      const cursorConfig = {
        url: MCP_URL,
        // Deep-link installers do not expose an editable config surface before
        // launch. Never pass the placeholder token into the external app; omit
        // headers so the client can use OAuth discovery instead.
        ...(useOauth || !apiKey ? {} : { headers: concreteAuthHeaderObj }),
      };
      const cursorConfigB64 = btoa(JSON.stringify(cursorConfig));
      return {
        shape: 'deep',
        deepLabel: 'Install in Cursor',
        deepHref: `cursor://anysphere.cursor-deeplink/mcp/install?name=armature&config=${encodeURIComponent(cursorConfigB64)}`,
        cmd: `cursor mcp add armature ${MCP_URL}${authHeaderFlag}`,
      };
    }
    case 'vscode': {
      // VS Code's MCP install handler does `JSON.parse(decodeURIComponent(uri.query))`
      // on the entire query string — there is no named param. See
      // microsoft/vscode src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts
      // (handleMcpInstallUri). The encoded JSON IS the query string; using a
      // named param like `?config=...` would make the handler's JSON.parse
      // fail. Reviewers occasionally flag this; keep the inline reference.
      const vscodeConfig = {
        name: 'armature',
        type: 'http',
        url: MCP_URL,
        ...(useOauth || !apiKey ? {} : { headers: concreteAuthHeaderObj }),
      };
      return {
        shape: 'deep',
        deepLabel: 'Install in VS Code',
        deepHref: `vscode:mcp/install?${encodeURIComponent(JSON.stringify(vscodeConfig))}`,
        cmd: `code --add-mcp '${JSON.stringify(vscodeConfig)}'`,
        note: useOauth
          ? 'This adds the server config. VS Code starts OAuth when it starts the Armature MCP server.'
          : null,
      };
    }
    case 'codex':
      return {
        shape: 'cli',
        cmd: useOauth
          ? `codex mcp add armature --url ${MCP_URL}
codex mcp login armature`
          : `export ARMATURE_MCP_TOKEN=${token}
codex mcp add armature \\
  --url ${MCP_URL} \\
  --bearer-token-env-var ARMATURE_MCP_TOKEN`,
        note: useOauth
          ? 'The first command adds the server. The second command opens the OAuth browser flow. If Armature was already added with an older command, remove it first with codex mcp remove armature.'
          : 'Codex stores the environment variable name, not the token value. Keep ARMATURE_MCP_TOKEN available when Codex starts. If Armature was already added with an older command, remove it first with codex mcp remove armature.',
      };
    case 'opencode':
      return {
        shape: 'cli',
        cmd: useOauth
          ? `opencode mcp add armature ${MCP_URL}`
          : `opencode mcp add armature ${MCP_URL} \\
  --auth "Bearer ${token}"`,
      };
    case 'openclaw':
      return {
        shape: 'cli',
        cmd: useOauth
          ? `openclaw connect armature \\
  --url ${MCP_URL}`
          : `openclaw connect armature \\
  --url ${MCP_URL} \\
  --bearer ${token}`,
      };
    case 'claude-desktop':
      return {
        shape: 'json',
        configPaths: {
          macos:   '~/Library/Application Support/Claude/claude_desktop_config.json',
          windows: '%APPDATA%\\Claude\\claude_desktop_config.json',
        },
        restart: 'Quit and reopen Claude Desktop after saving.',
        json: JSON.stringify({
          mcpServers: {
            armature: {
              transport: 'http',
              url: MCP_URL,
              ...(useOauth ? {} : { headers: authHeaderObj }),
            },
          },
        }, null, 2),
      };
    case 'chatgpt':
      return {
        shape: 'web',
        url: MCP_URL,
        header: authHeaderValue,
        steps: [
          'Open ChatGPT → Settings → Connectors → Advanced settings.',
          'Turn on Developer mode.',
          'Click Create app.',
          useOauth
            ? 'Set MCP Server URL to the Armature MCP URL, set Authentication to OAuth, then choose I understand and want to continue.'
            : 'Paste the URL and the Authorization header into the matching fields.',
          'Create the app, connect it, then start a new chat with the Armature connector enabled.',
        ],
      };
    case 'other':
    default:
      return {
        shape: 'json',
        json: JSON.stringify({
          name: 'armature',
          transport: 'http',
          url: MCP_URL,
          ...(useOauth ? {} : { headers: authHeaderObj }),
        }, null, 2),
      };
  }
}

// ---- Snippet renderers ----------------------------------------------------
function CodeBlock({ children, label }) {
  const ref = useRefOMCP(null);
  const [copied, setCopied] = useStateOMCP(false);
  const copy = () => {
    const t = ref.current?.textContent || '';
    if (navigator.clipboard?.writeText) navigator.clipboard.writeText(t).catch(() => {});
    setCopied(true);
    setTimeout(() => setCopied(false), 1200);
  };
  return (
    <div style={{ position: 'relative' }}>
      {label && (
        <div style={{
          fontFamily: 'var(--font-mono)', fontSize: 10, textTransform: 'uppercase',
          letterSpacing: '0.08em', color: 'var(--text-3)', marginBottom: 6,
        }}>{label}</div>
      )}
      <pre ref={ref} style={{
        margin: 0, padding: '14px 56px 14px 14px',
        background: 'var(--ink)', color: 'var(--on-ink)',
        fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.55,
        whiteSpace: 'pre-wrap', wordBreak: 'break-word',
        border: '1px solid var(--ink)',
      }}>{children}</pre>
      <button onClick={copy} style={{
        position: 'absolute', top: label ? 24 : 8, right: 8,
        padding: '4px 9px', background: 'rgba(255,255,255,0.08)',
        border: '1px solid rgba(255,255,255,0.18)', color: 'var(--on-ink)',
        fontFamily: 'var(--font-mono)', fontSize: 10.5, fontWeight: 500,
        textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer',
      }}>{copied ? 'Copied' : 'Copy'}</button>
    </div>
  );
}

function FieldCopy({ label, value }) {
  const [copied, setCopied] = useStateOMCP(false);
  return (
    <div>
      <div style={{
        fontFamily: 'var(--font-mono)', fontSize: 10, textTransform: 'uppercase',
        letterSpacing: '0.06em', color: 'var(--text-3)', marginBottom: 5,
      }}>{label}</div>
      <div style={{ display: 'flex', border: '1px solid var(--ink)', background: 'var(--surface)' }}>
        <code style={{
          flex: 1, padding: '7px 10px',
          fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--text)',
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
        }}>{value}</code>
        <button onClick={() => {
          if (navigator.clipboard?.writeText) navigator.clipboard.writeText(value).catch(() => {});
          setCopied(true);
          setTimeout(() => setCopied(false), 1200);
        }} style={{
          padding: '0 12px', border: 0, borderLeft: '1px solid var(--ink)',
          background: 'var(--surface-2)', color: 'var(--text-2)',
          fontFamily: 'var(--font-mono)', fontSize: 10.5, fontWeight: 500,
          textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer',
        }}>{copied ? '✓' : 'Copy'}</button>
      </div>
    </div>
  );
}

function InstallContent({ client, authMode, apiKey, hasKey, oauthEnabled, onGenerateKey, toast }) {
  const s = buildSnippet({ client, authMode, oauthEnabled, apiKey });
  const [deepLinkAttempted, setDeepLinkAttempted] = useStateOMCP(false);
  // API-key tab always needs a token. OAuth tab also needs one until the org
  // flag flips ON, because snippets fall back to API-key shape in that case.
  const oauthIsLive = authMode === 'oauth' && oauthEnabled;
  const showTokenHelp = !hasKey && !oauthIsLive;

  useEffectOMCP(() => {
    setDeepLinkAttempted(false);
  }, [s.deepHref]);

  function handleDeepLinkClick() {
    if (s.shape !== 'deep') return;
    setDeepLinkAttempted(true);
    if (toast) {
      toast.show({
        tone: 'ok',
        title: `Opening ${s.deepLabel.replace(/^Install in\s+/i, '')}`,
        description: 'If nothing opens, the app may not be installed. Use the terminal command below.',
      });
    }
  }

  return (
    <div>
      {/* OAuth → API-key fallback note, only when the org doesn't have OAuth yet */}
      {authMode === 'oauth' && !oauthEnabled && (
        <div style={{
          display: 'flex', alignItems: 'flex-start', gap: 8,
          padding: '8px 12px', marginBottom: 14,
          background: 'var(--amber-soft)', border: '1px solid var(--amber-soft-border)',
          fontSize: 12, color: 'var(--amber)',
        }}>
          <Icon name="info" size={13} className="" />
          <div>
            <b style={{ fontWeight: 600 }}>OAuth is in preview.</b> Snippets fall back to API-key
            shape while we finish provider review. Switch to API key to dismiss this.
          </div>
        </div>
      )}

      {/* Deep-link */}
      {s.shape === 'deep' && (
        <>
          <div style={{ display: 'flex', gap: 10, alignItems: 'center', marginBottom: 16 }}>
            <a
              href={s.deepHref}
              className="btn btn-primary"
              onClick={handleDeepLinkClick}
              style={{ padding: '10px 18px', fontSize: 13 }}>
              <Icon name="download" size={13} />
              {s.deepLabel}
            </a>
            <span
              role={deepLinkAttempted ? 'status' : undefined}
              style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-3)' }}>
              {deepLinkAttempted
                ? 'If nothing opened, use the terminal command below.'
                : 'Opens your MCP client via deep link.'}
            </span>
          </div>
          <CodeBlock label="or run from your terminal">{s.cmd}</CodeBlock>
          {s.note && (
            <div style={{ marginTop: 8, fontSize: 12, lineHeight: 1.45, color: 'var(--text-2)' }}>
              {s.note}
            </div>
          )}
        </>
      )}

      {/* CLI */}
      {s.shape === 'cli' && (
        <>
          <CodeBlock label="run in your terminal">{s.cmd}</CodeBlock>
          {s.note && (
            <div style={{ marginTop: 8, fontSize: 12, lineHeight: 1.45, color: 'var(--text-2)' }}>
              {s.note}
            </div>
          )}
        </>
      )}

      {/* JSON config */}
      {s.shape === 'json' && (
        <>
          <CodeBlock label="paste into your config">{s.json}</CodeBlock>
          {s.configPaths && (
            <div style={{ marginTop: 14, fontSize: 12, color: 'var(--text-2)', lineHeight: 1.55 }}>
              <div style={{ display: 'grid', gridTemplateColumns: '70px 1fr', gap: '4px 12px' }}>
                <span style={{
                  fontFamily: 'var(--font-mono)', fontSize: 10.5, textTransform: 'uppercase',
                  letterSpacing: '0.06em', color: 'var(--text-3)',
                }}>macOS</span>
                <code style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5 }}>{s.configPaths.macos}</code>
                <span style={{
                  fontFamily: 'var(--font-mono)', fontSize: 10.5, textTransform: 'uppercase',
                  letterSpacing: '0.06em', color: 'var(--text-3)',
                }}>Windows</span>
                <code style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5 }}>{s.configPaths.windows}</code>
              </div>
              {s.restart && (
                <div style={{
                  marginTop: 10, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-3)',
                }}>
                  ↻ {s.restart}
                </div>
              )}
            </div>
          )}
        </>
      )}

      {/* Web (ChatGPT) */}
      {s.shape === 'web' && (
        <>
          <div style={{
            display: 'grid',
            // Drop the second column when OAuth is enabled — there's no
            // Authorization header to surface; ChatGPT handles it via OAuth.
            gridTemplateColumns: s.header
              ? 'minmax(0, 1fr) minmax(0, 1fr)'
              : 'minmax(0, 1fr)',
            gap: 12, marginBottom: 16,
          }}>
            <FieldCopy label="Server URL" value={s.url} />
            {s.header && (
              <FieldCopy label="Authorization header" value={s.header} />
            )}
          </div>
          <ol style={{ margin: 0, paddingLeft: 18, color: 'var(--text-2)', fontSize: 13, lineHeight: 1.65 }}>
            {s.steps.map((step, i) => <li key={i} style={{ paddingLeft: 4 }}>{step}</li>)}
          </ol>
          <div style={{ marginTop: 14 }}>
            <a className="btn btn-sm" href="https://chatgpt.com/#settings/Connectors" target="_blank" rel="noopener noreferrer">
              Open ChatGPT connectors <Icon name="chevronRight" size={11} />
            </a>
          </div>
        </>
      )}

      {/* Token help — only when there's no API key */}
      {showTokenHelp && (
        <div style={{
          marginTop: 18, padding: '12px 14px',
          background: 'var(--surface-2)', border: '1px dashed var(--border-strong)',
          display: 'flex', alignItems: 'center', gap: 12,
        }}>
          <Icon name="key" size={16} className="" />
          <div style={{ flex: 1, fontSize: 12.5, color: 'var(--text-2)', lineHeight: 1.5 }}>
            <b style={{ fontWeight: 600 }}>You need an Armature API key</b> to finish this install.
            The snippet shows <code style={{ fontFamily: 'var(--font-mono)' }}>&lt;YOUR_TOKEN&gt;</code> until one exists.
          </div>
          <Button onClick={onGenerateKey} size="sm" variant="primary">Generate key</Button>
        </div>
      )}
    </div>
  );
}

// ---- Install panel — the offset double-border window from the design -----
function InstallPanel({ children, client }) {
  const clientLabel = (CLIENTS.find(c => c.id === client) || {}).label || client;
  const url = `armature.tech / armature-mcp · ${clientLabel.toLowerCase()}`;
  return (
    <div className="window" style={{ marginRight: 6, marginBottom: 6 }}>
      <div className="window-head">
        <span style={{ display: 'inline-flex', gap: 5 }}>
          <i style={{ width: 9, height: 9, border: '1px solid var(--ink)', display: 'inline-block' }} />
          <i style={{ width: 9, height: 9, border: '1px solid var(--ink)', display: 'inline-block' }} />
          <i style={{ width: 9, height: 9, border: '1px solid var(--ink)', display: 'inline-block' }} />
        </span>
        <span style={{ color: 'var(--text-2)' }}>{url}</span>
        <span style={{ marginLeft: 'auto', color: 'var(--text-3)' }}>INSTALL</span>
      </div>
      <div className="window-body" style={{ padding: '22px 22px' }}>{children}</div>
    </div>
  );
}

// ---- Connections list -----------------------------------------------------
// `items` is a merged list of API keys and OAuth grants, each row carrying:
//   { id, kind: 'api_key' | 'oauth', name, tokenPrefix, createdAt, lastUsedAt }
function ConnectionsList({ items, loading, onRevokeGrant, revokingId, navigate }) {
  // Distinguish loading from empty so the user doesn't get a misleading
  // "No active credentials" message while the first fetch is still in flight.
  if (loading) {
    return (
      <div style={{
        padding: '24px 18px', border: '1px dashed var(--border)',
        background: 'var(--surface-2)', textAlign: 'center', color: 'var(--text-3)',
        fontSize: 12.5,
      }}>
        Loading credentials…
      </div>
    );
  }
  if (!items.length) {
    return (
      <div style={{
        padding: '24px 18px', border: '1px dashed var(--border-strong)',
        background: 'var(--surface-2)', textAlign: 'center', color: 'var(--text-3)',
      }}>
        <div style={{ fontSize: 13, color: 'var(--text-2)', fontWeight: 500, marginBottom: 4 }}>
          No active credentials.
        </div>
        <div style={{ fontSize: 12 }}>
          Follow the install above — your first key is created in the same flow.
        </div>
      </div>
    );
  }
  return (
    <div className="table-wrap">
      <table className="table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Type</th>
            <th>Token</th>
            <th>Created</th>
            <th>Last used</th>
            <th />
          </tr>
        </thead>
        <tbody>
          {items.map(k => (
            <tr key={`${k.kind}:${k.id}`}>
              <td><b>{k.name || (k.kind === 'oauth' ? 'OAuth client' : 'Unnamed key')}</b></td>
              <td className="muted" style={{ fontFamily: 'var(--font-mono)', fontSize: 11 }}>
                {k.kind === 'oauth' ? 'OAuth' : 'API key'}
              </td>
              <td className="num">{k.tokenPrefix || '—'}</td>
              <td className="num muted">{k.createdAt ? formatShortDate(k.createdAt) : '—'}</td>
              <td className="num muted">{k.lastUsedAt ? formatRelative(k.lastUsedAt) : 'Never'}</td>
              <td>
                {k.kind === 'oauth' ? (
                  // Inline revoke for OAuth grants — calls DELETE /api/mcp/oauth/grants/:id
                  <button
                    onClick={() => onRevokeGrant && onRevokeGrant(k.id)}
                    disabled={revokingId === k.id}
                    style={{
                      background: 'transparent', border: 0,
                      fontFamily: 'var(--font-mono)', fontSize: 10.5,
                      color: revokingId === k.id ? 'var(--text-3)' : 'var(--red)',
                      textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 500,
                      cursor: revokingId === k.id ? 'wait' : 'pointer', padding: 0,
                    }}>
                    {revokingId === k.id ? 'Revoking…' : 'Revoke'}
                  </button>
                ) : (
                  // API keys aren't revocable from this page — the action
                  // lives in Settings → API keys. Label the link as a
                  // navigation, not a destructive verb.
                  <a
                    href="/settings/api-keys"
                    onClick={(e) => { e.preventDefault(); if (navigate) navigate('/settings/api-keys'); }}
                    style={{
                      fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-3)',
                      textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 500,
                    }}>
                    Manage →
                  </a>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function formatShortDate(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return '—';
  try {
    return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
  } catch { return '—'; }
}
function formatRelative(iso) {
  if (!iso) return '—';
  const t = new Date(iso).getTime();
  if (!Number.isFinite(t)) return '—';
  const diff = Date.now() - t;
  if (diff < 0) return 'just now';
  if (diff < 60_000) return 'just now';
  if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m ago`;
  if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h ago`;
  return `${Math.round(diff / 86_400_000)}d ago`;
}

// ---- Capabilities tabs ----------------------------------------------------
function MiniCopy({ value }) {
  const [copied, setCopied] = useStateOMCP(false);
  return (
    <button onClick={(e) => {
      e.stopPropagation();
      if (navigator.clipboard?.writeText) navigator.clipboard.writeText(value).catch(() => {});
      setCopied(true);
      setTimeout(() => setCopied(false), 1200);
    }} style={{
      padding: '3px 8px', background: 'transparent',
      border: '1px solid rgba(255,255,255,0.3)', color: 'rgba(255,255,255,0.85)',
      fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 500,
      textTransform: 'uppercase', letterSpacing: '0.06em', cursor: 'pointer',
      flexShrink: 0, whiteSpace: 'nowrap',
    }}>{copied ? '✓ Copied' : 'Copy'}</button>
  );
}

function UseCaseCard({ uc }) {
  return (
    <div style={{
      background: 'var(--surface)', border: '1px solid var(--border-strong)',
      display: 'flex', flexDirection: 'column', minHeight: 196,
    }}>
      <div style={{ padding: '16px 18px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
        <Eyebrow>{uc.eyebrow}</Eyebrow>
        <h3 style={{
          fontFamily: 'var(--font-display)', fontSize: 17, fontWeight: 500,
          letterSpacing: '-0.02em', lineHeight: 1.25, margin: 0, color: 'var(--text)',
        }}>{uc.title}</h3>
        <p style={{ margin: 0, fontSize: 13, lineHeight: 1.5, color: 'var(--text-2)' }}>
          {uc.body}
        </p>
      </div>
      <div style={{
        margin: 'auto 18px 18px',
        padding: '12px 14px',
        background: 'var(--ink)', color: 'var(--on-ink)',
        display: 'flex', alignItems: 'flex-start', gap: 12,
      }}>
        <div style={{
          flex: 1, minWidth: 0,
          fontSize: 12.5, lineHeight: 1.55,
          color: 'rgba(255,255,255,0.92)', fontStyle: 'italic',
        }}>“{uc.prompt}”</div>
        <MiniCopy value={uc.prompt} />
      </div>
    </div>
  );
}

function RegistryRow({ name, desc, last }) {
  return (
    <div
      style={{
        display: 'grid', gridTemplateColumns: 'minmax(220px, 280px) minmax(0, 1fr)',
        gap: 24, padding: '12px 18px',
        borderBottom: last ? 0 : '1px solid var(--border-soft)',
        transition: 'background 90ms var(--ease-out)',
      }}
      onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--surface-2)'; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
      <code style={{
        fontFamily: 'var(--font-mono)', fontSize: 12.5, fontWeight: 500,
        color: 'var(--text)', overflowWrap: 'anywhere',
      }}>{name}</code>
      <span style={{ fontSize: 13, lineHeight: 1.5, color: 'var(--text-2)' }}>{desc}</span>
    </div>
  );
}

function CapabilitiesTabs({ manifest }) {
  const [tab, setTab] = useStateOMCP('use-cases');
  const tools = manifest?.tools || [];
  const prompts = manifest?.prompts || [];
  const tabs = [
    {
      id: 'use-cases', label: 'Use cases', count: USE_CASES.length,
      blurb: 'Four examples once Armature is connected. Copy a starter prompt to try it.',
      source: 'examples',
    },
    {
      id: 'tools', label: 'Tools', count: tools.length,
      blurb: 'Functions your MCP client can call to investigate runs, repair workflows, and dispatch validations.',
      source: '/api/mcp/manifest',
    },
    {
      id: 'prompts', label: 'Prompts', count: prompts.length,
      blurb: 'Pre-written templates that combine tools into a complete agent loop — invokable as “/prompt-name” in your client.',
      source: '/api/mcp/manifest',
    },
  ];
  const active = tabs.find(t => t.id === tab);
  const rows = tab === 'tools' ? tools : tab === 'prompts' ? prompts : [];

  return (
    <div style={{ background: 'var(--surface)', border: '1px solid var(--ink)' }}>
      {/* Tab strip */}
      <div style={{ display: 'flex', borderBottom: '1px solid var(--ink)', background: 'var(--surface-2)' }}>
        {tabs.map(t => {
          const isActive = t.id === tab;
          return (
            <button key={t.id} onClick={() => setTab(t.id)} style={{
              padding: '14px 22px',
              border: 0, background: isActive ? 'var(--surface)' : 'transparent',
              borderRight: '1px solid var(--ink)',
              borderBottom: isActive ? '1px solid var(--surface)' : 0,
              marginBottom: isActive ? -1 : 0,
              cursor: 'pointer',
              display: 'flex', alignItems: 'baseline', gap: 10,
              position: 'relative', zIndex: isActive ? 1 : 0,
            }}>
              <span style={{
                fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 500,
                textTransform: 'uppercase', letterSpacing: '0.08em',
                color: isActive ? 'var(--text)' : 'var(--text-3)',
                whiteSpace: 'nowrap',
              }}>{t.label}</span>
              <span style={{
                fontFamily: 'var(--font-mono)', fontSize: 12, fontVariantNumeric: 'tabular-nums',
                color: isActive ? 'var(--brand)' : 'var(--text-3)', fontWeight: 500,
              }}>{t.count}</span>
              {isActive && (
                <span style={{
                  position: 'absolute', left: 22, right: 22, top: 0,
                  height: 2, background: 'var(--brand)',
                }} />
              )}
            </button>
          );
        })}
      </div>
      {/* Sub-header */}
      <div style={{
        padding: '12px 18px 14px',
        borderBottom: '1px solid var(--border)',
        background: 'var(--surface)',
        display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16,
      }}>
        <span style={{ fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.5, maxWidth: 620 }}>
          {active.blurb}
        </span>
        <span style={{
          fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-3)',
          textTransform: 'uppercase', letterSpacing: '0.06em', whiteSpace: 'nowrap',
        }}>{active.source}</span>
      </div>
      {/* Content */}
      {tab === 'use-cases' ? (
        <div style={{
          padding: 18, display: 'grid',
          gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 14,
        }}>
          {USE_CASES.map(uc => <UseCaseCard key={uc.id} uc={uc} />)}
        </div>
      ) : (
        <div>
          {rows.length === 0 ? (
            <div style={{ padding: '24px 18px', color: 'var(--text-3)', fontSize: 12.5, textAlign: 'center' }}>
              Loading…
            </div>
          ) : rows.map((it, i) => (
            <RegistryRow
              key={it.name}
              name={it.name}
              desc={it.description || ''}
              last={i === rows.length - 1} />
          ))}
        </div>
      )}
    </div>
  );
}

// ============================================================
// Main page
// ============================================================
function OurMcpPage({ navigate, queryString: _queryString = '' }) {
  const auth = useAuth();
  const oauthEnabled = Boolean(auth?.me?.featureFlags?.mcpOauthEnabled);

  const prefs = useMemoOMCP(() => loadPrefs(), []);
  // Default to OAuth when the org has it enabled; fall back to API key
  // otherwise so the user lands on a working install path.
  const [authMode, setAuthMode] = useStateOMCP(
    prefs.authMode || (oauthEnabled ? 'oauth' : 'apikey'),
  );
  const [client, setClient] = useStateOMCP(prefs.client || 'claude-code');
  const [revokingGrantId, setRevokingGrantId] = useStateOMCP(null);
  const toast = (typeof useToast === 'function') ? useToast() : null;

  useEffectOMCP(() => { savePrefs({ authMode, client }); }, [authMode, client]);
  // If the org flag flips off mid-session, snap back to API key so we never
  // show an OAuth install snippet the org can't actually complete.
  useEffectOMCP(() => {
    if (!oauthEnabled && authMode === 'oauth') setAuthMode('apikey');
  }, [oauthEnabled, authMode]);

  const apiKeys = useApiResource('/api/settings/api-keys');
  const manifest = useApiResource('/api/mcp/manifest');
  // OAuth grants endpoint exists once main's MCP-OAuth PR is merged in.
  // The endpoint 403s for non-editor roles — useApiResource degrades to
  // an empty rows array, which is fine for the read-only connection list.
  const oauthGrants = useApiResource(oauthEnabled ? '/api/mcp/oauth/grants' : null);

  const activeKeys = apiKeys.data?.rows || [];
  const activeGrants = (oauthGrants.data?.rows || []).filter((g) => !g.revokedAt);
  const hasKey = activeKeys.length > 0;
  // Hero pill counts what the user can SEE in the connections table below.
  // /api/me/our-mcp-status applies the same user-scoping but we don't fetch
  // it here — the table data is already in flight via /api/settings/api-keys,
  // and reading both would just duplicate the SQL.
  const visibleCount = activeKeys.length + activeGrants.length;
  const connected = visibleCount > 0;
  const toolCount = manifest.data?.tools?.length || 16;

  // For snippet rendering we never inject the real secret — tokens are only
  // shown once at creation in /settings/api-keys. The snippet shows the
  // <YOUR_TOKEN> placeholder until the user pastes their token in.
  const apiKey = null;

  function handleGenerateKey() {
    if (typeof navigate === 'function') navigate('/settings/api-keys');
  }

  // Merge API keys + OAuth grants into a single connection list, newest first.
  const connectionItems = useMemoOMCP(() => {
    const keys = activeKeys.map((k) => ({
      kind: 'api_key',
      id: k.id,
      name: k.name,
      tokenPrefix: k.tokenPrefix,
      createdAt: k.createdAt,
      lastUsedAt: k.lastUsedAt,
    }));
    const grants = activeGrants.map((g) => ({
      kind: 'oauth',
      id: g.id,
      name: g.clientName,
      // OAuth grants don't have a safe-to-display token prefix; the spec only
      // emits the bound client_id. The human-readable client name lives in
      // the Name column — surface the client_id as the token prefix here.
      tokenPrefix: g.clientId,
      createdAt: g.createdAt,
      lastUsedAt: g.lastUsedAt,
    }));
    return [...keys, ...grants].sort((a, b) => {
      const ta = a.createdAt ? new Date(a.createdAt).getTime() : 0;
      const tb = b.createdAt ? new Date(b.createdAt).getTime() : 0;
      return tb - ta;
    });
  }, [activeKeys, activeGrants]);

  async function handleRevokeGrant(grantId) {
    if (!grantId) return;
    if (typeof window !== 'undefined' && !window.confirm('Revoke this OAuth grant?')) return;
    setRevokingGrantId(grantId);
    let revokeError = null;
    try {
      await apiFetch(`/api/mcp/oauth/grants/${grantId}`, { method: 'DELETE' });
    } catch (error) {
      revokeError = error;
    }
    setRevokingGrantId(null);
    // ALWAYS reload — on success the grant disappears; on failure the table
    // re-syncs with the server so we don't leave a stale "deleted but still
    // visible" row behind.
    if (oauthGrants.reload) oauthGrants.reload();
    if (revokeError) {
      console.error('Failed to revoke OAuth grant', revokeError);
      if (toast) {
        toast.show({
          tone: 'bad',
          title: 'Revoke failed',
          description: revokeError.message || 'The OAuth grant could not be revoked. Try again.',
        });
      } else if (typeof window !== 'undefined' && typeof window.alert === 'function') {
        window.alert(`Revoke failed: ${revokeError.message || revokeError}`);
      }
    } else if (toast) {
      toast.show({ tone: 'ok', title: 'OAuth grant revoked' });
    }
  }

  return (
    <div className="page-inner">
      <HeroLede
        status={{
          connected,
          activeKeyCount: visibleCount,
          loading: apiKeys.loading || (oauthEnabled && oauthGrants.loading),
        }}
        />

      {/* Picker row — auth mode + client mosaic */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 14, marginBottom: 18 }}>
        <div style={{
          display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
          gap: 16, flexWrap: 'wrap',
        }}>
          <div>
            <Eyebrow>1 · Auth mode</Eyebrow>
            <div style={{ marginTop: 8 }}>
              <AuthModeToggle value={authMode} onChange={setAuthMode} oauthEnabled={oauthEnabled} />
            </div>
          </div>
        </div>
        <div>
          <Eyebrow>2 · AI client</Eyebrow>
          <div style={{ marginTop: 8 }}>
            <ClientPicker value={client} onChange={setClient} />
          </div>
        </div>
      </div>

      {/* Install */}
      <div style={{ marginBottom: 36, marginTop: 8 }}>
        <Eyebrow>3 · Install</Eyebrow>
        <div style={{ marginTop: 12 }}>
          <InstallPanel client={client}>
            <InstallContent
              client={client}
              authMode={authMode}
              apiKey={apiKey}
              hasKey={hasKey}
              oauthEnabled={oauthEnabled}
              onGenerateKey={handleGenerateKey}
              toast={toast} />
          </InstallPanel>
        </div>
      </div>

      {/* Connections — merge API keys and OAuth grants into one table */}
      <div style={{ marginBottom: 36 }}>
        <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', marginBottom: 12 }}>
          <div>
            <Eyebrow>Active credentials · {connectionItems.length}</Eyebrow>
            <h2 style={{
              fontFamily: 'var(--font-display)', fontSize: 20, fontWeight: 500,
              letterSpacing: '-0.02em', lineHeight: 1.15, margin: '6px 0 0',
            }}>What’s connected</h2>
          </div>
          <a href="/settings/api-keys" style={{
            fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-3)',
            textTransform: 'uppercase', letterSpacing: '0.06em',
          }} onClick={(e) => { e.preventDefault(); if (navigate) navigate('/settings/api-keys'); }}>
            Manage in settings →
          </a>
        </div>
        <ConnectionsList
          items={connectionItems}
          loading={apiKeys.loading || (oauthEnabled && oauthGrants.loading)}
          onRevokeGrant={handleRevokeGrant}
          revokingId={revokingGrantId}
          navigate={navigate} />
      </div>

      {/* Capabilities */}
      <div style={{ marginBottom: 36 }}>
        <div style={{ marginBottom: 12 }}>
          <Eyebrow>What your agent can do now on Armature</Eyebrow>
        </div>
        <CapabilitiesTabs manifest={manifest.data} />
      </div>
    </div>
  );
}

window.OurMcpPage = OurMcpPage;
