/* global React, useApi, useMeta, useAuth, Loading, ApiError, Tag, SignInButton, formatMoney, LiveMarketGrid, AccuracyLineChart */

// =========================================================================
// PROFILE / TRACK — user's tracked prompts. Auth-gated.
// =========================================================================
function ProfilePage({ navigate }) {
  const { user, signedIn, meta, tier } = useAuth();
  const { models, categories } = useMeta();
  const prompts = useApi(
    () => signedIn ? window.CloudLayerAPI.listMyPrompts() : Promise.resolve({ items: [] }),
    [signedIn],
  );
  const [editing, setEditing] = React.useState(null); // prompt id or 'new'
  // Which prompt row is expanded — at most one at a time. Must live ABOVE
  // the !signedIn early-return so the hook count stays stable across the
  // signed-out → signed-in transition (React errors otherwise).
  const [expandedId, setExpandedId] = React.useState(null);

  React.useEffect(() => {
    if (!signedIn) return;
    const t = setInterval(prompts.refresh, 30_000);
    return () => clearInterval(t);
  }, [signedIn, prompts.refresh]);

  if (!signedIn) {
    return (
      <div data-screen-label="06 Track (signed-out)">
        <div className="page-head">
          <div className="container">
            <div className="crumb">Track</div>
            <h1 style={{ fontSize: 64, fontFamily: 'var(--ff-display)', fontWeight: 300, letterSpacing: '-0.025em', margin: 0 }}>
              Track your own <em style={{ fontStyle: 'italic', color: 'var(--accent)' }}>prompt</em>
            </h1>
            <div className="dim" style={{ marginTop: 16, fontSize: 16, maxWidth: 640, lineHeight: 1.6 }}>
              Sign in to ship a custom forecasting prompt. The runner replays it against the top-volume markets every 15 minutes — your prompt's accuracy joins the community leaderboard alongside the cold prompt.
            </div>
          </div>
        </div>
        <div className="container" style={{ padding: '48px 32px' }}>
          <div className="panel" style={{ padding: 36, maxWidth: 480 }}>
            <h3 style={{ fontFamily: 'var(--ff-display)', fontWeight: 400, fontSize: 22, marginTop: 0 }}>Sign in to continue</h3>
            {meta?.auth?.googleEnabled ? (
              <div style={{ marginTop: 12 }}>
                <SignInButton />
              </div>
            ) : (
              <>
                <div className="dim" style={{ fontSize: 13, lineHeight: 1.6, marginBottom: 16 }}>
                  Google sign-in isn't configured yet. Set <code className="mono">GOOGLE_OAUTH_CLIENT_ID</code> in <code className="mono">.env</code> and restart the server.
                </div>
                <button className="btn" disabled>Sign in (unavailable)</button>
              </>
            )}
          </div>
        </div>
      </div>
    );
  }

  const items = prompts.data?.items || [];
  const editingPrompt = editing === 'new' ? null : items.find((p) => p._id === editing);

  const previewMode = meta?.auth?.devBypass;
  const maxPrompts = tier?.maxPrompts ?? 2;
  const marketsPerCategory = tier?.marketsPerCategory ?? 5;
  const atLimit = items.length >= maxPrompts;

  return (
    <div data-screen-label="06 Track">
      {previewMode && (
        <div style={{
          background: 'rgba(212, 174, 90, 0.12)',
          borderBottom: '1px solid var(--line)',
          padding: '8px 16px',
          textAlign: 'center',
          fontSize: 12,
          fontFamily: 'var(--ff-mono, monospace)',
        }}>
          PREVIEW MODE · acting as <strong>@preview</strong> · DEV_AUTH_BYPASS is on, anyone hitting this server is treated as this user
        </div>
      )}
      <div className="page-head">
        <div className="container">
          <div className="crumb">
            <span style={{ cursor: 'pointer' }} onClick={() => navigate('home')}>Benchmark</span>
            <span style={{ margin: '0 8px' }}>/</span>
            <span>Track</span>
          </div>
          <div style={{ display: 'flex', gap: 32, alignItems: 'flex-end' }}>
            <div className="avatar" style={{ width: 80, height: 80, fontSize: 28 }}>
              {user.picture
                ? <img src={user.picture} alt="" width="80" height="80" style={{ borderRadius: '50%' }} />
                : (user.name || user.email).slice(0, 1).toUpperCase()}
            </div>
            <div style={{ flex: 1 }}>
              <h1 style={{ margin: 0, fontSize: 56, fontFamily: 'var(--ff-display)', fontWeight: 300, letterSpacing: '-0.025em' }}>
                @{user.handle || user.email.split('@')[0]}
              </h1>
              <div className="dim" style={{ fontSize: 14, marginTop: 8 }}>
                {user.email} · joined {new Date(user.joinedAt).toLocaleDateString()}
              </div>
            </div>
            {atLimit && !editing ? (
              <button className="btn" disabled title={`Free tier allows ${maxPrompts} prompts. Upgrade to add more.`}>
                {maxPrompts} of {maxPrompts} used
              </button>
            ) : (
              <button className="btn btn-accent" onClick={() => setEditing('new')}>+ Track a prompt</button>
            )}
          </div>
          <div className="stat-strip">
            <div className="stat">
              <div className="label">Prompts tracked</div>
              <div className="value mono">{items.length}<span className="faint" style={{ fontSize: 14, marginLeft: 6 }}>/ {maxPrompts}</span></div>
            </div>
            <div className="stat">
              <div className="label">Predictions made</div>
              <div className="value mono">{items.reduce((s, p) => s + (p.predictionCount || 0), 0)}</div>
            </div>
            <div className="stat">
              <div className="label">Best rank</div>
              <div className="value mono">{user.bestRank || '—'}</div>
            </div>
            <div className="stat">
              <div className="label">Tier</div>
              <div className="value mono" style={{ textTransform: 'capitalize', display: 'flex', alignItems: 'baseline', gap: 10 }}>
                <span>{tier?.name || 'free'}</span>
                {(tier?.name || 'free') === 'free' && (
                  <button
                    className="btn-ghost"
                    onClick={() => navigate('upgrade')}
                    style={{
                      fontSize: 11, padding: '4px 10px',
                      color: 'var(--accent)', borderColor: 'var(--accent)',
                    }}
                  >Upgrade →</button>
                )}
              </div>
            </div>
          </div>
        </div>
      </div>

      <section className="container" style={{ padding: '40px 32px 96px' }}>
        {editing && (
          <div style={{ marginBottom: 32 }}>
            <PromptEditor
              prompt={editingPrompt}
              models={models}
              categories={categories}
              marketsPerCategory={marketsPerCategory}
              minMarketVolume={tier?.minMarketVolume ?? 1000}
              maxModelsPerPrompt={tier?.maxModelsPerPrompt ?? 1}
              navigate={navigate}
              onCancel={() => setEditing(null)}
              onSaved={() => { setEditing(null); prompts.refresh(); }}
            />
          </div>
        )}

        {prompts.error ? <ApiError error={prompts.error} /> : prompts.loading && !prompts.data ? <Loading label="Loading prompts…" /> : (
          items.length === 0 && !editing ? (
            <div className="panel" style={{ padding: 36, textAlign: 'center' }}>
              <div className="dim" style={{ marginBottom: 12 }}>You haven't tracked any prompts yet.</div>
              <button className="btn btn-accent" onClick={() => setEditing('new')}>Track your first prompt →</button>
            </div>
          ) : (
            <table className="tbl">
              <thead>
                <tr>
                  <th>Name</th>
                  <th>Model</th>
                  <th>Categories</th>
                  <th>Status</th>
                  <th className="num">Accuracy</th>
                  <th className="num">Predictions</th>
                  <th />
                </tr>
              </thead>
              <tbody>
                {items.map((p) => {
                  // Multi-model: list of full model objects for the dots.
                  // Single-model legacy: fall back to modelId.
                  const ids = (p.modelIds && p.modelIds.length) ? p.modelIds : (p.modelId ? [p.modelId] : []);
                  const promptModels = ids.map((id) => models.find((m) => m.id === id)).filter(Boolean);
                  const primaryColor = promptModels[0]?.color || '#888';
                  const isOpen = expandedId === p._id;
                  return (
                    <React.Fragment key={p._id}>
                      <tr style={{ cursor: 'pointer' }}
                          onClick={(e) => {
                            if (e.target.closest('button')) return; // don't toggle when clicking edit
                            setExpandedId(isOpen ? null : p._id);
                          }}>
                        <td style={{ fontFamily: 'var(--ff-display)', fontSize: 18, fontWeight: 400 }}>
                          <span aria-hidden style={{
                            display: 'inline-block', width: 10, fontSize: 10, marginRight: 6,
                            transform: isOpen ? 'rotate(90deg)' : 'rotate(0deg)',
                            transition: 'transform .15s ease', opacity: 0.5,
                          }}>▸</span>
                          {p.name}
                          {p.tools?.length > 0 && (
                            <span className="mono" style={{
                              fontSize: 10, marginLeft: 8, padding: '2px 6px',
                              background: 'rgba(212, 174, 90, 0.15)', borderRadius: 4,
                              color: 'var(--accent)', verticalAlign: 'middle',
                            }} title={`Tools: ${p.tools.join(', ')}`}>
                              {p.tools.length === 1 ? p.tools[0].replace('_', ' ') : `${p.tools.length} tools`}
                            </span>
                          )}
                          {p.predictBeforeCloseHours > 0 && (
                            <span className="mono faint" style={{
                              fontSize: 10, marginLeft: 6, padding: '2px 6px',
                              border: '1px solid var(--line)', borderRadius: 4,
                              verticalAlign: 'middle',
                            }} title="Only predicts on markets resolving within this window">
                              ≤ {p.predictBeforeCloseHours}h
                            </span>
                          )}
                        </td>
                        <td>
                          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
                            <span style={{ display: 'inline-flex', gap: 2 }}>
                              {promptModels.map((m) => (
                                <span key={m.id} title={m.label}
                                      style={{ width: 8, height: 8, borderRadius: '50%', background: m.color }} />
                              ))}
                            </span>
                            <span className="mono" style={{ fontSize: 12 }}>
                              {promptModels.length === 0
                                ? (p.modelId || '—')
                                : promptModels.length === 1
                                ? promptModels[0].label
                                : `${promptModels.length} models`}
                            </span>
                          </span>
                        </td>
                        <td className="mono dim" style={{ fontSize: 12 }}>
                          {p.categories?.length ? p.categories.join(', ') : 'all'}
                        </td>
                        <td>
                          <Tag tone={p.status === 'live' ? 'live' : ''}>{p.status}</Tag>
                          {p.lastError && (
                            <span className="mono neg" style={{ fontSize: 10.5, marginLeft: 8 }} title={p.lastError}>error</span>
                          )}
                        </td>
                        <td className="num mono tnum">
                          {p.accuracy == null
                            ? <span className="faint" title={p.predictionCount === 0 ? 'No predictions yet' : 'Pending resolution'}>—</span>
                            : `${Math.round(p.accuracy * 100)}%`}
                        </td>
                        <td className="num mono tnum dim">{p.predictionCount || 0}</td>
                        <td className="num">
                          <button
                            className="btn-ghost"
                            style={{ fontSize: 11, padding: '4px 8px' }}
                            onClick={(e) => { e.stopPropagation(); setEditing(p._id); }}
                          >Edit</button>
                        </td>
                      </tr>
                      {isOpen && (
                        <tr>
                          <td colSpan={7} style={{ padding: 0, background: 'rgba(0,0,0,0.025)' }}>
                            <PromptDetailPanel
                              promptId={p._id}
                              modelColor={primaryColor}
                              navigate={navigate}
                            />
                          </td>
                        </tr>
                      )}
                    </React.Fragment>
                  );
                })}
              </tbody>
            </table>
          )
        )}
      </section>
    </div>
  );
}

