/* API key management panel for the Pass Studio hub.
   Operators generate per-program keys here; their store backend uses those keys to call
   POST /v1/enroll/session and auto-enroll logged-in customers without a form. */

const KEYS_ENDPOINT = () => (window.SpoonityAuth && window.SpoonityAuth.getEndpoint
  ? window.SpoonityAuth.getEndpoint()
  : 'https://wallet-api.spoonity.com');

async function apiFetch(method, path, body) {
  const r = await fetch(`${KEYS_ENDPOINT()}${path}`, {
    method,
    credentials: 'include',
    headers: body ? { 'Content-Type': 'application/json' } : undefined,
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await r.json().catch(() => ({}));
  if (!r.ok) throw new Error(json.error || `Request failed (${r.status})`);
  return json;
}

/* Tiny clipboard helper with visual feedback. */
function CopyButton({ text, accent }) {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(text); } catch { /* fallback: select */ }
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  return (
    <button type="button" onClick={copy}
      style={{ padding: '6px 14px', borderRadius: 8, border: `1px solid ${accent}`, background: copied ? accent : 'transparent',
               color: copied ? '#fff' : accent, fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
               transition: 'background .15s, color .15s', flexShrink: 0 }}>
      {copied ? 'Copied!' : 'Copy'}
    </button>
  );
}

/* Modal shown once after key generation. Displays the raw key with a copy button. */
function NewKeyModal({ keyData, onClose, accent }) {
  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(17,24,39,.55)', display: 'flex',
                  alignItems: 'center', justifyContent: 'center', zIndex: 9999, padding: 24 }}>
      <div style={{ background: 'var(--surface-base)', borderRadius: 18, padding: '32px 28px', maxWidth: 520,
                    width: '100%', boxShadow: '0 24px 56px -16px rgba(17,24,39,.38)', fontFamily: 'var(--font-sans)' }}>
        <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--typography-primary)', marginBottom: 6 }}>
          Your new API key
        </div>
        <p style={{ fontSize: 13.5, color: 'var(--typography-secondary)', marginTop: 0, marginBottom: 20, lineHeight: 1.6 }}>
          Copy this key now. <strong>It will never be shown again.</strong> Store it in your backend environment
          variables, never in client-side code.
        </p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: 'var(--surface-secondary, #f5f3f1)',
                      borderRadius: 10, padding: '12px 16px', marginBottom: 24 }}>
          <code style={{ flex: 1, fontSize: 12, wordBreak: 'break-all', color: 'var(--typography-primary)',
                         fontFamily: 'ui-monospace,SFMono-Regular,monospace', lineHeight: 1.5 }}>
            {keyData.key}
          </code>
          <CopyButton text={keyData.key} accent={accent} />
        </div>
        <div style={{ fontSize: 12, color: 'var(--typography-tertiary)', marginBottom: 20 }}>
          <strong>Label:</strong> {keyData.label} &nbsp;·&nbsp; <strong>Program:</strong> {keyData.programId}
        </div>
        <button type="button" onClick={onClose}
          style={{ width: '100%', padding: '12px 0', borderRadius: 10, border: 'none', background: accent,
                   color: '#fff', fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 700, cursor: 'pointer' }}>
          I've saved my key, close
        </button>
      </div>
    </div>
  );
}

