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

// Public benchmark orchestrator (/benchmark-admin). Staff-only
// (me.featureFlags.benchmarkAdmin === isArmatureStaff). Edits the
// public_benchmark_config_* catalog that drives the PUBLIC benchmark at
// /leaderboard/:category — replacing the old config/public-benchmark.json
// + PR flow. Changes apply on the next aggregator run (hourly at :13).
//
// Flow: pick a category → toggle whether it's live → edit its vendor list
// and workflow columns → "View public leaderboard" opens the real public
// page. There is no per-org dimension; the public benchmark is sourced
// from the house org (ARMATURE_BENCHMARK_ORG_ID).

const {
  useCallback: useCallbackBa,
  useEffect: useEffectBa,
  useMemo: useMemoBa,
  useState: useStateBa,
} = React;

const BA_TABS = [
  { key: 'vendors', label: 'Vendors' },
  { key: 'workflows', label: 'Workflows' },
  { key: 'gaps', label: 'Capability gaps' },
];

function baNormaliseTab(tab) {
  return BA_TABS.find((t) => t.key === tab)?.key || 'vendors';
}

function BenchmarkAdminPage({ navigate, queryString }) {
  const toast = useToast();
  const params = useMemoBa(() => new URLSearchParams(queryString || ''), [queryString]);
  const tab = baNormaliseTab(params.get('tab'));
  const categorySlug = params.get('category') || '';

  const [cats, setCats] = useStateBa({ loading: true, data: [], error: null });

  const reloadCats = useCallbackBa(async () => {
    setCats((s) => ({ ...s, loading: true }));
    try {
      const res = await apiFetch('/api/admin/public-benchmark/categories');
      setCats({ loading: false, data: (res && res.categories) || [], error: null });
    } catch (err) {
      setCats({ loading: false, data: [], error: err?.message || 'Could not load categories' });
    }
  }, []);
  useEffectBa(() => { reloadCats(); }, [reloadCats]);

  const selected = useMemoBa(
    () => cats.data.find((c) => c.slug === categorySlug) || null,
    [cats.data, categorySlug],
  );

  /** @param {{tab?: string, category?: string}} [opts] */
  function nav(opts) {
    const { tab: nextTab, category: nextCat } = opts || {};
    const t = baNormaliseTab(nextTab || tab);
    const c = nextCat != null ? nextCat : categorySlug;
    const qs = new URLSearchParams();
    if (c) qs.set('category', c);
    if (t !== 'vendors') qs.set('tab', t);
    const q = qs.toString();
    navigate(q ? `/benchmark-admin?${q}` : '/benchmark-admin');
  }

  async function toggleEnabled(cat, enabled) {
    try {
      await apiFetch('/api/admin/public-benchmark/categories', {
        method: 'PATCH',
        body: JSON.stringify({ slug: cat.slug, enabled }),
      });
      setCats((s) => ({ ...s, data: s.data.map((c) => (c.slug === cat.slug ? { ...c, enabled } : c)) }));
      toast?.show?.({ kind: 'success', message: `${cat.label} ${enabled ? 'is enabled on the public benchmark (appears automatically once it has aggregated runs)' : 'is hidden from the public benchmark'}.` });
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not update category' });
    }
  }

  return (
    <div className="page-inner org-benchmark-page">
      <div className="ui-page-head">
        <div>
          <h1 className="ui-page-title">Benchmark Admin</h1>
          <p className="ui-page-subtitle">
            Orchestrate the <strong>public</strong> benchmark. Editing a category's vendors or
            workflows changes <code>/leaderboard/&lt;category&gt;</code> on the next aggregator run
            (hourly at :13). Sourced from the house benchmark org.
          </p>
        </div>
        {selected && (
          <div className="ui-page-actions">
            <a className="ui-button ui-button--secondary ui-button--sm"
               href={`/leaderboard/${encodeURIComponent(selected.slug)}`}
               target="_blank" rel="noopener noreferrer">
              <span>View public leaderboard</span>
            </a>
          </div>
        )}
      </div>

      <div className="ob-category-picker">
        {cats.loading ? (
          <div className="ob-loading">Loading categories…</div>
        ) : cats.error ? (
          <div className="ob-error" role="alert">{cats.error}</div>
        ) : (
          <Select
            label="Category"
            value={categorySlug}
            placeholder="Pick a category to edit…"
            options={cats.data.map((c) => ({
              value: c.slug,
              label: `${c.label}${c.enabled ? ' · shown' : ''} (${c.vendorCount}v · ${c.workflowCount}w)`,
            }))}
            searchable
            portal
            onChange={(slug) => nav({ category: slug })} />
        )}
      </div>

      {!selected && !cats.loading && !cats.error && (
        <div className="ob-empty" style={{ marginTop: 16 }}>
          Pick a category to edit its public vendor list + workflow columns, or toggle whether it
          shows on the public benchmark. Categories with no aggregated runs are hidden automatically.
        </div>
      )}

      {selected && (
        <>
          <div className="ob-pane-head" style={{ marginTop: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
            <h2 style={{ margin: 0 }}>{selected.label}</h2>
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
              <label className="ob-checkbox">
                <input type="checkbox" checked={selected.enabled} onChange={(e) => toggleEnabled(selected, e.target.checked)} />
                <span>Show on public benchmark</span>
              </label>
              <div className="ob-hint" style={{ fontSize: 11, color: 'var(--text-3)', maxWidth: 280, textAlign: 'right' }}>
                Categories with no aggregated runs are hidden automatically.
              </div>
            </div>
          </div>

          <div className="mcp-inst-toolbar" style={{ margin: '12px 0 16px' }}>
            <div className="mcp-inst-tabs" role="tablist" aria-label="Benchmark admin sub-tabs">
              {BA_TABS.map((t) => (
                <button key={t.key} type="button" role="tab" aria-selected={tab === t.key}
                        className={`mcp-inst-tab ${tab === t.key ? 'active' : ''}`}
                        onClick={() => nav({ tab: t.key })}>
                  <span>{t.label}</span>
                </button>
              ))}
            </div>
          </div>

          {tab === 'vendors'
            ? <BaVendorsPane categorySlug={selected.slug} toast={toast} onCount={reloadCats} />
            : tab === 'workflows'
              ? <BaWorkflowsPane categorySlug={selected.slug} toast={toast} onCount={reloadCats} />
              : <BaGapsPane categorySlug={selected.slug} toast={toast} />}
        </>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Vendors
// ─────────────────────────────────────────────────────────────────────────────

function BaVendorsPane({ categorySlug, toast, onCount }) {
  const [state, setState] = useStateBa({ loading: true, vendors: [], error: null });
  const [busy, setBusy] = useStateBa(null);
  const [editing, setEditing] = useStateBa(null); // vendor being edited, or null

  const reload = useCallbackBa(async () => {
    setState((s) => ({ ...s, loading: true }));
    try {
      const res = await apiFetch(`/api/admin/public-benchmark/vendors?category=${encodeURIComponent(categorySlug)}`);
      setState({ loading: false, vendors: (res && res.vendors) || [], error: null });
    } catch (err) {
      setState({ loading: false, vendors: [], error: err?.message || 'Could not load vendors' });
    }
  }, [categorySlug]);
  useEffectBa(() => { reload(); setEditing(null); }, [reload]);

  async function upsert(payload) {
    setBusy(payload.vendor_slug || 'new');
    try {
      await apiFetch(`/api/admin/public-benchmark/vendors?category=${encodeURIComponent(categorySlug)}`, {
        method: 'POST', body: JSON.stringify(payload),
      });
      await reload(); onCount?.();
      setEditing(null);
      toast?.show?.({ kind: 'success', message: 'Vendor saved.' });
      return true;
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not save vendor' });
      return false;
    } finally { setBusy(null); }
  }
  async function toggleEnabled(v) {
    const next = !v.enabled;
    setBusy(v.vendorSlug);
    try {
      await apiFetch(`/api/admin/public-benchmark/vendors?category=${encodeURIComponent(categorySlug)}`, {
        method: 'PATCH', body: JSON.stringify({ vendor_slug: v.vendorSlug, enabled: next }),
      });
      setState((s) => ({
        ...s,
        vendors: s.vendors.map((x) => (x.vendorSlug === v.vendorSlug ? { ...x, enabled: next } : x)),
      }));
      toast?.show?.({ kind: 'success', message: `${v.name} ${next ? 'activated' : 'deactivated'}.` });
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not update vendor' });
    } finally { setBusy(null); }
  }
  async function remove(v) {
    if (!window.confirm(`Remove ${v.name} from the public ${categorySlug} benchmark?`)) return;
    setBusy(v.vendorSlug);
    try {
      await apiFetch(`/api/admin/public-benchmark/vendors?category=${encodeURIComponent(categorySlug)}`, {
        method: 'DELETE', body: JSON.stringify({ vendor_slug: v.vendorSlug }),
      });
      if (editing && editing.vendorSlug === v.vendorSlug) setEditing(null);
      await reload(); onCount?.();
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not remove vendor' });
    } finally { setBusy(null); }
  }

  // Stage a file upload as a base64 string and POST it to the vendor-icon
  // route. The route writes icon_path on the vendor row, which beats
  // icon_slug in resolveVendorIconUrl, so this overrides any Simple Icons
  // fallback the row may have had.
  async function uploadIcon(v, file) {
    if (!file) return;
    // Match the server-side cap in lib/storage/supabase-storage.js so a
    // staff member who picks a 5 MB raw PNG gets immediate feedback
    // instead of waiting through a base64 encode + POST.
    if (file.size > 512 * 1024) {
      toast?.show?.({ kind: 'error', message: `Logo must be 512 KB or smaller (this one is ${Math.round(file.size / 1024)} KB).` });
      return;
    }
    setBusy(v.vendorSlug);
    try {
      const base64 = await new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(String(reader.result || ''));
        reader.onerror = () => reject(reader.error || new Error('Could not read file'));
        reader.readAsDataURL(file);
      });
      await apiFetch(`/api/admin/public-benchmark/vendor-icon?category=${encodeURIComponent(categorySlug)}`, {
        method: 'POST',
        body: JSON.stringify({
          vendor_slug: v.vendorSlug,
          content_type: file.type,
          base64,
        }),
      });
      await reload();
      toast?.show?.({ kind: 'success', message: `Logo updated for ${v.name}.` });
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not upload logo' });
    } finally { setBusy(null); }
  }

  if (state.loading) return <div className="ob-loading">Loading…</div>;
  if (state.error) return <div className="ob-error" role="alert">{state.error}</div>;

  return (
    <section className="ob-pane">
      <div className="ob-pane-head">
        <h2>Vendors</h2>
        <p>Each row is a vendor column on the public leaderboard. Slug must match the house org's MCP server name resolution; brand color + icon drive the display. Use the upload button to attach a custom logo (PNG/JPEG/SVG/WebP, max 512 KB) — uploaded images take precedence over the Simple Icons slug. Use the eye toggle to hide a vendor from the public benchmark without losing its configuration.</p>
      </div>
      {state.vendors.length === 0 && <p className="ob-empty">No vendors yet. Add one below.</p>}
      <ul className="ob-list ob-vendor-list">
        {state.vendors.map((v) => (
          <li key={v.id} className={`ob-list-row ob-vendor-row ${editing && editing.id === v.id ? 'is-editing' : ''} ${v.enabled === false ? 'is-disabled' : ''}`}>
            <div className="ob-vendor-icon" style={{ background: v.iconUrl ? '#fff' : (v.brandColor || 'var(--surface-soft,#f3f4f6)') }}>
              {v.iconUrl
                ? <img src={v.iconUrl} alt={`${v.name} logo`} loading="lazy" />
                : <span className="ob-vendor-placeholder">{(v.name || '?').slice(0, 2).toUpperCase()}</span>}
            </div>
            <div className="ob-vendor-main">
              <div className="ob-vendor-name">
                <span>{v.name}</span>
                {v.enabled === false && <span className="ob-vendor-inactive-tag">Inactive</span>}
              </div>
              <div className="ob-vendor-meta"><code>{v.vendorSlug}</code>{v.iconSlug ? ` · icon:${v.iconSlug}` : ''}{v.brandColor ? ` · ${v.brandColor}` : ''}</div>
            </div>
            <div className="ob-row-actions">
              <button type="button"
                      className={`ob-icon-btn ${v.enabled === false ? 'ob-icon-btn--muted' : ''}`}
                      title={v.enabled === false ? 'Activate (show on public benchmark)' : 'Deactivate (hide from public benchmark)'}
                      aria-label={v.enabled === false ? `Activate ${v.name}` : `Deactivate ${v.name}`}
                      aria-pressed={v.enabled !== false}
                      disabled={busy === v.vendorSlug}
                      onClick={() => toggleEnabled(v)}>
                <Icon name={v.enabled === false ? 'eyeOff' : 'eye'} size={14} />
              </button>
              <label className="ob-icon-btn" title="Upload logo (PNG/JPEG/SVG/WebP, max 512 KB)"
                     aria-label={`Upload logo for ${v.name}`}
                     style={{ cursor: busy === v.vendorSlug ? 'wait' : 'pointer' }}>
                <Icon name="upload" size={14} />
                <input type="file" accept="image/png,image/jpeg,image/svg+xml,image/webp"
                       disabled={busy === v.vendorSlug}
                       style={{ position: 'absolute', width: 1, height: 1, opacity: 0, pointerEvents: 'none' }}
                       onChange={(e) => {
                         const file = e.target.files && e.target.files[0];
                         e.target.value = '';
                         if (file) uploadIcon(v, file);
                       }} />
              </label>
              <button type="button" className="ob-icon-btn" title="Edit vendor" aria-label={`Edit ${v.name}`}
                      onClick={() => setEditing(v)}>
                <Icon name="edit" size={14} />
              </button>
              <button type="button" className="ob-icon-btn ob-icon-btn--danger" title="Remove vendor" aria-label={`Remove ${v.name}`}
                      disabled={busy === v.vendorSlug} onClick={() => remove(v)}>
                <Icon name="trash" size={14} />
              </button>
            </div>
          </li>
        ))}
      </ul>
      <div className="ob-pane-subhead"><h3>{editing ? `Edit vendor · ${editing.vendorSlug}` : 'Add vendor'}</h3></div>
      <BaVendorForm busy={busy === 'new' || (editing && busy === editing.vendorSlug)} editing={editing}
                    onCancel={() => setEditing(null)} onSubmit={upsert} />
    </section>
  );
}

function BaVendorForm({ busy, editing, onCancel, onSubmit }) {
  const [slug, setSlug] = useStateBa('');
  const [name, setName] = useStateBa('');
  const [color, setColor] = useStateBa('');
  const [iconSlug, setIconSlug] = useStateBa('');
  // Sync the form when the row's Edit button selects a vendor (or clears).
  useEffectBa(() => {
    setSlug(editing?.vendorSlug || '');
    setName(editing?.name || '');
    setColor(editing?.brandColor || '');
    setIconSlug(editing?.iconSlug || '');
  }, [editing]);
  return (
    <form className="ob-custom-form" style={{ flexWrap: 'wrap' }}
          onSubmit={async (e) => {
            e.preventDefault();
            if (!slug.trim() || !name.trim()) return;
            const ok = await onSubmit({
              vendor_slug: slug.trim(), name: name.trim(),
              brand_color: color.trim() || null, icon_slug: iconSlug.trim() || null,
            });
            if (ok && !editing) { setSlug(''); setName(''); setColor(''); setIconSlug(''); }
          }}>
      <input type="text" placeholder="slug (lower_snake)" value={slug} disabled={!!editing}
             title={editing ? 'Slug is the vendor identity and cannot be changed; remove + re-add to rename it' : ''}
             onChange={(e) => setSlug(e.target.value)} />
      <input type="text" placeholder="Display name" value={name} onChange={(e) => setName(e.target.value)} />
      <input type="text" placeholder="#brand" value={color} onChange={(e) => setColor(e.target.value)} style={{ width: 90 }} />
      <input type="text" placeholder="icon slug (optional)" value={iconSlug} onChange={(e) => setIconSlug(e.target.value)} />
      <button type="submit" className="ui-button ui-button--primary ui-button--sm" disabled={busy || !slug.trim() || !name.trim()}>
        {busy ? 'Saving…' : editing ? 'Update vendor' : 'Save vendor'}
      </button>
      {editing && (
        <button type="button" className="ui-button ui-button--ghost ui-button--sm" onClick={onCancel}>Cancel</button>
      )}
    </form>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Workflows
// ─────────────────────────────────────────────────────────────────────────────

function BaWorkflowsPane({ categorySlug, toast, onCount }) {
  const [state, setState] = useStateBa({ loading: true, workflows: [], error: null });
  const [busy, setBusy] = useStateBa(null);
  const [editing, setEditing] = useStateBa(null);

  const reload = useCallbackBa(async () => {
    setState((s) => ({ ...s, loading: true }));
    try {
      const res = await apiFetch(`/api/admin/public-benchmark/workflows?category=${encodeURIComponent(categorySlug)}`);
      setState({ loading: false, workflows: (res && res.workflows) || [], error: null });
    } catch (err) {
      setState({ loading: false, workflows: [], error: err?.message || 'Could not load workflows' });
    }
  }, [categorySlug]);
  useEffectBa(() => { reload(); setEditing(null); }, [reload]);

  async function upsert(payload) {
    setBusy(payload.workflow_slug || 'new');
    try {
      await apiFetch(`/api/admin/public-benchmark/workflows?category=${encodeURIComponent(categorySlug)}`, {
        method: 'POST', body: JSON.stringify(payload),
      });
      await reload(); onCount?.();
      setEditing(null);
      toast?.show?.({ kind: 'success', message: 'Workflow saved.' });
      return true;
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not save workflow' });
      return false;
    } finally { setBusy(null); }
  }
  async function remove(w) {
    if (!window.confirm(`Remove "${w.name}" from the public ${categorySlug} benchmark?`)) return;
    setBusy(w.workflowSlug);
    try {
      await apiFetch(`/api/admin/public-benchmark/workflows?category=${encodeURIComponent(categorySlug)}`, {
        method: 'DELETE', body: JSON.stringify({ workflow_slug: w.workflowSlug }),
      });
      if (editing && editing.workflowSlug === w.workflowSlug) setEditing(null);
      await reload(); onCount?.();
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not remove workflow' });
    } finally { setBusy(null); }
  }

  if (state.loading) return <div className="ob-loading">Loading…</div>;
  if (state.error) return <div className="ob-error" role="alert">{state.error}</div>;

  return (
    <section className="ob-pane">
      <div className="ob-pane-head">
        <h2>Workflows</h2>
        <p>Each row is a workflow column on the public leaderboard, in order. Slug must match the house org's workflow slug; "short" is the column header.</p>
      </div>
      {state.workflows.length === 0 && <p className="ob-empty">No workflows yet. Add one below.</p>}
      <ul className="ob-list">
        {state.workflows.map((w) => (
          <li key={w.id} className={`ob-list-row ${editing && editing.id === w.id ? 'is-editing' : ''}`}>
            <div>
              <div><strong>{w.short}</strong> — {w.name}</div>
              <div className="ob-vendor-meta"><code>{w.workflowSlug}</code></div>
            </div>
            <div className="ob-row-actions">
              <button type="button" className="ob-icon-btn" title="Edit workflow" aria-label={`Edit ${w.name}`}
                      onClick={() => setEditing(w)}>
                <Icon name="edit" size={14} />
              </button>
              <button type="button" className="ob-icon-btn ob-icon-btn--danger" title="Remove workflow" aria-label={`Remove ${w.name}`}
                      disabled={busy === w.workflowSlug} onClick={() => remove(w)}>
                <Icon name="trash" size={14} />
              </button>
            </div>
          </li>
        ))}
      </ul>
      <div className="ob-pane-subhead"><h3>{editing ? `Edit workflow · ${editing.workflowSlug}` : 'Add workflow'}</h3></div>
      <BaWorkflowForm busy={busy === 'new' || (editing && busy === editing.workflowSlug)} editing={editing}
                      onCancel={() => setEditing(null)} onSubmit={upsert} />
    </section>
  );
}

function BaWorkflowForm({ busy, editing, onCancel, onSubmit }) {
  const [slug, setSlug] = useStateBa('');
  const [name, setName] = useStateBa('');
  const [short, setShort] = useStateBa('');
  useEffectBa(() => {
    setSlug(editing?.workflowSlug || '');
    setName(editing?.name || '');
    setShort(editing?.short || '');
  }, [editing]);
  return (
    <form className="ob-custom-form" style={{ flexWrap: 'wrap' }}
          onSubmit={async (e) => {
            e.preventDefault();
            if (!slug.trim() || !name.trim()) return;
            const ok = await onSubmit({ workflow_slug: slug.trim(), name: name.trim(), short: short.trim() || null });
            if (ok && !editing) { setSlug(''); setName(''); setShort(''); }
          }}>
      <input type="text" placeholder="slug (lower_snake)" value={slug} disabled={!!editing}
             title={editing ? 'Slug is the workflow identity and cannot be changed; remove + re-add to rename it' : ''}
             onChange={(e) => setSlug(e.target.value)} />
      <input type="text" placeholder="Full name" value={name} onChange={(e) => setName(e.target.value)} style={{ flex: '1 1 240px' }} />
      <input type="text" placeholder="Short (column header)" value={short} onChange={(e) => setShort(e.target.value)} />
      <button type="submit" className="ui-button ui-button--primary ui-button--sm" disabled={busy || !slug.trim() || !name.trim()}>
        {busy ? 'Saving…' : editing ? 'Update workflow' : 'Save workflow'}
      </button>
      {editing && (
        <button type="button" className="ui-button ui-button--ghost ui-button--sm" onClick={onCancel}>Cancel</button>
      )}
    </form>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Capability gaps
// ─────────────────────────────────────────────────────────────────────────────
//
// Workflow-grouped editor. For each workflow in the category, expand the
// row to see every vendor as a toggle. A vendor switched OFF means "this
// vendor's product cannot do this workflow" — the aggregator promotes
// the absence to public_benchmark_cells.capability_gap on the next pass,
// the public leaderboard renders the cell as "N/A — capability gap",
// and the vendor's macro-averaged rating drops the cell from the mean.
//
// Toggle-on (supported) is the default — gaps are the exception. A row
// counter shows "X of Y vendors flagged" so staff can scan which
// workflows have the most coverage holes.

function BaGapsPane({ categorySlug, toast }) {
  const [state, setState] = useStateBa({ loading: true, gaps: [], vendors: [], workflows: [], error: null });
  const [expanded, setExpanded] = useStateBa(() => new Set());
  // Per-pair pending toggle so the UI flashes a spinner only on the cell
  // being mutated, not the whole pane.
  const [busy, setBusy] = useStateBa(() => new Set());

  const reload = useCallbackBa(async () => {
    setState((s) => ({ ...s, loading: true }));
    try {
      const [gapsRes, vendorsRes, workflowsRes] = await Promise.all([
        apiFetch(`/api/admin/public-benchmark/capability-gaps?category=${encodeURIComponent(categorySlug)}`),
        apiFetch(`/api/admin/public-benchmark/vendors?category=${encodeURIComponent(categorySlug)}`),
        apiFetch(`/api/admin/public-benchmark/workflows?category=${encodeURIComponent(categorySlug)}`),
      ]);
      setState({
        loading: false,
        gaps: (gapsRes && gapsRes.gaps) || [],
        vendors: (vendorsRes && vendorsRes.vendors) || [],
        workflows: (workflowsRes && workflowsRes.workflows) || [],
        error: null,
      });
    } catch (err) {
      setState({ loading: false, gaps: [], vendors: [], workflows: [], error: err?.message || 'Could not load capability gaps' });
    }
  }, [categorySlug]);
  useEffectBa(() => { reload(); }, [reload]);

  // Index gaps by "vendor|workflow" so toggles can flip in O(1) and the
  // row counter doesn't rescan the whole list per-render.
  const gapByPair = useMemoBa(() => {
    const m = new Map();
    for (const g of state.gaps) m.set(`${g.vendorSlug}|${g.workflowSlug}`, g);
    return m;
  }, [state.gaps]);

  function toggleExpanded(workflowSlug) {
    setExpanded((prev) => {
      const next = new Set(prev);
      if (next.has(workflowSlug)) next.delete(workflowSlug);
      else next.add(workflowSlug);
      return next;
    });
  }

  function markBusy(key, on) {
    setBusy((prev) => {
      const next = new Set(prev);
      if (on) next.add(key); else next.delete(key);
      return next;
    });
  }

  // Toggle a single (vendor, workflow) pair. `supported=true` removes the
  // gap; `supported=false` creates one. Optimistic-update the local gap
  // list so the toggle flips immediately; reload on failure.
  async function setPairSupported(vendorSlug, workflowSlug, supported, reason) {
    const key = `${vendorSlug}|${workflowSlug}`;
    markBusy(key, true);
    try {
      if (supported) {
        await apiFetch(`/api/admin/public-benchmark/capability-gaps?category=${encodeURIComponent(categorySlug)}`, {
          method: 'DELETE',
          body: JSON.stringify({ vendor_slug: vendorSlug, workflow_slug: workflowSlug }),
        });
        setState((s) => ({
          ...s,
          gaps: s.gaps.filter((g) => !(g.vendorSlug === vendorSlug && g.workflowSlug === workflowSlug)),
        }));
      } else {
        const res = await apiFetch(`/api/admin/public-benchmark/capability-gaps?category=${encodeURIComponent(categorySlug)}`, {
          method: 'POST',
          body: JSON.stringify({ vendor_slug: vendorSlug, workflow_slug: workflowSlug, reason: reason || null }),
        });
        const saved = (res && res.gap) || { vendorSlug, workflowSlug, reason: reason || null };
        setState((s) => {
          const others = s.gaps.filter((g) => !(g.vendorSlug === vendorSlug && g.workflowSlug === workflowSlug));
          return { ...s, gaps: [...others, saved] };
        });
      }
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not update capability gap' });
      await reload();
    } finally { markBusy(key, false); }
  }

  async function updateReason(vendorSlug, workflowSlug, reason) {
    const key = `${vendorSlug}|${workflowSlug}`;
    markBusy(key, true);
    try {
      const res = await apiFetch(`/api/admin/public-benchmark/capability-gaps?category=${encodeURIComponent(categorySlug)}`, {
        method: 'POST',
        body: JSON.stringify({ vendor_slug: vendorSlug, workflow_slug: workflowSlug, reason: reason || null }),
      });
      const saved = (res && res.gap) || { vendorSlug, workflowSlug, reason: reason || null };
      setState((s) => {
        const others = s.gaps.filter((g) => !(g.vendorSlug === vendorSlug && g.workflowSlug === workflowSlug));
        return { ...s, gaps: [...others, saved] };
      });
    } catch (err) {
      toast?.show?.({ kind: 'error', message: err?.message || 'Could not update reason' });
      await reload();
    } finally { markBusy(key, false); }
  }

  if (state.loading) return <div className="ob-loading">Loading…</div>;
  if (state.error) return <div className="ob-error" role="alert">{state.error}</div>;

  const { vendors, workflows } = state;

  return (
    <section className="ob-pane">
      <div className="ob-pane-head">
        <h2>Capability gaps</h2>
        <p>
          For each workflow, switch OFF the vendors whose product simply doesn't ship the capability the workflow exercises (e.g. Neon does not ship edge functions, so it's a capability gap on <code>edge_fn_lifecycle</code>). Those cells render as <em>N/A — capability gap</em> on the public leaderboard and are excluded from the vendor's macro-averaged rating, so a vendor isn't penalised for a feature its product never claimed to ship. Toggles save instantly; changes propagate on the next aggregator pass (hourly at :13).
        </p>
      </div>

      {workflows.length === 0 ? (
        <p className="ob-empty">No workflows yet. Add some in the Workflows tab first.</p>
      ) : vendors.length === 0 ? (
        <p className="ob-empty">No vendors yet. Add some in the Vendors tab first.</p>
      ) : (
        <ul className="ob-list" style={{ display: 'grid', gap: 8 }}>
          {workflows.map((w) => {
            const isOpen = expanded.has(w.workflowSlug);
            const flagged = vendors.filter((v) => gapByPair.has(`${v.vendorSlug}|${w.workflowSlug}`)).length;
            return (
              <li key={w.id} className="ob-list-row"
                  style={{ display: 'block', padding: 0 }}>
                <button type="button"
                        onClick={() => toggleExpanded(w.workflowSlug)}
                        aria-expanded={isOpen}
                        style={{
                          width: '100%', display: 'flex', alignItems: 'center',
                          justifyContent: 'space-between', gap: 12,
                          padding: '12px 14px', background: 'transparent',
                          border: 0, cursor: 'pointer', textAlign: 'left',
                        }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
                    <Icon name={isOpen ? 'chevronDown' : 'chevronRight'} size={14} />
                    <div style={{ minWidth: 0 }}>
                      <div><strong>{w.short}</strong> — {w.name}</div>
                      <div className="ob-vendor-meta"><code>{w.workflowSlug}</code></div>
                    </div>
                  </div>
                  <span className={`ob-vendor-inactive-tag`}
                        style={{
                          background: flagged > 0 ? 'rgba(200, 65, 13, 0.12)' : 'transparent',
                          color: flagged > 0 ? 'var(--brand, #c8410d)' : 'var(--text-3)',
                          borderColor: flagged > 0 ? 'rgba(200, 65, 13, 0.3)' : 'var(--border)',
                        }}
                        title="Number of vendors marked as a capability gap for this workflow">
                    {flagged} of {vendors.length} flagged
                  </span>
                </button>
                {isOpen && (
                  <div style={{
                    borderTop: '1px solid var(--border)',
                    padding: '8px 14px 14px',
                    background: 'var(--surface-soft, rgba(0,0,0,0.02))',
                  }}>
                    <BaWorkflowGapToggles
                      workflow={w}
                      vendors={vendors}
                      gapByPair={gapByPair}
                      busy={busy}
                      onToggle={setPairSupported}
                      onReason={updateReason} />
                  </div>
                )}
              </li>
            );
          })}
        </ul>
      )}
    </section>
  );
}

function BaWorkflowGapToggles({ workflow, vendors, gapByPair, busy, onToggle, onReason }) {
  return (
    <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'grid', gap: 6 }}>
      {vendors.map((v) => {
        const key = `${v.vendorSlug}|${workflow.workflowSlug}`;
        const gap = gapByPair.get(key);
        const supported = !gap;
        const isBusy = busy.has(key);
        return (
          <li key={v.id} style={{
            display: 'grid',
            gridTemplateColumns: '180px auto 1fr',
            gap: 12, alignItems: 'center',
            padding: '6px 8px',
            background: supported ? 'transparent' : 'rgba(200, 65, 13, 0.06)',
            border: '1px solid var(--border-soft, var(--border))',
          }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
              <div className="ob-vendor-icon"
                   style={{
                     width: 22, height: 22,
                     background: v.iconUrl ? '#fff' : (v.brandColor || 'var(--surface-soft,#f3f4f6)'),
                   }}>
                {v.iconUrl
                  ? <img src={v.iconUrl} alt="" loading="lazy" />
                  : <span className="ob-vendor-placeholder">{(v.name || '?').slice(0, 1).toUpperCase()}</span>}
              </div>
              <span style={{ fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                {v.name}
              </span>
            </div>
            <label className="ob-checkbox" style={{ whiteSpace: 'nowrap' }}>
              <input type="checkbox"
                     checked={supported}
                     disabled={isBusy}
                     onChange={(e) => onToggle(v.vendorSlug, workflow.workflowSlug, e.target.checked, null)} />
              <span style={{ color: supported ? 'var(--text-2)' : 'var(--brand, #c8410d)', fontWeight: supported ? 400 : 600 }}>
                {supported ? 'Supported' : 'Capability gap'}
              </span>
            </label>
            <BaGapReason
              disabled={supported || isBusy}
              value={gap?.reason || ''}
              placeholder={supported ? '—' : 'Reason (optional, shown on hover)'}
              onSave={(text) => onReason(v.vendorSlug, workflow.workflowSlug, text)} />
          </li>
        );
      })}
    </ul>
  );
}

function BaGapReason({ value, placeholder, disabled, onSave }) {
  const [draft, setDraft] = useStateBa(value);
  useEffectBa(() => { setDraft(value); }, [value]);
  const dirty = draft !== value;
  return (
    <input type="text"
           value={draft}
           placeholder={placeholder}
           disabled={disabled}
           onChange={(e) => setDraft(e.target.value)}
           onBlur={() => { if (dirty && !disabled) onSave(draft.trim() || null); }}
           onKeyDown={(e) => {
             if (e.key === 'Enter' && dirty && !disabled) { e.preventDefault(); onSave(draft.trim() || null); }
             if (e.key === 'Escape') setDraft(value);
           }}
           style={{
             width: '100%',
             fontStyle: disabled ? 'italic' : 'normal',
             color: disabled ? 'var(--text-4)' : undefined,
             background: dirty ? 'rgba(200, 65, 13, 0.08)' : undefined,
           }} />
  );
}

window.BenchmarkAdminPage = BenchmarkAdminPage;