// =========================================================================
// PromptEditor — create / edit / delete a tracked prompt.
// =========================================================================
function PromptEditor({ prompt, models, categories, onCancel, onSaved, navigate, marketsPerCategory = 5, minMarketVolume = 1000, maxModelsPerPrompt = 1 }) {
  const enabledModels = models.filter((m) => m.enabled);
  // Initial model selection. New prompts default to one enabled model;
  // existing prompts use their stored list (with legacy-modelId fallback).
  const initialModelIds = (prompt?.modelIds && prompt.modelIds.length)
    ? prompt.modelIds
    : (prompt?.modelId ? [prompt.modelId] : (enabledModels[0]?.id ? [enabledModels[0].id] : []));

  const [name, setName]             = React.useState(prompt?.name || '');
  const [promptText, setPromptText] = React.useState(prompt?.promptText || DEFAULT_PROMPT);
  const [modelIds, setModelIds]     = React.useState(initialModelIds);
  const [chosenCats, setChosenCats] = React.useState(prompt?.categories || []);
  const [chosenTools, setChosenTools] = React.useState(prompt?.tools || []);
  const [closeHours, setCloseHours] = React.useState(prompt?.predictBeforeCloseHours ?? null);
  const [status, setStatus]         = React.useState(prompt?.status || 'live');
  const [saving, setSaving]         = React.useState(false);
  const [err, setErr]               = React.useState(null);

  function toggleCat(slug) {
    setChosenCats((cs) => (cs.includes(slug) ? cs.filter((c) => c !== slug) : [...cs, slug]));
  }
  function toggleTool(id) {
    setChosenTools((ts) => (ts.includes(id) ? ts.filter((t) => t !== id) : [...ts, id]));
  }
  function toggleModel(id) {
    setModelIds((ids) => {
      // Single-model tier → radio behavior: tapping a chip replaces the
      // selection. Tapping the same chip deselects.
      if (maxModelsPerPrompt <= 1) {
        return ids[0] === id ? [] : [id];
      }
      // Multi-model tier → checkbox behavior, capped at the limit.
      if (ids.includes(id)) return ids.filter((x) => x !== id);
      if (ids.length >= maxModelsPerPrompt) return ids; // refuse silently; UI shows disabled state
      return [...ids, id];
    });
  }
  function selectAllEnabled() {
    setModelIds(enabledModels.slice(0, maxModelsPerPrompt).map((m) => m.id));
  }

  // For tool-support warnings: a tool is "unsupported" if any picked model
  // can't run it. DeepSeek can't do web search, so picking DeepSeek + web
  // search shows the warning.
  const selectedModels = models.filter((m) => modelIds.includes(m.id));
  const toolSupport = {
    web_search: ['anthropic', 'openai', 'google', 'xai'], // not deepseek
  };
  function modelSupportsTool(tool) {
    return selectedModels.length > 0 && selectedModels.every((m) => toolSupport[tool]?.includes(m.provider));
  }

  async function save(e) {
    e.preventDefault();
    setSaving(true); setErr(null);
    const body = {
      name: name.trim(),
      promptText: promptText.trim(),
      modelIds,
      categories: chosenCats,
      tools: chosenTools,
      predictBeforeCloseHours: closeHours,
      status,
    };
    if (!body.name || !body.promptText || modelIds.length === 0) {
      setErr({ message: 'Name, prompt and at least one model are required.' });
      setSaving(false);
      return;
    }
    try {
      if (prompt?._id) await window.CloudLayerAPI.updatePrompt(prompt._id, body);
      else             await window.CloudLayerAPI.createPrompt(body);
      onSaved();
    } catch (e) {
      setErr(e);
    } finally {
      setSaving(false);
    }
  }

  async function remove() {
    if (!prompt?._id) return;
    if (!confirm('Delete this tracked prompt? Predictions stay on the leaderboard but stop being attributed.')) return;
    setSaving(true);
    try {
      await window.CloudLayerAPI.deletePrompt(prompt._id);
      onSaved();
    } catch (e) {
      setErr(e);
    } finally {
      setSaving(false);
    }
  }

  return (
    <form className="panel" onSubmit={save}>
      <div className="panel-header">
        <h3>{prompt ? 'Edit prompt' : 'New tracked prompt'}</h3>
        <button type="button" className="btn-ghost" onClick={onCancel}>Cancel</button>
      </div>
      <div className="panel-body" style={{ display: 'grid', gap: 16 }}>
        <div className="panel" style={{
          background: 'rgba(212, 174, 90, 0.08)',
          padding: '10px 14px', fontSize: 12.5, lineHeight: 1.5,
        }}>
          <strong>Free tier:</strong> this prompt runs on the <strong>top {marketsPerCategory} markets per category</strong> — soonest to resolve, with at least <strong>${minMarketVolume >= 1000 ? (minMarketVolume / 1000) + 'K' : minMarketVolume}</strong> volume so it skips dust. As a market resolves, the next one rotates in. One prediction per market. Web search is free.
        </div>
        <div className="field">
          <label>Name</label>
          <input
            type="text"
            placeholder="e.g. base-rate sceptic"
            value={name}
            onChange={(e) => setName(e.target.value)}
            maxLength={80}
            required
          />
          <div className="help">Public on the community leaderboard.</div>
        </div>

        <div className="field">
          <label>
            {maxModelsPerPrompt > 1 ? 'Models' : 'Model'}
            <span className="mono faint" style={{ fontSize: 11, marginLeft: 6 }}>
              {maxModelsPerPrompt > 1
                ? `${modelIds.length} of ${maxModelsPerPrompt} max${modelIds.length > 1 ? ' · head-to-head' : ''}`
                : 'free tier · 1 model'}
            </span>
            {maxModelsPerPrompt > 1 && (
              <button
                type="button"
                className="btn-ghost"
                style={{ float: 'right', fontSize: 11, padding: '2px 8px' }}
                onClick={() => modelIds.length === maxModelsPerPrompt ? setModelIds([]) : selectAllEnabled()}
              >
                {modelIds.length === maxModelsPerPrompt ? 'clear' : 'select all'}
              </button>
            )}
          </label>
          <div className="filter-row">
            {models.map((m) => {
              const on = modelIds.includes(m.id);
              const noKey = !m.enabled;
              const lockedByTier = !on && maxModelsPerPrompt > 1 && modelIds.length >= maxModelsPerPrompt;
              const disabled = noKey || lockedByTier;
              return (
                <button
                  key={m.id}
                  type="button"
                  disabled={disabled}
                  className={`fchip ${on ? 'active' : ''}`}
                  onClick={() => !disabled && toggleModel(m.id)}
                  title={noKey ? 'no API key configured' : lockedByTier ? `Free tier allows ${maxModelsPerPrompt} model${maxModelsPerPrompt > 1 ? 's' : ''} — upgrade to add more` : ''}
                  style={{
                    opacity: disabled ? 0.35 : 1,
                    cursor: disabled ? 'not-allowed' : 'pointer',
                    borderColor: on ? m.color : undefined,
                  }}
                >
                  <span style={{
                    display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
                    background: m.color, marginRight: 6, verticalAlign: 'middle',
                  }} />
                  {m.label}{noKey ? ' — no API key' : ''}
                </button>
              );
            })}
          </div>
          {/* Free-tier upgrade pitch when locked to 1 model. */}
          {maxModelsPerPrompt <= 1 && (
            <div className="panel" style={{
              marginTop: 10, padding: '10px 14px', display: 'flex',
              alignItems: 'center', justifyContent: 'space-between', gap: 12,
              background: 'rgba(212, 174, 90, 0.08)',
            }}>
              <div style={{ fontSize: 12.5, lineHeight: 1.5 }}>
                <strong>Want head-to-head?</strong>
                <span className="dim" style={{ marginLeft: 6 }}>
                  Upgrade to run the same prompt across all {enabledModels.length} enabled models on every market.
                </span>
              </div>
              <button
                type="button"
                className="btn btn-accent"
                style={{ fontSize: 12, whiteSpace: 'nowrap' }}
                onClick={() => navigate && navigate('upgrade')}
              >Upgrade →</button>
            </div>
          )}
          <div className="help">
            {maxModelsPerPrompt > 1
              ? 'Pick any subset of enabled models. Each market gets one prediction per chosen model. The 5-per-category cap is on markets, so more models multiplies API spend but doesn’t broaden the pool.'
              : 'Pick one model. Each market gets one prediction from this model. The 5-per-category cap means up to 25 markets in flight at once.'}
          </div>
        </div>

        <div className="field">
          <label>Categories ({chosenCats.length ? chosenCats.length + ' selected' : 'all'})</label>
          <div className="filter-row">
            {categories.map((c) => (
              <button
                key={c.slug}
                type="button"
                className={`fchip ${chosenCats.includes(c.slug) ? 'active' : ''}`}
                onClick={() => toggleCat(c.slug)}
              >{c.name}</button>
            ))}
          </div>
          <div className="help">Empty = run on all tracked categories.</div>
        </div>

        <div className="field">
          <label>Tools ({chosenTools.length ? chosenTools.length + ' on' : 'none'})</label>
          <div className="filter-row">
            {[
              { id: 'web_search', label: 'Web search', blurb: 'Live web grounding via the provider’s hosted search' },
            ].map((t) => {
              const on = chosenTools.includes(t.id);
              const unsupported = on && !modelSupportsTool(t.id);
              return (
                <button
                  key={t.id}
                  type="button"
                  className={`fchip ${on ? 'active' : ''}`}
                  onClick={() => toggleTool(t.id)}
                  title={t.blurb}
                  style={unsupported ? { borderColor: 'var(--neg)', color: 'var(--neg)' } : undefined}
                >
                  {t.label}
                </button>
              );
            })}
          </div>
          <div className="help">
            Hosted by the provider — fresh info before the model answers. Anthropic, OpenAI, Gemini and Grok support it natively. DeepSeek doesn’t and will run without search.
            {selectedModels.length > 0 && chosenTools.includes('web_search') && !modelSupportsTool('web_search') && (() => {
              const unsupported = selectedModels.filter((m) => !['anthropic','openai','google','xai'].includes(m.provider));
              return (
                <div className="mono neg" style={{ fontSize: 11, marginTop: 6 }}>
                  {unsupported.map((m) => m.label).join(', ')} {unsupported.length === 1 ? "doesn't" : "don't"} support web search — those models will run without it.
                </div>
              );
            })()}
          </div>
        </div>

        {/* Timing field hidden from the UI — schema + runner still respect
            prompt.predictBeforeCloseHours if a prompt has it set, so existing
            timer-bound prompts keep working. New prompts always use the
            free-tier rule (top-5 per category soonest, $1K+ volume). */}

        <div className="field">
          <label>Prompt text</label>
          <textarea
            value={promptText}
            onChange={(e) => setPromptText(e.target.value)}
            rows={10}
            maxLength={4000}
            required
            style={{ minHeight: 220 }}
          />
          <div className="help">
            The runner appends a strict JSON-output instruction so even loose wording yields parseable output. The market context (question, description, market YES, volume, end date) is provided in the user message — your prompt is the system instruction.
            <span className="mono faint" style={{ marginLeft: 6 }}>{promptText.length}/4000</span>
          </div>
        </div>

        <div className="field">
          <label>Status</label>
          <div className="filter-row">
            <button type="button" className={`fchip ${status === 'live' ? 'active' : ''}`} onClick={() => setStatus('live')}>Live</button>
            <button type="button" className={`fchip ${status === 'paused' ? 'active' : ''}`} onClick={() => setStatus('paused')}>Paused</button>
          </div>
        </div>

        {err && <ApiError error={err} />}

        <div className="row gap-12" style={{ marginTop: 8 }}>
          <button className="btn btn-accent" type="submit" disabled={saving}>
            {saving ? 'Saving…' : prompt ? 'Save changes' : 'Start tracking →'}
          </button>
          {prompt && (
            <button type="button" className="btn" onClick={remove} disabled={saving} style={{ color: 'var(--neg)', borderColor: 'var(--neg)' }}>
              Delete
            </button>
          )}
        </div>
      </div>
    </form>
  );
}