/* One key row in the list. */
function KeyRow({ k, onRevoke, accent }) {
  const [confirming, setConfirming] = React.useState(false);
  const [revoking, setRevoking] = React.useState(false);
  const revoked = !!k.revokedAt;
  const lastUsed = k.lastUsedAt
    ? new Date(k.lastUsedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
    : 'Never';
  const created = k.createdAt
    ? new Date(k.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
    : '';

  const doRevoke = async () => {
    setRevoking(true);
    try { await onRevoke(k.keyId); } finally { setRevoking(false); setConfirming(false); }
  };

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 18px',
                  background: 'var(--surface-base)', borderRadius: 12, border: '1px solid var(--border-primary)',
                  opacity: revoked ? 0.55 : 1 }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--typography-primary)',
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {k.label}
        </div>
        <div style={{ fontSize: 11.5, color: 'var(--typography-tertiary)', marginTop: 3 }}>
          Created {created} &nbsp;·&nbsp; Last used: {lastUsed}
          {k.createdBy && <> &nbsp;·&nbsp; by {k.createdBy}</>}
        </div>
      </div>
      <span style={{ fontSize: 11, fontWeight: 600, padding: '3px 10px', borderRadius: 99, flexShrink: 0,
                     background: revoked ? 'rgba(195,34,63,.1)' : 'rgba(63,125,88,.12)',
                     color: revoked ? '#c3223f' : '#3f7d58' }}>
        {revoked ? 'Revoked' : 'Active'}
      </span>
      {!revoked && (
        confirming ? (
          <div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
            <button type="button" onClick={() => setConfirming(false)} disabled={revoking}
              style={{ padding: '6px 12px', borderRadius: 8, border: '1px solid var(--border-primary)',
                       background: 'transparent', color: 'var(--typography-secondary)', fontFamily: 'var(--font-sans)',
                       fontSize: 12, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
            <button type="button" onClick={doRevoke} disabled={revoking}
              style={{ padding: '6px 12px', borderRadius: 8, border: 'none', background: '#c3223f',
                       color: '#fff', fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 600,
                       cursor: revoking ? 'wait' : 'pointer', opacity: revoking ? 0.7 : 1 }}>
              {revoking ? 'Revoking…' : 'Confirm'}
            </button>
          </div>
        ) : (
          <button type="button" onClick={() => setConfirming(true)}
            style={{ padding: '6px 12px', borderRadius: 8, border: '1px solid rgba(195,34,63,.35)',
                     background: 'transparent', color: '#c3223f', fontFamily: 'var(--font-sans)',
                     fontSize: 12, fontWeight: 600, cursor: 'pointer', flexShrink: 0 }}>Revoke</button>
        )
      )}
    </div>
  );
}

/* Confirmation modal shown before a key is actually generated. Requires reading the warning and
   checking the ack box before "Generate key" is clickable - a deliberate friction point, not
   busywork. Since the raw key can only ever be shown once (see the hashing note in "How it
   works"), an operator who generates one without reading this is the most likely way a key ends
   up screenshotted, pasted into a chat, or committed to a repo. */
function ConfirmGenerateModal({ label, onConfirm, onCancel, accent }) {
  const [ack, setAck] = React.useState(false);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState('');

  const confirm = async () => {
    setLoading(true); setError('');
    try {
      await onConfirm();
    } catch (err) {
      setError(err.message);
      setLoading(false);
    }
  };

  return ReactDOM.createPortal(
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(17,24,39,.55)', display: 'flex',
                  alignItems: 'center', justifyContent: 'center', zIndex: 9999, padding: 24 }}>
      <div style={{ background: 'var(--surface-base)', borderRadius: 18, padding: '32px 28px', maxWidth: 480,
                    width: '100%', boxShadow: '0 24px 56px -16px rgba(17,24,39,.38)', fontFamily: 'var(--font-sans)' }}>
        <div style={{ fontSize: 18, fontWeight: 700, color: 'var(--typography-primary)', marginBottom: 6 }}>
          Read before generating
        </div>
        <div style={{ fontSize: 13, color: 'var(--typography-tertiary)', marginBottom: 16 }}>
          Key: <strong>{label}</strong>
        </div>
        <div style={{ fontSize: 13.5, color: 'var(--typography-secondary)', lineHeight: 1.6, marginBottom: 20 }}>
          This key will be shown to you <strong>exactly once</strong>, immediately after you confirm
          below, never again after that. <strong>Do not share it, screenshot it, paste it into
          chat/email/tickets, or let anyone else see it.</strong> If this key is ever seen by anyone
          it shouldn't be, <strong>revoke it immediately</strong> and warn anyone else who needs to
          know it was exposed.
        </div>

        <label style={{ display: 'flex', gap: 9, alignItems: 'flex-start', marginBottom: 20, cursor: 'pointer' }}>
          <input type="checkbox" checked={ack} onChange={(e) => setAck(e.target.checked)}
            style={{ marginTop: 2, width: 15, height: 15, flexShrink: 0, cursor: 'pointer', accentColor: accent }} />
          <span style={{ fontSize: 12.5, color: 'var(--typography-secondary)', lineHeight: 1.5 }}>
            I understand this key can only be shown once, I will store it securely (e.g. an
            environment variable), and I won't share or expose it to anyone.
          </span>
        </label>

        {error && <div style={{ fontSize: 12.5, color: '#c3223f', marginBottom: 14 }}>{error}</div>}
        <div style={{ display: 'flex', gap: 8 }}>
          <button type="button" onClick={confirm} disabled={loading || !ack}
            style={{ flex: 1, padding: '11px 0', borderRadius: 10, border: 'none', background: accent, color: '#fff',
                     fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 700,
                     cursor: loading ? 'wait' : !ack ? 'not-allowed' : 'pointer', opacity: loading || !ack ? 0.5 : 1 }}>
            {loading ? 'Generating...' : 'Generate key'}
          </button>
          <button type="button" onClick={onCancel} disabled={loading}
            style={{ padding: '11px 18px', borderRadius: 10, border: '1px solid var(--border-primary)',
                     background: 'transparent', color: 'var(--typography-secondary)', fontFamily: 'var(--font-sans)',
                     fontSize: 14, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
        </div>
      </div>
    </div>,
    document.body
  );
}

/* Create-key form: label input + submit. Submitting doesn't generate the key directly - it opens
   ConfirmGenerateModal, which is the actual gate on the real API call. */
function CreateKeyForm({ programId, onCreated, onCancel, accent }) {
  const [label, setLabel] = React.useState('');
  const [confirming, setConfirming] = React.useState(false);
  const [error, setError] = React.useState('');

  const submit = (e) => {
    e.preventDefault();
    if (!label.trim()) return setError('Please enter a label for this key.');
    setError('');
    setConfirming(true);
  };

  const generate = async () => {
    const data = await apiFetch('POST', '/v1/api-keys', { programId, label: label.trim() });
    setConfirming(false);
    onCreated(data);
  };

  return (
    <form onSubmit={submit}
      style={{ background: 'var(--surface-secondary, #f5f3f1)', borderRadius: 12, padding: '18px 20px',
               border: `1.5px dashed ${accent}`, marginBottom: 16 }}>
      <div style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--typography-primary)', marginBottom: 12 }}>
        New API key
      </div>
      <input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="e.g. My Store Website"
        style={{ width: '100%', padding: '10px 14px', borderRadius: 9, border: '1px solid var(--border-primary)',
                 fontFamily: 'var(--font-sans)', fontSize: 13.5, color: 'var(--typography-primary)',
                 background: 'var(--surface-base)', boxSizing: 'border-box', marginBottom: 10 }} />
      {error && <div style={{ fontSize: 12.5, color: '#c3223f', marginBottom: 10 }}>{error}</div>}
      <div style={{ display: 'flex', gap: 8 }}>
        <button type="submit"
          style={{ padding: '9px 18px', borderRadius: 9, border: 'none', background: accent, color: '#fff',
                   fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>
          Generate key
        </button>
        <button type="button" onClick={onCancel}
          style={{ padding: '9px 16px', borderRadius: 9, border: '1px solid var(--border-primary)',
                   background: 'transparent', color: 'var(--typography-secondary)', fontFamily: 'var(--font-sans)',
                   fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
      </div>

      {confirming && (
        <ConfirmGenerateModal label={label.trim()} accent={accent}
          onConfirm={generate} onCancel={() => setConfirming(false)} />
      )}
    </form>
  );
}

/* Code block with a copy button. */
function CodeBlock({ code, accent }) {
  const [copied, setCopied] = React.useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(code); } catch {}
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  return (
    <div style={{ position: 'relative', background: '#0f172a', borderRadius: 10, overflow: 'hidden' }}>
      <button type="button" onClick={copy}
        style={{ position: 'absolute', top: 10, right: 10, padding: '4px 11px', borderRadius: 6,
                 border: `1px solid ${copied ? accent : 'rgba(255,255,255,.15)'}`,
                 background: copied ? accent : 'rgba(255,255,255,.06)', color: copied ? '#fff' : 'rgba(255,255,255,.6)',
                 fontFamily: 'var(--font-sans)', fontSize: 11.5, fontWeight: 600, cursor: 'pointer',
                 transition: 'all .15s' }}>
        {copied ? 'Copied!' : 'Copy'}
      </button>
      <pre style={{ margin: 0, padding: '18px 48px 18px 18px', overflowX: 'auto',
                    fontFamily: 'ui-monospace,SFMono-Regular,monospace', fontSize: 12.5,
                    lineHeight: 1.75, color: '#e2e8f0', whiteSpace: 'pre' }}>
        <code>{code}</code>
      </pre>
    </div>
  );
}

/* Inline code pill. */
const IC = ({ children }) => (
  <code style={{ fontSize: 12, background: 'var(--surface-secondary, #f0ede9)', padding: '2px 7px',
                 borderRadius: 5, fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                 color: 'var(--typography-primary)' }}>{children}</code>
);

/* Doc-tab ids/labels, shared between the page-level sidebar nav (which picks the tab) and
   DocsSection (which renders it), so both agree on what tabs exist without prop-drilling. */
const DOC_TABS = [
  { id: 'reference',  label: 'API Reference' },
  { id: 'appSession', label: 'Mobile Apps' },
  { id: 'nodejs',     label: 'Node.js' },
  { id: 'python',     label: 'Python' },
  { id: 'php',        label: 'PHP' },
  { id: 'curl',       label: 'cURL' },
];

/* Docs content for the currently-selected sidebar tab. `tab` is controlled by the page-level
   nav in IntegrationsView, not owned here, so this section has no nav of its own. */
function DocsSection({ tab, programId, accent }) {
  const PID = programId || 'YOUR_PROGRAM_ID';
  const BASE = 'https://wallet-api.spoonity.com';

  const NODE_CODE = `// Install: npm install node-fetch (or use built-in fetch in Node 18+)
// Store SPOONITY_API_KEY in your environment variables - never in code.

app.get('/add-to-wallet', async (req, res) => {
  const user = req.session.user; // your already-authenticated user

  const response = await fetch('${BASE}/v1/enroll/session', {
    method: 'POST',
    headers: {
      'Authorization': \`Bearer \${process.env.SPOONITY_API_KEY}\`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      programId: '${PID}',
      user: {
        name: user.name,       // full name
        email: user.email,     // email address
        externalId: user.id,   // your own user ID (used for deduplication)
      },
      consentGiven: true,      // you must have the user's consent
    }),
  });

  const { sessionUrl } = await response.json();

  // Redirect the user's browser - they land on the Add to Wallet screen.
  res.redirect(sessionUrl);
});`;

  const NODE_CODE_SESSION = `// Alternative: if your login already goes through Spoonity (you hold a
// Spoonity session_key from their own customer-session flow), send THAT
// instead of typing out name/email/id yourself - we look the profile up
// directly from Spoonity.

app.get('/add-to-wallet', async (req, res) => {
  const response = await fetch('${BASE}/v1/enroll/session', {
    method: 'POST',
    headers: {
      'Authorization': \`Bearer \${process.env.SPOONITY_API_KEY}\`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      programId: '${PID}',
      spoonitySessionKey: req.session.spoonitySessionKey, // from Spoonity's own session
      consentGiven: true,
    }),
  });

  const { sessionUrl } = await response.json();
  res.redirect(sessionUrl);
});`;

  const PYTHON_CODE = `# Install: pip install requests
# Store SPOONITY_API_KEY in your environment - never hard-code it.
import os, requests
from flask import redirect, session

@app.route('/add-to-wallet')
def add_to_wallet():
    user = session['user']  # your already-authenticated user

    resp = requests.post(
        '${BASE}/v1/enroll/session',
        headers={
            'Authorization': f'Bearer {os.environ["SPOONITY_API_KEY"]}',
            'Content-Type': 'application/json',
        },
        json={
            'programId': '${PID}',
            'user': {
                'name': user['name'],
                'email': user['email'],
                'externalId': str(user['id']),  # your own user ID
            },
            'consentGiven': True,
        }
    )

    session_url = resp.json()['sessionUrl']
    return redirect(session_url)`;

  const PYTHON_CODE_SESSION = `# Alternative: if your login already goes through Spoonity (you hold a
# Spoonity session_key from their own customer-session flow), send THAT
# instead of typing out name/email/id yourself - we look the profile up
# directly from Spoonity.

@app.route('/add-to-wallet')
def add_to_wallet():
    resp = requests.post(
        '${BASE}/v1/enroll/session',
        headers={
            'Authorization': f'Bearer {os.environ["SPOONITY_API_KEY"]}',
            'Content-Type': 'application/json',
        },
        json={
            'programId': '${PID}',
            'spoonitySessionKey': session['spoonity_session_key'],  # from Spoonity's own session
            'consentGiven': True,
        }
    )

    return redirect(resp.json()['sessionUrl'])`;

  const PHP_CODE = `<?php
// Store SPOONITY_API_KEY in your .env - never hard-code it.

$user = $_SESSION['user']; // your already-authenticated user

$ch = curl_init('${BASE}/v1/enroll/session');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('SPOONITY_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'programId' => '${PID}',
        'user' => [
            'name'       => $user['name'],
            'email'      => $user['email'],
            'externalId' => (string) $user['id'], // your own user ID
        ],
        'consentGiven' => true,
    ]),
]);

$data = json_decode(curl_exec($ch), true);
curl_close($ch);

// Redirect the user - they land on the Add to Wallet screen.
header('Location: ' . $data['sessionUrl']);`;

  const PHP_CODE_SESSION = `<?php
// Alternative: if your login already goes through Spoonity (you hold a
// Spoonity session_key from their own customer-session flow), send THAT
// instead of typing out name/email/id yourself - we look the profile up
// directly from Spoonity.

$ch = curl_init('${BASE}/v1/enroll/session');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('SPOONITY_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'programId'          => '${PID}',
        'spoonitySessionKey' => $_SESSION['spoonity_session_key'], // from Spoonity's own session
        'consentGiven'       => true,
    ]),
]);

$data = json_decode(curl_exec($ch), true);
curl_close($ch);
header('Location: ' . $data['sessionUrl']);`;

  const CURL_CODE = `curl -X POST ${BASE}/v1/enroll/session \\
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \\
  -H "Content-Type: application/json" \\
  -d '{
    "programId": "${PID}",
    "user": {
      "name": "Jane Doe",
      "email": "jane@example.com",
      "externalId": "your_user_id_123"
    },
    "consentGiven": true
  }'`;

  const CURL_CODE_SESSION = `# Alternative: if you already have a Spoonity session_key for this customer
# (from Spoonity's own customer-session flow), send that instead - we look
# the profile up directly from Spoonity, no need to pass name/email/id.
curl -X POST ${BASE}/v1/enroll/session \\
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \\
  -H "Content-Type: application/json" \\
  -d '{
    "programId": "${PID}",
    "spoonitySessionKey": "the_customers_spoonity_session_key",
    "consentGiven": true
  }'`;

  const REQUEST_BODY_SESSION = `{
  "programId": "${PID}",             // required - your program ID
  "spoonitySessionKey": "...",       // the customer's Spoonity session key -
                                      // we fetch their name/email/id directly
                                      // from Spoonity, you don't provide them
  "consentGiven": true               // required - must be true
}`;

  const REQUEST_BODY = `{
  "programId": "${PID}",          // required - your program ID
  "user": {
    "name": "Jane Doe",           // required - customer's full name
    "email": "jane@example.com",  // required - customer's email
    "externalId": "user_123"      // required - your own user ID (deduplication key)
  },
  "consentGiven": true            // required - must be true
}`;

  const RESPONSE_BODY = `{
  "sessionUrl": "${BASE}/enroll/s/<opaque_key>",
  "expiresAt": "2026-07-15T13:15:00.000Z",  // 15 minutes from now
  "alreadyEnrolled": false                   // true if this user already has a pass
}`;

  const ERRORS = [
    ['400', 'Missing or invalid field (check error message for which one)'],
    ['401', 'Invalid or revoked API key - OR, if using spoonitySessionKey, that session key is invalid/expired'],
    ['403', 'programId does not match this API key\'s scope'],
    ['429', 'Rate limit exceeded (max 600 requests per 15 minutes)'],
    ['500', 'Server error; safe to retry'],
  ];

  // --- Update favorite store (POST /v1/members/favorite-store): same API key/auth as above,
  // for refreshing an EXISTING member's favorite-store personalization at any time after enrollment.
  const FAVORITE_STORE_REQUEST = `{
  "programId": "${PID}",       // required - your program ID
  "spoonityUserId": "123456"   // required - the Spoonity user_id for this customer
                                // (the same id you may have passed as externalId at enrollment)
}`;

  const FAVORITE_STORE_RESPONSE = `{
  "ok": true,
  "updated": true,                 // false if Spoonity has no ranked favorite yet - not an
                                    // error; their previous favorite (if any) is left untouched
  "favoriteLocation": {
    "storeId": "482",
    "name": "Cutters Point - Downtown",
    "lat": 45.4215,
    "lon": -75.6972,
    "address": "123 Main St",
    "favoriteProduct": "ESPRESSO 8OZ"  // their most-ordered item at that store, or null
  }
}`;

  const FAVORITE_STORE_ERRORS = [
    ['400', 'Missing or invalid field (check error message for which one)'],
    ['401', 'Invalid or revoked API key'],
    ['403', 'programId does not match this API key\'s scope'],
    ['404', 'This customer does not have a wallet pass in this program yet (favorite-store refresh only works on existing members - enroll them first)'],
    ['429', 'Rate limit exceeded (max 600 requests per 15 minutes)'],
    ['500', 'Server error; safe to retry'],
  ];

  // --- Mobile app flow (POST /enroll/app-session): NO API key. Authenticated by the customer's
  // own live Spoonity session key instead, since a compiled mobile app can't keep a server secret.
  const APP_SESSION_REQUEST = `{
  "programId": "${PID}",             // required - your program ID
  "spoonitySessionKey": "...",       // required - the CUSTOMER's own live Spoonity session key
                                      // (the one their own device already holds from logging
                                      // into your app via Spoonity) - we fetch their name/email/id
                                      // directly from Spoonity, you don't provide them
  "consentGiven": true               // required - must be true
}`;

  const APP_SESSION_RESPONSE = `{
  "sessionUrl": "${BASE}/enroll/s/<opaque_key>",
  "expiresAt": "2026-07-15T13:15:00.000Z",  // 15 minutes from now
  "alreadyEnrolled": false                   // true if this user already has a pass
}`;

  const APP_SESSION_FETCH = `// Called directly from the app - no server, no API key, no secret to protect.
// The customer's own spoonitySessionKey IS the authentication.

async function addToWallet(spoonitySessionKey, programId) {
  const response = await fetch('${BASE}/enroll/app-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      programId: '${PID}',
      spoonitySessionKey,   // from your app's existing Spoonity login/session
      consentGiven: true,   // you must have the customer's explicit consent
    }),
  });

  const { sessionUrl } = await response.json();

  // Open sessionUrl in an in-app browser / system webview - it completes enrollment and
  // shows the "Add to Apple/Google Wallet" screen. No further API calls needed.
  return sessionUrl;
}`;

  const APP_SESSION_ERRORS = [
    ['400', 'Missing or invalid field (check error message for which one)'],
    ['401', 'Invalid or expired Spoonity session key'],
    ['404', 'Program not found'],
    ['429', 'Rate limit exceeded (max 120 requests per 15 minutes - shared across all callers, not per-key, since there is no key)'],
    ['500', 'Server error; safe to retry'],
  ];

  const sectionHead = (title) => (
    <div style={{ fontSize: 11.5, fontWeight: 700, color: 'var(--typography-tertiary)',
                  textTransform: 'uppercase', letterSpacing: '.07em', marginBottom: 10, marginTop: 24 }}>
      {title}
    </div>
  );

  return (
    <div>
      <h2 style={{ fontSize: 20, fontWeight: 800, color: 'var(--typography-primary)',
                   margin: '0 0 6px', letterSpacing: '-.01em' }}>
        Integration guide
      </h2>
      <p style={{ fontSize: 13.5, color: 'var(--typography-secondary)', marginBottom: 28, lineHeight: 1.6 }}>
        Your API key is a <strong>server-side secret</strong>: it must only be used from your backend,
        never from browser JavaScript or a mobile app. The pattern is always: your backend calls
        the API, gets a short-lived URL, and redirects the user's browser to it.
      </p>

      {/* Reference tab */}
      {tab === 'reference' && (
        <div>
          {sectionHead('Endpoint')}
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 16px',
                        background: 'var(--surface-base)', borderRadius: 10, border: '1px solid var(--border-primary)',
                        marginBottom: 4 }}>
            <span style={{ fontSize: 11.5, fontWeight: 700, padding: '3px 8px', borderRadius: 6,
                           background: accent + '18', color: accent }}>POST</span>
            <code style={{ fontSize: 13, fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                           color: 'var(--typography-primary)' }}>{BASE}/v1/enroll/session</code>
          </div>

          {sectionHead('Authentication')}
          <div style={{ padding: '12px 16px', background: 'var(--surface-base)', borderRadius: 10,
                        border: '1px solid var(--border-primary)', fontSize: 13,
                        color: 'var(--typography-secondary)', lineHeight: 1.7 }}>
            Pass your API key in the <IC>Authorization</IC> header:<br />
            <span style={{ fontFamily: 'ui-monospace,SFMono-Regular,monospace', fontSize: 12.5,
                           color: 'var(--typography-primary)' }}>
              Authorization: Bearer sk_live_…
            </span>
          </div>

          {sectionHead('Request body')}
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 12, lineHeight: 1.6 }}>
            There are <strong>two ways</strong> to identify the customer: use whichever fits how
            your login works. Both need <IC>programId</IC> and <IC>consentGiven: true</IC>.
          </div>
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 8 }}>
            Option A: you already have a Spoonity session key
          </div>
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginBottom: 10, lineHeight: 1.6 }}>
            If your customer's login already goes through Spoonity, pass their <IC>session_key</IC> and
            we'll fetch their real name/email/id directly from Spoonity, you don't provide them yourself.
          </div>
          <CodeBlock code={REQUEST_BODY_SESSION} accent={accent} />
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 8, marginTop: 20 }}>
            Option B: you have your own user data
          </div>
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginBottom: 10, lineHeight: 1.6 }}>
            If you maintain your own customer accounts (not tied to a live Spoonity session), pass their
            details directly.
          </div>
          <CodeBlock code={REQUEST_BODY} accent={accent} />

          {sectionHead('Response')}
          <CodeBlock code={RESPONSE_BODY} accent={accent} />
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginTop: 8, lineHeight: 1.6 }}>
            Redirect the user's browser to <IC>sessionUrl</IC>. The link is single-use and expires in
            15 minutes. If <IC>alreadyEnrolled</IC> is <IC>true</IC>, the user already has a pass;
            the sessionUrl will still take them to it.
          </div>

          {sectionHead('Error codes')}
          <div style={{ background: 'var(--surface-base)', borderRadius: 10, border: '1px solid var(--border-primary)',
                        overflow: 'hidden' }}>
            {ERRORS.map(([code, desc], i) => (
              <div key={code} style={{ display: 'flex', gap: 16, padding: '11px 16px',
                                       borderBottom: i < ERRORS.length - 1 ? '1px solid var(--border-primary)' : 'none',
                                       alignItems: 'flex-start' }}>
                <code style={{ fontSize: 12.5, fontWeight: 700, color: '#c3223f', fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                               flexShrink: 0, marginTop: 1 }}>{code}</code>
                <span style={{ fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>{desc}</span>
              </div>
            ))}
          </div>

          {sectionHead('Security notes')}
          <div style={{ background: accent + '0d', borderRadius: 10, border: `1px solid ${accent}30`,
                        padding: '14px 16px' }}>
            {[
              'Never put your API key in browser JavaScript, a mobile app, or version control.',
              'Building a mobile app with no backend of its own? Don\'t try to embed a key - use the Mobile Apps tab instead, which needs no server secret at all.',
              'Store it as an environment variable (e.g. SPOONITY_API_KEY) and read it at runtime.',
              'Each key is scoped to one program: it cannot access other programs\' data.',
              'Revoke a key immediately if it\'s ever accidentally exposed.',
            ].map((note, i, arr) => (
              <div key={i} style={{ display: 'flex', gap: 10, marginBottom: i < arr.length - 1 ? 8 : 0 }}>
                <span style={{ color: accent, fontWeight: 700, flexShrink: 0, marginTop: 1 }}>·</span>
                <span style={{ fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>{note}</span>
              </div>
            ))}
          </div>

          {sectionHead('Other endpoints')}
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 12, lineHeight: 1.6 }}>
            Same API key, same <IC>Authorization: Bearer sk_live_…</IC> header as above.
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 16px',
                        background: 'var(--surface-base)', borderRadius: 10, border: '1px solid var(--border-primary)',
                        marginBottom: 8 }}>
            <span style={{ fontSize: 11.5, fontWeight: 700, padding: '3px 8px', borderRadius: 6,
                           background: accent + '18', color: accent }}>POST</span>
            <code style={{ fontSize: 13, fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                           color: 'var(--typography-primary)' }}>{BASE}/v1/members/favorite-store</code>
          </div>
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginBottom: 12, lineHeight: 1.6 }}>
            Refreshes an <strong>existing</strong> member's favorite store (used to personalize their pass's
            location features). Not part of enrollment: call it any time after the fact, whenever you have
            the Spoonity <IC>user_id</IC> for a customer who already has a pass in this program.
          </div>
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 8 }}>
            Request body
          </div>
          <CodeBlock code={FAVORITE_STORE_REQUEST} accent={accent} />
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 8, marginTop: 16 }}>
            Response
          </div>
          <CodeBlock code={FAVORITE_STORE_RESPONSE} accent={accent} />
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginTop: 8, marginBottom: 16, lineHeight: 1.6 }}>
            <IC>updated: false</IC> with a <IC>null favoriteLocation</IC> isn't an error: it just means Spoonity
            has no ranked favorite for that customer yet (e.g. no transaction history). Their previously
            recorded favorite, if any, is left untouched rather than cleared.
          </div>
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', marginBottom: 8 }}>
            Error codes
          </div>
          <div style={{ background: 'var(--surface-base)', borderRadius: 10, border: '1px solid var(--border-primary)',
                        overflow: 'hidden' }}>
            {FAVORITE_STORE_ERRORS.map(([code, desc], i) => (
              <div key={code} style={{ display: 'flex', gap: 16, padding: '11px 16px',
                                       borderBottom: i < FAVORITE_STORE_ERRORS.length - 1 ? '1px solid var(--border-primary)' : 'none',
                                       alignItems: 'flex-start' }}>
                <code style={{ fontSize: 12.5, fontWeight: 700, color: '#c3223f', fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                               flexShrink: 0, marginTop: 1 }}>{code}</code>
                <span style={{ fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>{desc}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {tab === 'appSession' && (
        <div>
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 16, lineHeight: 1.6 }}>
            For a vendor's own <strong>native or mobile app</strong> with no backend of its own to hold a
            server secret. This is a different endpoint from the rest of this guide: it needs
            <strong> no API key at all</strong>. Instead, the customer's own live Spoonity session key
            (from your app's existing Spoonity login) proves who they are; it only resolves against
            this program's own vendor, so it can't be used to enroll anyone else.
          </div>
          <div style={{ background: accent + '0d', borderRadius: 10, border: `1px solid ${accent}30`,
                        padding: '14px 16px', marginBottom: 20, fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>
            Use this <strong>only</strong> when there's no backend in the picture. If your app talks to your
            own server first, use the server-side flow instead (API Reference tab). It's the same idea,
            just called from a place that can actually keep a secret.
          </div>

          {sectionHead('Endpoint')}
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '12px 16px',
                        background: 'var(--surface-base)', borderRadius: 10, border: '1px solid var(--border-primary)',
                        marginBottom: 4 }}>
            <span style={{ fontSize: 11.5, fontWeight: 700, padding: '3px 8px', borderRadius: 6,
                           background: accent + '18', color: accent }}>POST</span>
            <code style={{ fontSize: 13, fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                           color: 'var(--typography-primary)' }}>{BASE}/enroll/app-session</code>
          </div>

          {sectionHead('Authentication')}
          <div style={{ padding: '12px 16px', background: 'var(--surface-base)', borderRadius: 10,
                        border: '1px solid var(--border-primary)', fontSize: 13,
                        color: 'var(--typography-secondary)', lineHeight: 1.7 }}>
            None: no header, no key. The <IC>spoonitySessionKey</IC> in the request body <strong>is</strong>
            the authentication.
          </div>

          {sectionHead('Request body')}
          <CodeBlock code={APP_SESSION_REQUEST} accent={accent} />

          {sectionHead('Response')}
          <CodeBlock code={APP_SESSION_RESPONSE} accent={accent} />
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginTop: 8, lineHeight: 1.6 }}>
            Same shape as the server-side flow: open <IC>sessionUrl</IC> in an in-app browser / system
            webview. It's single-use and expires in 15 minutes.
          </div>

          {sectionHead('Example (JavaScript / React Native)')}
          <div style={{ fontSize: 12.5, color: 'var(--typography-tertiary)', marginBottom: 10, lineHeight: 1.6 }}>
            The same request works from any platform (Swift, Kotlin, etc.). It's just a plain POST with
            a JSON body, no auth header to construct.
          </div>
          <CodeBlock code={APP_SESSION_FETCH} accent={accent} />

          {sectionHead('Error codes')}
          <div style={{ background: 'var(--surface-base)', borderRadius: 10, border: '1px solid var(--border-primary)',
                        overflow: 'hidden' }}>
            {APP_SESSION_ERRORS.map(([code, desc], i) => (
              <div key={code} style={{ display: 'flex', gap: 16, padding: '11px 16px',
                                       borderBottom: i < APP_SESSION_ERRORS.length - 1 ? '1px solid var(--border-primary)' : 'none',
                                       alignItems: 'flex-start' }}>
                <code style={{ fontSize: 12.5, fontWeight: 700, color: '#c3223f', fontFamily: 'ui-monospace,SFMono-Regular,monospace',
                               flexShrink: 0, marginTop: 1 }}>{code}</code>
                <span style={{ fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>{desc}</span>
              </div>
            ))}
          </div>

          {sectionHead('Security notes')}
          <div style={{ background: accent + '0d', borderRadius: 10, border: `1px solid ${accent}30`,
                        padding: '14px 16px' }}>
            {[
              'There\'s no key to leak here, but the spoonitySessionKey is still the customer\'s own live session - treat it the same way you\'d treat any auth token (don\'t log it, don\'t send it anywhere except this endpoint).',
              'consentGiven must reflect a real, explicit action by the customer in your app (e.g. tapping "Add to Wallet") - not a default or a pre-checked box.',
              'This endpoint can only enroll the customer who owns the session key it\'s given; it cannot be used to enroll or look up anyone else.',
            ].map((note, i, arr) => (
              <div key={i} style={{ display: 'flex', gap: 10, marginBottom: i < arr.length - 1 ? 8 : 0 }}>
                <span style={{ color: accent, fontWeight: 700, flexShrink: 0, marginTop: 1 }}>·</span>
                <span style={{ fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>{note}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {tab === 'nodejs' && (
        <div>
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 14, lineHeight: 1.6 }}>
            Works with Express, Next.js API routes, Fastify, or any Node.js server.
            Requires Node 18+ (built-in <IC>fetch</IC>) or install <IC>node-fetch</IC>.
          </div>
          <CodeBlock code={NODE_CODE} accent={accent} />
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', margin: '20px 0 8px' }}>
            Or, if you already have a Spoonity session key
          </div>
          <CodeBlock code={NODE_CODE_SESSION} accent={accent} />
        </div>
      )}

      {tab === 'python' && (
        <div>
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 14, lineHeight: 1.6 }}>
            Works with Flask, Django, FastAPI, or any Python backend.
            Install the <IC>requests</IC> library: <IC>pip install requests</IC>
          </div>
          <CodeBlock code={PYTHON_CODE} accent={accent} />
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', margin: '20px 0 8px' }}>
            Or, if you already have a Spoonity session key
          </div>
          <CodeBlock code={PYTHON_CODE_SESSION} accent={accent} />
        </div>
      )}

      {tab === 'php' && (
        <div>
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 14, lineHeight: 1.6 }}>
            Works with Laravel, WordPress, Symfony, or plain PHP.
            Uses the built-in <IC>curl</IC> extension (enabled by default on most hosts).
          </div>
          <CodeBlock code={PHP_CODE} accent={accent} />
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', margin: '20px 0 8px' }}>
            Or, if you already have a Spoonity session key
          </div>
          <CodeBlock code={PHP_CODE_SESSION} accent={accent} />
        </div>
      )}

      {tab === 'curl' && (
        <div>
          <div style={{ fontSize: 13, color: 'var(--typography-secondary)', marginBottom: 14, lineHeight: 1.6 }}>
            Useful for testing the API from your terminal before writing code.
            Replace <IC>sk_live_YOUR_API_KEY</IC> with your actual key.
          </div>
          <CodeBlock code={CURL_CODE} accent={accent} />
          <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--typography-tertiary)', textTransform: 'uppercase', letterSpacing: '.06em', margin: '20px 0 8px' }}>
            Or, if you already have a Spoonity session key
          </div>
          <CodeBlock code={CURL_CODE_SESSION} accent={accent} />
        </div>
      )}
    </div>
  );
}

/* Full-screen integrations view mounted by SpoonityApp when view === "integrations". */
function IntegrationsView({ onHome, accent = '#cc6716' }) {
  const [programs, setPrograms] = React.useState(null); // null = loading
  const [selectedProgram, setSelectedProgram] = React.useState('');
  const [keys, setKeys] = React.useState(null);
  const [keysLoading, setKeysLoading] = React.useState(false);
  const [creating, setCreating] = React.useState(false);
  const [newKeyData, setNewKeyData] = React.useState(null);
  const [showRevoked, setShowRevoked] = React.useState(false);
  const [error, setError] = React.useState('');
  const [section, setSection] = React.useState('keys'); // 'keys' | one of DOC_TABS' ids
  const mainRef = React.useRef(null);
  // The content pane is one persistent scrollable element across every tab (only its children
  // swap), so its scrollTop otherwise carries over from whichever tab you were previously on.
  React.useEffect(() => { if (mainRef.current) mainRef.current.scrollTop = 0; }, [section]);

  const navStyle = (id) => ({
    padding: '9px 12px', border: 'none', borderRadius: 8, textAlign: 'left', width: '100%',
    background: section === id ? accent : 'transparent',
    color: section === id ? '#fff' : 'var(--typography-secondary)',
    fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600,
    cursor: 'pointer', transition: 'background .15s, color .15s',
  });
  const navGroupLabel = (text) => (
    <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--typography-tertiary)',
                  textTransform: 'uppercase', letterSpacing: '.07em', padding: '0 10px', margin: '22px 0 6px' }}>
      {text}
    </div>
  );

  // The vendor's real published programs (wallet_programs), not the editor drafts collection.
  // See vendor-programs.js for why the drafts list undercounts.
  React.useEffect(() => {
    apiFetch('GET', '/v1/programs')
      .then((data) => {
        const list = (data.programs || []).map((p) => ({ programId: p.programId, name: p.name || p.company || p.programId }));
        setPrograms(list);
        if (list.length > 0) setSelectedProgram(list[0].programId);
      })
      .catch(() => setPrograms([]));
  }, []);

  // Reload keys when selected program changes.
  React.useEffect(() => {
    if (!selectedProgram) { setKeys(null); return; }
    setKeysLoading(true); setError('');
    apiFetch('GET', `/v1/api-keys?programId=${encodeURIComponent(selectedProgram)}`)
      .then((data) => setKeys(data.keys || []))
      .catch((err) => { setError(err.message); setKeys([]); })
      .finally(() => setKeysLoading(false));
  }, [selectedProgram]);

  const handleRevoke = async (keyId) => {
    await apiFetch('DELETE', `/v1/api-keys/${keyId}`);
    setKeys((prev) => prev.map((k) => k.keyId === keyId ? { ...k, revokedAt: new Date().toISOString() } : k));
  };

  const handleCreated = (data) => {
    setCreating(false);
    setNewKeyData(data);
    setKeys((prev) => [{ keyId: data.keyId, label: data.label, programId: data.programId,
      createdAt: data.createdAt, revokedAt: null, lastUsedAt: null }, ...(prev || [])]);
  };

  const selectedName = programs?.find((p) => p.programId === selectedProgram)?.name || selectedProgram;
  const activeKeys = (keys || []).filter((k) => !k.revokedAt);
  const revokedKeys = (keys || []).filter((k) => k.revokedAt);

  return (
    <div style={{ height: '100vh', background: 'var(--surface-secondary, #f8f6f4)', fontFamily: 'var(--font-sans)', display: 'flex', flexDirection: 'column' }}>
      {/* Header */}
      <header style={{ height: 64, flexShrink: 0, display: 'flex', alignItems: 'center', gap: 16, padding: '0 28px',
                       background: 'var(--surface-base)', borderBottom: '1px solid var(--border-primary)' }}>
        <button type="button" onClick={onHome}
          style={{ display: 'flex', alignItems: 'center', gap: 6, border: 'none', background: 'transparent',
                   cursor: 'pointer', padding: '6px 10px 6px 6px', borderRadius: 9, color: 'var(--typography-secondary)',
                   fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 600 }}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2"
               strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5M12 5l-7 7 7 7" /></svg>
          Home
        </button>
        <span style={{ width: 1, height: 20, background: 'var(--border-primary)' }} />
        <span style={{ fontSize: 15, fontWeight: 700, color: 'var(--typography-primary)' }}>API Integrations</span>
      </header>

      <div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
        {/* Sidebar: spans the full height of the page, not just the docs section. */}
        <nav style={{ width: 216, flexShrink: 0, background: 'var(--surface-base)',
                      borderRight: '1px solid var(--border-primary)', padding: '24px 12px',
                      overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 2 }}>
          <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--typography-tertiary)',
                        textTransform: 'uppercase', letterSpacing: '.07em', padding: '0 10px', marginBottom: 6 }}>
            Manage
          </div>
          <button type="button" style={navStyle('keys')} onClick={() => setSection('keys')}>API Keys</button>

          {navGroupLabel('Integration guide')}
          {DOC_TABS.map(({ id, label }) => (
            <button key={id} type="button" style={navStyle(id)} onClick={() => setSection(id)}>{label}</button>
          ))}
        </nav>

        {/* Content fills the rest of the screen width. */}
        <main ref={mainRef} style={{ flex: 1, minWidth: 0, overflowY: 'auto', padding: '40px 44px 64px' }}>
        {section !== 'keys' ? (
          <div style={{ maxWidth: 820 }}>
            <DocsSection tab={section} programId={selectedProgram} accent={accent} />
          </div>
        ) : (
        <div style={{ width: '100%', maxWidth: 900 }}>

          {/* Intro */}
          <h1 style={{ fontSize: 26, fontWeight: 800, color: 'var(--typography-primary)', margin: '0 0 8px', letterSpacing: '-.01em' }}>
            API Keys
          </h1>
          <p style={{ fontSize: 14, color: 'var(--typography-secondary)', marginTop: 0, marginBottom: 32, lineHeight: 1.6, maxWidth: 520 }}>
            Generate a key for your store's backend to auto-enroll logged-in customers; no form required.
            Your backend calls <code style={{ fontSize: 12.5, background: 'var(--surface-secondary, #f0ede9)', padding: '2px 6px', borderRadius: 5 }}>POST /v1/enroll/session</code> and redirects the user to the returned URL.
          </p>

          {/* How it works */}
          <div style={{ background: 'var(--surface-base)', borderRadius: 14, border: '1px solid var(--border-primary)',
                        padding: '20px 22px', marginBottom: 32 }}>
            <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--typography-primary)', marginBottom: 14, textTransform: 'uppercase', letterSpacing: '.06em' }}>How it works</div>
            {[
              ['1', 'Generate an API key below and store it in your backend environment variables.'],
              ['2', 'When a logged-in customer clicks "Add to Wallet", your backend calls POST /v1/enroll/session with their name, email, and your own user ID.'],
              ['3', 'Redirect their browser to the returned sessionUrl; they land on the Add to Wallet page. No form, no re-typing.'],
              ['4', 'Behind the scenes, your key is one-way hashed before we ever store it, like a password. We can\'t reverse that hash, so if you lose the key, it genuinely can\'t be recovered; the only fix is to revoke it and generate a new one.'],
            ].map(([n, text], i, arr) => (
              <div key={n} style={{ display: 'flex', gap: 12, marginBottom: i === arr.length - 1 ? 0 : 12 }}>
                <span style={{ width: 22, height: 22, borderRadius: '50%', background: accent + '1a', color: accent,
                               fontWeight: 700, fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>{n}</span>
                <span style={{ fontSize: 13, color: 'var(--typography-secondary)', lineHeight: 1.6 }}>{text}</span>
              </div>
            ))}
          </div>

          {/* Program selector */}
          {programs === null ? (
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, color: 'var(--typography-tertiary)', fontSize: 13 }}>
              <span className="spty-spin-gravity" style={{ width: 18, height: 18 }} />
              Loading programs…
            </div>
          ) : programs.length === 0 ? (
            <div style={{ padding: '28px 24px', borderRadius: 14, border: '1.5px dashed var(--border-primary)', textAlign: 'center' }}>
              <div style={{ fontSize: 13, color: 'var(--typography-tertiary)', lineHeight: 1.6 }}>
                No published passes yet. Design and publish a pass first, then come back to generate an API key.
              </div>
            </div>
          ) : (
            <>
              {programs.length > 1 && (
                <div style={{ marginBottom: 22 }}>
                  <label style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--typography-secondary)', display: 'block', marginBottom: 6 }}>Program</label>
                  <select value={selectedProgram} onChange={(e) => setSelectedProgram(e.target.value)}
                    style={{ padding: '9px 12px', borderRadius: 10, border: '1px solid var(--border-primary)',
                             background: 'var(--surface-base)', fontFamily: 'var(--font-sans)', fontSize: 13.5,
                             color: 'var(--typography-primary)', cursor: 'pointer', maxWidth: 340 }}>
                    {programs.map((p) => <option key={p.programId} value={p.programId}>{p.name}</option>)}
                  </select>
                </div>
              )}

              {/* Keys for selected program */}
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
                <div>
                  <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--typography-primary)' }}>
                    {selectedName}
                  </div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 2 }}>
                    <span style={{ fontSize: 12, color: 'var(--typography-tertiary)' }}>Program ID: {selectedProgram}</span>
                    <CopyButton text={selectedProgram} accent={accent} />
                  </div>
                </div>
                {!creating && (
                  <button type="button" onClick={() => setCreating(true)}
                    style={{ padding: '9px 18px', borderRadius: 10, border: 'none', background: accent, color: '#fff',
                             fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 700, cursor: 'pointer' }}>
                    + Generate key
                  </button>
                )}
              </div>

              {creating && (
                <CreateKeyForm programId={selectedProgram} onCreated={handleCreated}
                  onCancel={() => setCreating(false)} accent={accent} />
              )}

              {error && <div style={{ fontSize: 13, color: '#c3223f', marginBottom: 14 }}>{error}</div>}

              {keysLoading ? (
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '20px 0', color: 'var(--typography-tertiary)', fontSize: 13 }}>
                  <span className="spty-spin-gravity" style={{ width: 18, height: 18 }} />
                  Loading keys…
                </div>
              ) : keys && keys.length === 0 ? (
                <div style={{ padding: '28px 24px', borderRadius: 14, border: '1.5px dashed var(--border-primary)', textAlign: 'center' }}>
                  <div style={{ fontSize: 13, color: 'var(--typography-tertiary)' }}>
                    No API keys yet. Generate one to get started.
                  </div>
                </div>
              ) : (
                <>
                  <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                    {activeKeys.map((k) => (
                      <KeyRow key={k.keyId} k={k} onRevoke={handleRevoke} accent={accent} />
                    ))}
                  </div>

                  {revokedKeys.length > 0 && (
                    <div style={{ marginTop: 20 }}>
                      <button type="button" onClick={() => setShowRevoked((v) => !v)}
                        style={{ display: 'flex', alignItems: 'center', gap: 6, border: 'none', background: 'transparent',
                                 padding: '6px 2px', cursor: 'pointer', color: 'var(--typography-tertiary)',
                                 fontFamily: 'var(--font-sans)', fontSize: 12.5, fontWeight: 600 }}>
                        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"
                             strokeLinecap="round" strokeLinejoin="round"
                             style={{ transform: showRevoked ? 'rotate(90deg)' : 'none', transition: 'transform .15s' }}>
                          <path d="M9 18l6-6-6-6" />
                        </svg>
                        Revoked keys ({revokedKeys.length})
                      </button>

                      {showRevoked && (
                        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 10 }}>
                          {revokedKeys.map((k) => (
                            <KeyRow key={k.keyId} k={k} onRevoke={handleRevoke} accent={accent} />
                          ))}
                        </div>
                      )}
                    </div>
                  )}
                </>
              )}
            </>
          )}

        </div>
        )}
        </main>
      </div>

      {newKeyData && (
        <NewKeyModal keyData={newKeyData} accent={accent} onClose={() => setNewKeyData(null)} />
      )}
    </div>
  );
}

window.IntegrationsView = IntegrationsView;