const DEFAULT_PROMPT = `You are a binary forecaster with a base-rate prior.
For each prediction market, weigh the most recent reliable evidence
against the historical base rate. Disagree with the market when you
have a specific reason. Avoid hedging language.`;

// =========================================================================
// PromptDetailPanel — opened when you click a prompt row on the Track page.
// Shows: stat strip · per-category cards · accuracy chart · predictions list
// with their market outcomes.
// =========================================================================
function PromptDetailPanel({ promptId, modelColor, navigate }) {
  const { categories, models } = useMeta();
  const modelById = React.useMemo(
    () => Object.fromEntries(models.map((m) => [m.id, m])),
    [models],
  );
  const detail = useApi(() => window.CloudLayerAPI.promptPredictions(promptId), [promptId]);

  if (detail.loading && !detail.data) {
    return <div style={{ padding: 32 }}><Loading label="Loading prompt detail…" /></div>;
  }
  if (detail.error) {
    return <div style={{ padding: 24 }}><ApiError error={detail.error} /></div>;
  }
  const { prompt, predictions = [], marketsById = {}, byCategory = [], timeseries = [], stats = {} } = detail.data || {};
  const byCatMap = Object.fromEntries((byCategory || []).map((c) => [c.category, c]));

  const series = [{
    id: promptId,
    label: prompt?.name || 'this prompt',
    color: modelColor,
    points: timeseries,
  }];

  return (
    <div style={{ padding: '20px 24px 28px', borderTop: '1px solid var(--line)' }}>
      <div className="stat-strip" style={{ marginBottom: 20 }}>
        <div className="stat"><div className="label">Predictions</div><div className="value mono">{stats.predictions ?? 0}</div></div>
        <div className="stat"><div className="label">Resolved</div><div className="value mono">{stats.resolvedCount ?? 0}</div></div>
        <div className="stat"><div className="label">Pending</div><div className="value mono">{stats.pending ?? 0}</div></div>
        <div className="stat">
          <div className="label">Accuracy</div>
          <div className="value mono">{stats.accuracy == null ? '—' : Math.round(stats.accuracy * 100) + '%'}</div>
        </div>
      </div>

      <div style={{ marginBottom: 20 }}>
        <div className="uppercase faint" style={{ fontSize: 10, letterSpacing: '0.08em', marginBottom: 8 }}>
          accuracy · last 60 days
        </div>
        <AccuracyLineChart series={series} days={60} height={200} showLegend={false} />
      </div>

      <div className="uppercase faint" style={{ fontSize: 10, letterSpacing: '0.08em', marginBottom: 10 }}>
        per-category
      </div>
      <div style={{
        display: 'grid',
        gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
        gap: 12, marginBottom: 24,
      }}>
        {categories.map((c) => {
          const cell = byCatMap[c.slug] || { predictions: 0 };
          const empty = !cell.predictions;
          const hasReal = cell.accuracy != null;
          return (
            <div
              key={c.slug}
              onClick={() => !empty && navigate('category', { slug: c.slug })}
              style={{
                background: 'var(--bg)', border: '1px solid var(--line)', borderRadius: 8,
                padding: '12px 14px', cursor: empty ? 'default' : 'pointer',
                opacity: empty ? 0.55 : 1,
              }}
              onMouseEnter={(e) => { if (!empty) e.currentTarget.style.borderColor = modelColor; }}
              onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--line)'; }}
            >
              <div className="uppercase faint" style={{ fontSize: 10 }}>{c.name}</div>
              {empty ? (
                <div className="mono faint" style={{ fontSize: 12, marginTop: 10 }}>no predictions</div>
              ) : (
                <>
                  <div className="mono tnum" style={{ fontSize: 22, marginTop: 6, lineHeight: 1.1 }}>
                    {hasReal ? `${Math.round(cell.accuracy * 100)}%` : <span className="faint">—</span>}
                  </div>
                  <div className="mono faint" style={{ fontSize: 10, marginTop: 2 }}>
                    {hasReal
                      ? `${cell.correct}/${cell.resolvedCount} resolved`
                      : `${cell.predictions} pending`}
                  </div>
                </>
              )}
            </div>
          );
        })}
      </div>

      {/* Group predictions by market so each card shows ALL models for that
          market in one place — same shape as the home-page market cards. */}
      {(() => {
        const byMarket = new Map();
        for (const pr of predictions) {
          const m = marketsById[pr.polymarketId];
          if (!m) continue;
          if (!byMarket.has(pr.polymarketId)) {
            byMarket.set(pr.polymarketId, { ...m, predictions: [] });
          }
          byMarket.get(pr.polymarketId).predictions.push({
            modelId: pr.modelId,
            modelLabel: pr.modelLabel,
            provider: pr.provider,
            yesProbability: pr.yesProbability,
            verdict: pr.verdict,
            reasoning: pr.reasoning,
            edge: pr.edge,
            at: pr.createdAt,
          });
        }
        const cards = Array.from(byMarket.values()).map((mkt) => {
          // Consensus = mean of all model probabilities for this market.
          const probs = mkt.predictions.map((p) => p.yesProbability).filter(Number.isFinite);
          const mean = probs.length ? probs.reduce((s, n) => s + n, 0) / probs.length : null;
          return {
            ...mkt,
            consensus: mean == null ? null : { yesProbability: mean, verdict: mean >= 0.5 ? 'YES' : 'NO' },
          };
        });
        // Sort: pending (soonest endDate) first, then resolved.
        cards.sort((a, b) => {
          if (a.resolved !== b.resolved) return a.resolved ? 1 : -1;
          return new Date(a.endDate) - new Date(b.endDate);
        });

        return (
          <>
            <div className="uppercase faint" style={{ fontSize: 10, letterSpacing: '0.08em', marginBottom: 10 }}>
              markets ({cards.length}) <span className="mono faint" style={{ marginLeft: 6 }}>· {predictions.length} predictions total</span>
            </div>
            {cards.length === 0 ? (
              <div className="mono faint" style={{ padding: 16 }}>
                No predictions yet. The runner fires every 5 minutes — give it a moment.
              </div>
            ) : (
              <LiveMarketGrid markets={cards} navigate={navigate} />
            )}
          </>
        );
      })()}
    </div>
  );
}

window.ProfilePage = ProfilePage;
window.PromptEditor = PromptEditor;
window.PromptDetailPanel = PromptDetailPanel;
