/* ===========================================================
   Views (set 2): Visor, Terminal, Arena, Ranking, Sources, Settings
   =========================================================== */

const { useState: useState2, useEffect: useEffect2, useMemo: useMemo2, useRef: useRef2, Fragment } = React;

/* ------------------------- RETRY VISOR ------------------------- */
function VisorView({ run, nav }) {
  const attempts = run.writer.attempts_history;
  const [selIdx, setSelIdx] = useState2(attempts.length - 1);
  const [showDiff, setShowDiff] = useState2(false);

  const sel = attempts[selIdx];
  const prev = selIdx > 0 ? attempts[selIdx - 1] : null;

  return (
    <div className="page">
      <div className="page-header">
        <div style={{display:'flex', alignItems:'center', gap: 12}}>
          <button className="chip" onClick={() => nav(`/runs/${run.id}`)} style={{cursor:'pointer'}}><I.back /> Run</button>
          <div>
            <span className="kicker">VISOR DE REINTENTOS · {run.id}</span>
            <h1 style={{fontSize: 22, maxWidth: 880}}>{run.editor.selected_titulo}</h1>
          </div>
        </div>
        <div className="meta">
          <Chip kind="warn">{attempts.length} intentos</Chip>
          <span>writer · <span className="mono t1">{run.writer.winner_model}</span></span>
        </div>
      </div>

      <div className="visor">
        {/* LEFT git-log */}
        <div className="gitlog">
          <div className="gl-title">Historial · git-log</div>
          {attempts.map((a, i) => (
            <div
              key={i}
              className={`gl-node ${i === selIdx ? 'active' : ''}`}
              onClick={() => setSelIdx(i)}
            >
              <div className="gl-track">
                <div className={`gl-dot ${a.passed ? 'ok' : 'rej'}`}>
                  {a.passed ? <I.check /> : <I.x />}
                </div>
                <div className="gl-line" />
              </div>
              <div className="gl-content">
                <div className="gl-att">Attempt {a.attempt_n}{a.passed ? ' ✓' : ''}</div>
                <div className="gl-model">{a.model}</div>
                <div className="gl-status">
                  {a.passed
                    ? <Chip kind="ok"><I.check /> approved</Chip>
                    : a.reason === 'factcheck'
                      ? <Chip kind="err">factcheck</Chip>
                      : <Chip kind="err">quality</Chip>}
                </div>
                <div className="gl-dur">{a.duration_seconds.toFixed(1)}s · {a.prompt_tokens}↑/{a.output_tokens}↓ tok</div>
              </div>
            </div>
          ))}

          <div className="thin-rule" />
          <div style={{padding: '0 6px', fontSize: 11.5, lineHeight: 1.5}}>
            <div className="italic-label" style={{marginBottom: 4}}>Veredicto del selector</div>
            <div className="t2">{run.selector.reason}</div>
            <div className="thin-rule" style={{margin: '10px 0'}} />
            <div className="italic-label" style={{marginBottom: 4}}>Modelo ganador</div>
            <div className="mono t1">{run.selector.winner}</div>
            <div className="mono t3" style={{fontSize: 10}}>
              mistral {run.selector.scores['mistral-nemo:12b'].toFixed(2)} · gemma {run.selector.scores['gemma4:e4b'].toFixed(2)}
            </div>
          </div>
        </div>

        {/* RIGHT main */}
        <div>
          <div className="tabs">
            {attempts.map((a, i) => (
              <div
                key={i}
                className={`tab ${i === selIdx ? 'active' : ''} ${a.passed ? 'approved' : ''}`}
                onClick={() => setSelIdx(i)}
              >
                Intento {a.attempt_n}
                {a.passed && <I.check />}
                {!a.passed && <I.x style={{color: 'var(--err)'}} />}
              </div>
            ))}
            <div style={{flex: 1}} />
            {selIdx > 0 && (
              <label style={{display: 'flex', alignItems:'center', gap: 8, fontSize: 12, color: 'var(--text2)', cursor: 'pointer', padding: '8px 12px'}}>
                <input type="checkbox" checked={showDiff} onChange={e => setShowDiff(e.target.checked)} />
                Ver diff vs Intento {selIdx}
              </label>
            )}
          </div>

          {/* header strip */}
          <div className="card pad-sm" style={{marginBottom: 18, display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 18}}>
            <div>
              <div className="italic-label">Modelo</div>
              <div className="mono t1" style={{fontSize: 13, fontWeight: 600, marginTop: 2}}>{sel.model}</div>
            </div>
            <div>
              <div className="italic-label">Duración</div>
              <div className="serif t1" style={{fontSize: 18, fontWeight: 600, marginTop: 2}}>{sel.duration_seconds.toFixed(1)}<span style={{color:'var(--text3)', fontSize: 12, fontWeight:400}}> s</span></div>
            </div>
            <div>
              <div className="italic-label">Prompt</div>
              <div className="mono t1" style={{fontSize: 14, marginTop: 2}}>{sel.prompt_tokens.toLocaleString()}<span style={{color: 'var(--text3)', fontSize: 11}}> tok</span></div>
            </div>
            <div>
              <div className="italic-label">Output</div>
              <div className="mono t1" style={{fontSize: 14, marginTop: 2}}>{sel.output_tokens.toLocaleString()}<span style={{color: 'var(--text3)', fontSize: 11}}> tok</span></div>
            </div>
            <div>
              <div className="italic-label">Estado</div>
              <div style={{marginTop: 4}}>
                {sel.passed
                  ? <Chip kind="ok"><I.check /> aprobado</Chip>
                  : sel.reason === 'factcheck'
                    ? <Chip kind="err">rechazado · factcheck</Chip>
                    : <Chip kind="err">rechazado · quality gate</Chip>}
              </div>
            </div>
          </div>

          {/* rejection card */}
          {!sel.passed && sel.quality_issues.length > 0 && (
            <div className="card pad" style={{marginBottom: 18, borderColor: 'var(--err)'}}>
              <div style={{display:'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12}}>
                <div>
                  <span className="kicker" style={{color: 'var(--err)'}}>{sel.reason === 'factcheck' ? 'Fact-check gate' : 'Quality gate'}</span>
                  <div className="serif" style={{fontSize: 16, fontWeight: 600, color: 'var(--text1)'}}>Motivos del rechazo</div>
                </div>
                <Chip kind="err">{sel.quality_issues.length} problemas</Chip>
              </div>
              <ul style={{margin: 0, paddingLeft: 20, fontSize: 13, lineHeight: 1.7, color: 'var(--text2)'}}>
                {sel.quality_issues.map((iss, k) => {
                  const cat = iss.startsWith('Frase prohibida') ? 'frase'
                            : iss.startsWith('Adjetivo') ? 'adjetivo'
                            : iss.startsWith('Párrafo') ? 'densidad'
                            : iss.startsWith('Estructura') ? 'estructura'
                            : 'otro';
                  return (
                    <li key={k} style={{marginBottom: 4}}>
                      <Chip kind={cat === 'frase' ? 'warn' : cat === 'adjetivo' ? 'warn' : cat === 'densidad' ? 'err' : 'info'} style={{marginRight: 8}}>{cat}</Chip>
                      <span dangerouslySetInnerHTML={{__html: iss.replace(/'([^']+)'/g, '<i style="color:var(--text1);font-family:var(--serif)">"$1"</i>')}} />
                    </li>
                  );
                })}
              </ul>
              {sel.removed_phrases.length > 0 && (
                <div style={{marginTop: 14, paddingTop: 12, borderTop: '1px dashed var(--border)'}}>
                  <div className="italic-label" style={{marginBottom: 6}}>Frases marcadas para reescribir</div>
                  <div style={{display: 'flex', flexWrap: 'wrap', gap: 6}}>
                    {sel.removed_phrases.map((p, k) => (
                      <span key={k} className="article-strike serif" style={{fontSize: 12.5, padding: '2px 6px'}}>"{p}"</span>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}

          {sel.passed && (
            <div className="card pad" style={{marginBottom: 18, borderColor: 'var(--ok)', background: 'var(--ok-soft)'}}>
              <div className="serif" style={{fontSize: 16, fontWeight: 600, marginBottom: 4}}>Quality gate aprobado en {sel.duration_seconds.toFixed(1)}s</div>
              <div style={{fontSize: 12.5, color: 'var(--text2)'}}>Todas las verificaciones pasaron: conteo de palabras, párrafos, frases prohibidas, densidad factual. Este intento fue elegido por el selector como el ganador del A/B.</div>
            </div>
          )}

          {/* article preview / diff */}
          {showDiff && prev ? (
            <div className="card">
              <div className="card-head">
                <div><span className="kicker">DIFF</span><span className="title">Intento {prev.attempt_n} → Intento {sel.attempt_n}</span></div>
                <div className="legend">
                  <span className="lg"><span className="sw" style={{background: 'rgba(239,68,68,0.5)'}} />removed</span>
                  <span className="lg"><span className="sw" style={{background: 'rgba(74,222,128,0.5)'}} />added</span>
                </div>
              </div>
              <div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--border)'}}>
                <div style={{background: 'var(--surface1)', padding: 18, minHeight: 400}}>
                  <div className="italic-label" style={{marginBottom: 10}}>Intento {prev.attempt_n} (rechazado)</div>
                  <DiffPane html={prev.html} mode="left" />
                </div>
                <div style={{background: 'var(--surface1)', padding: 18, minHeight: 400}}>
                  <div className="italic-label" style={{marginBottom: 10}}>Intento {sel.attempt_n}{sel.passed ? ' (aprobado)' : ' (rechazado)'}</div>
                  <DiffPane html={sel.html} mode="right" />
                </div>
              </div>
            </div>
          ) : (
            <div className="card">
              <div className="card-head">
                <div><span className="kicker">PREVIEW</span><span className="title">Borrador renderizado</span></div>
                <div className="legend">
                  <span className="lg"><span className="sw" style={{background: 'var(--warn)'}} />frase marcada</span>
                  <span className="lg"><span className="sw" style={{background: 'var(--err)'}} />texto removido</span>
                </div>
              </div>
              <div className="card-body">
                <div className="newspaper" style={{margin: 0, padding: '24px 30px'}}>
                  <h1 className="headline" style={{fontSize: 24}}>{run.editor.selected_titulo}</h1>
                  <div className="byline">Borrador · intento {sel.attempt_n} · {sel.model}</div>
                  <div className="body-cols" style={{columnCount: 2, fontSize: 13.5}} dangerouslySetInnerHTML={{__html: sel.html}} />
                </div>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function DiffPane({ html, mode }) {
  // Cheap line-diff visualization: strip html tags into paragraphs
  const paras = html.replace(/<h2>([^<]+)<\/h2>/g, '\n## $1\n').replace(/<\/?p>/g, '\n').split('\n').filter(p => p.trim()).map(p => p.replace(/<[^>]+>/g, '').trim());
  return (
    <div className="serif" style={{fontSize: 13, lineHeight: 1.55, color: 'var(--text1)'}}>
      {paras.map((p, i) => {
        // mark first paragraph as "changed" for diff illusion
        const isMark = i === 1;
        const isHeader = p.startsWith('##');
        const text = isHeader ? p.slice(2).trim() : p;
        if (isMark && mode === 'left') {
          return <p key={i} style={{background: 'rgba(239,68,68,0.18)', padding: '4px 6px', margin: '0 0 10px', borderLeft: '2px solid var(--err)'}}>{text}</p>;
        }
        if (isMark && mode === 'right') {
          return <p key={i} style={{background: 'rgba(74,222,128,0.16)', padding: '4px 6px', margin: '0 0 10px', borderLeft: '2px solid var(--ok)'}}>{text.replace(/Cabe destacar que/i, '').replace(/importante/g, 'inédito').replace(/No obstante, el contexto sigue siendo complejo\./, '')}</p>;
        }
        if (isHeader) return <h2 key={i} style={{fontFamily:'var(--serif)', fontSize: 15, fontWeight: 700, margin:'12px 0 6px'}}>{text}</h2>;
        return <p key={i} style={{margin: '0 0 10px'}}>{text}</p>;
      })}
    </div>
  );
}

/* ------------------------- TERMINAL ------------------------- */
function TerminalView({ logs }) {
  const [filter, setFilter] = useState2('ALL');
  const [search, setSearch] = useState2('');
  const [follow, setFollow] = useState2(true);
  const [visible, setVisible] = useState2(50);
  const scrollRef = useRef2(null);

  // simulate "live" appending
  useEffect2(() => {
    if (!follow) return;
    const id = setInterval(() => {
      setVisible(v => Math.min(v + 1, logs.length));
    }, 900);
    return () => clearInterval(id);
  }, [follow, logs.length]);

  useEffect2(() => {
    if (follow && scrollRef.current) {
      scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    }
  }, [visible, follow]);

  const filtered = useMemo2(() => {
    return logs.slice(0, visible).filter(l => {
      if (filter !== 'ALL' && l.level !== filter) return false;
      if (search && !(l.message.toLowerCase().includes(search.toLowerCase()) || l.module.toLowerCase().includes(search.toLowerCase()))) return false;
      return true;
    });
  }, [logs, visible, filter, search]);

  return (
    <div className="page" style={{paddingBottom: 30}}>
      <div className="page-header">
        <div>
          <span className="kicker">Stream</span>
          <h1>Live terminal</h1>
        </div>
        <div className="meta">
          <span className="mono">~/logs/newsroom-2026-05-12.log</span>
          <span>{filtered.length} líneas</span>
        </div>
      </div>

      <div className="terminal-shell">
        <div className="terminal-bar">
          <span className="connected"><span className="ledot" />CONNECTED</span>
          <span style={{color:'#888'}}>·</span>
          <span className="mono" style={{color: '#aaa'}}>ws://localhost:8000/logs</span>
          <span style={{flex: 1}} />
          <select value={filter} onChange={e => setFilter(e.target.value)}>
            <option>ALL</option>
            <option>INFO</option>
            <option>STEP</option>
            <option>WARNING</option>
            <option>ERROR</option>
          </select>
          <input className="search-in" value={search} onChange={e => setSearch(e.target.value)} placeholder="grep…" />
          <label style={{display:'inline-flex', gap: 6, color:'#aaa', cursor:'pointer', fontSize: 11, alignItems:'center'}}>
            <input type="checkbox" checked={follow} onChange={e => setFollow(e.target.checked)} />
            follow
          </label>
        </div>
        <div className="terminal-lines" ref={scrollRef}>
          {filtered.map(l => (
            <div key={l.id} className={`ln ${l.level.toLowerCase()}`}>
              <span className="ts">{l.ts_compact}</span>
              <span className="lv">[{l.level}]</span>
              <span className="msg"><span style={{color: '#888'}}>{l.module}:</span> {l.message}</span>
            </div>
          ))}
          {follow && (
            <div className="ln info" style={{opacity: 0.7}}>
              <span className="ts">▌</span>
              <span className="lv"></span>
              <span className="msg" style={{animation: 'blink 1s steps(2) infinite'}}>_</span>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ------------------------- RANKING ------------------------- */
function RankingView({ runs }) {
  const latest = runs[0];
  const candidates = latest.collector.candidates;
  const [sortBy, setSortBy] = useState2('score');

  const sorted = [...candidates].sort((a, b) => {
    if (sortBy === 'score') return b.editorial_score - a.editorial_score;
    if (sortBy === 'source') return a.fuente.localeCompare(b.fuente);
    if (sortBy === 'freshness') return b.freshness_score - a.freshness_score;
    return 0;
  });

  const maxScore = Math.max(...candidates.map(c => c.editorial_score));

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <span className="kicker">Cola actual</span>
          <h1>Ranking de candidatos</h1>
        </div>
        <div className="meta">
          <span>{candidates.length} candidatos</span>
          <span>run <b className="mono t1">{latest.id}</b></span>
          <span className="mono">{fmtClock(latest.timestamp)}</span>
        </div>
      </div>

      <div className="card flush">
        <table className="dt">
          <thead>
            <tr>
              <th style={{width: 32}}>#</th>
              <th onClick={() => setSortBy('source')} style={{cursor:'pointer'}}>Titular candidato</th>
              <th>Fuente</th>
              <th onClick={() => setSortBy('score')} style={{cursor:'pointer', textAlign:'right'}}>Editorial ▼</th>
              <th style={{width: 280}}>Desglose</th>
            </tr>
          </thead>
          <tbody>
            {sorted.map((c, i) => {
              const isSelected = c.titulo === latest.editor.selected_titulo;
              const bd = {
                source: c.source_score,
                freshness: c.freshness_score,
                relevance: c.relevance_score,
                trends: c.depth_bonus,
                gemini: Math.max(0, c.editorial_score - c.source_score - c.freshness_score - c.relevance_score - c.depth_bonus),
              };
              return (
                <tr key={i} style={isSelected ? {borderLeft: '3px solid var(--gold)'} : null}>
                  <td className="num" style={{textAlign:'left', color: 'var(--text3)'}}>{String(i+1).padStart(2, '0')}</td>
                  <td className="headline">
                    <div style={{display: 'flex', alignItems: 'baseline', gap: 8}}>
                      <div className="truncate" title={c.titulo} style={{maxWidth: 480}}>{c.titulo}</div>
                      {isSelected && <span className="italic-label" style={{color: 'var(--gold)', fontSize: 12}}>seleccionado</span>}
                    </div>
                  </td>
                  <td><Chip>{c.fuente} · w {(window.SOURCES.find(s => s.name === c.fuente)?.weight || 0.85).toFixed(2)}</Chip></td>
                  <td className="num"><span style={{fontSize: 15, fontWeight: 600}}>{c.editorial_score.toFixed(3)}</span></td>
                  <td>
                    <div style={{display: 'flex', gap: 8, alignItems: 'center'}}>
                      <div style={{flex: 1}}>
                        <ScoreBar breakdown={bd} total={maxScore} />
                      </div>
                    </div>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      <div style={{marginTop: 18, padding: '16px 0', borderTop: '1px solid var(--border)'}}>
        <div className="legend">
          <span className="lg"><span className="sw" style={{background: '#5b8def'}} />Source weight</span>
          <span className="lg"><span className="sw" style={{background: 'var(--gold)'}} />Freshness</span>
          <span className="lg"><span className="sw" style={{background: '#4ec9b0'}} />Relevance</span>
          <span className="lg"><span className="sw" style={{background: '#d685c3'}} />Trends boost</span>
          <span className="lg"><span className="sw" style={{background: '#c98c4e'}} />Gemini boost</span>
        </div>
      </div>
    </div>
  );
}

/* ------------------------- ARENA ------------------------- */
function ArenaView({ runs }) {
  const wins = { 'mistral-nemo:12b': 0, 'gemma4:e4b': 0 };
  runs.forEach(r => { wins[r.selector.winner] = (wins[r.selector.winner] || 0) + 1; });
  const totalDuels = runs.length;
  const mistralRate = (wins['mistral-nemo:12b'] / totalDuels * 100);
  const gemmaRate   = (wins['gemma4:e4b']      / totalDuels * 100);
  const leader = mistralRate > gemmaRate ? 'mistral-nemo:12b' : 'gemma4:e4b';

  // build cumulative win-rate "over time" (synthetic but coherent)
  const points = 30;
  const mistralLine = [];
  const gemmaLine = [];
  let mw = 0, gw = 0;
  for (let i = 0; i < points; i++) {
    const isMistral = ((i * 7 + 3) % 11) < 7;
    if (isMistral) mw++; else gw++;
    const total = i + 1;
    mistralLine.push((mw / total) * 100);
    gemmaLine.push((gw / total) * 100);
  }

  // by category
  const cats = window.CATEGORIES;
  const byCat = cats.map(c => {
    const a = 0.45 + Math.random() * 0.4;
    return { cat: c, mistral: a * 100, gemma: (1 - a) * 100 };
  });

  // recent duels (use runs)
  const duels = runs.slice(0, 10);

  // sheet state
  const [open, setOpen] = useState2(null);

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <span className="kicker">A/B</span>
          <h1>Arena de escritores</h1>
        </div>
        <div className="meta">{totalDuels} duelos · ganador líder · <b className="mono t1">{leader}</b></div>
      </div>

      {/* HERO */}
      <div className="arena-hero" style={{marginBottom: 18}}>
        <div className="side" style={leader === 'mistral-nemo:12b' ? {borderLeft: '3px solid var(--gold)'} : null}>
          <div className="italic-label">Writer A</div>
          <div className="model">mistral-nemo:12b</div>
          <div className="winrate" style={{color: leader === 'mistral-nemo:12b' ? 'var(--gold)' : 'var(--text1)'}}>{mistralRate.toFixed(0)}<span style={{fontSize: 24, color:'var(--text3)'}}>%</span></div>
          <div className="italic-label" style={{marginTop: 4}}>win-rate</div>
          <div className="stats">
            <span>duelos <b>{wins['mistral-nemo:12b']}</b></span>
            <span>dur. media <b>11.4s</b></span>
            <span>longitud media <b>458 pal.</b></span>
          </div>
        </div>
        <div className="vs">vs</div>
        <div className="side" style={leader === 'gemma4:e4b' ? {borderLeft: '3px solid var(--gold)'} : null}>
          <div className="italic-label">Writer B</div>
          <div className="model">gemma4:e4b</div>
          <div className="winrate" style={{color: leader === 'gemma4:e4b' ? 'var(--gold)' : 'var(--text1)'}}>{gemmaRate.toFixed(0)}<span style={{fontSize: 24, color:'var(--text3)'}}>%</span></div>
          <div className="italic-label" style={{marginTop: 4}}>win-rate</div>
          <div className="stats">
            <span>duelos <b>{wins['gemma4:e4b']}</b></span>
            <span>dur. media <b>9.8s</b></span>
            <span>longitud media <b>421 pal.</b></span>
          </div>
        </div>
      </div>

      {/* Win-rate over time + by category */}
      <div className="grid" style={{gridTemplateColumns: '7fr 5fr', marginBottom: 18}}>
        <div className="card">
          <div className="card-head"><div><span className="kicker">TENDENCIA</span><span className="title">Win-rate acumulado · últimos 30 duelos</span></div></div>
          <div className="card-body">
            <LineChart series={[
              { data: mistralLine, color: '#4ec9b0', label: 'mistral-nemo:12b' },
              { data: gemmaLine,   color: 'var(--gold)', label: 'gemma4:e4b' },
            ]} />
          </div>
        </div>

        <div className="card">
          <div className="card-head"><div><span className="kicker">CATEGORÍAS</span><span className="title">Win-rate por sección</span></div></div>
          <div className="card-body">
            <div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
              {byCat.map(b => (
                <div key={b.cat}>
                  <div style={{display: 'flex', justifyContent: 'space-between', fontSize: 11.5, marginBottom: 4}}>
                    <span className="serif" style={{color: 'var(--text1)'}}>{b.cat}</span>
                    <span className="mono" style={{color: 'var(--text3)'}}>m {b.mistral.toFixed(0)}% · g {b.gemma.toFixed(0)}%</span>
                  </div>
                  <div style={{display: 'flex', gap: 1, height: 10, background: 'var(--surface2)', borderRadius: 2, overflow: 'hidden'}}>
                    <div style={{width: `${b.mistral}%`, background: '#4ec9b0'}} />
                    <div style={{width: `${b.gemma}%`, background: 'var(--gold)'}} />
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>

      {/* Recent duels */}
      <div className="card flush">
        <div className="card-head"><div><span className="kicker">HISTORIAL</span><span className="title">Duelos recientes</span></div><div className="meta">click → veredicto completo</div></div>
        <table className="dt">
          <thead>
            <tr>
              <th>Run</th>
              <th>Titular</th>
              <th>Ganador</th>
              <th>Perdedor</th>
              <th>Razón del selector</th>
              <th style={{textAlign:'right'}}>Δ score</th>
            </tr>
          </thead>
          <tbody>
            {duels.map(d => {
              const mScore = d.selector.scores['mistral-nemo:12b'];
              const gScore = d.selector.scores['gemma4:e4b'];
              const gap = Math.abs(mScore - gScore);
              const loser = d.selector.winner === 'mistral-nemo:12b' ? 'gemma4:e4b' : 'mistral-nemo:12b';
              return (
                <tr key={d.id} className="row-link" onClick={() => setOpen(d)}>
                  <td className="num" style={{textAlign:'left'}}>{d.id}</td>
                  <td className="headline"><div className="truncate" style={{maxWidth: 380}}>{d.editor.selected_titulo}</div></td>
                  <td><Chip kind="gold"><I.trophy />{d.selector.winner}</Chip></td>
                  <td><span className="mono t3" style={{fontSize: 11}}>{loser}</span></td>
                  <td className="t2" style={{fontSize: 12, fontStyle:'italic'}}><div className="truncate" style={{maxWidth: 320}}>{d.selector.reason}</div></td>
                  <td className="num">+{gap.toFixed(2)}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>

      {open && <DuelSheet run={open} onClose={() => setOpen(null)} />}
    </div>
  );
}

function LineChart({ series }) {
  const w = 600, h = 220, pad = 30;
  const all = series.flatMap(s => s.data);
  const min = 0, max = 100;
  const n = series[0].data.length;
  const xAt = i => pad + (i / (n - 1)) * (w - pad - 10);
  const yAt = v => h - pad + 6 - ((v - min) / (max - min)) * (h - pad - 16);

  return (
    <div style={{width: '100%', overflowX: 'auto'}}>
      <svg viewBox={`0 0 ${w} ${h}`} style={{width: '100%', maxWidth: 700, height: 'auto'}}>
        {/* gridlines */}
        {[0,25,50,75,100].map(v => (
          <g key={v}>
            <line x1={pad} y1={yAt(v)} x2={w-10} y2={yAt(v)} stroke="var(--border)" strokeDasharray="2,3" />
            <text x={pad-6} y={yAt(v)+4} textAnchor="end" fontSize="9" fill="var(--text3)" fontFamily="var(--mono)">{v}%</text>
          </g>
        ))}
        {series.map((s, si) => (
          <Fragment key={si}>
            <polyline
              points={s.data.map((v, i) => `${xAt(i)},${yAt(v)}`).join(' ')}
              fill="none" stroke={s.color} strokeWidth="2"
            />
            {s.data.map((v, i) => i % 5 === 0 && <circle key={i} cx={xAt(i)} cy={yAt(v)} r="2" fill={s.color} />)}
          </Fragment>
        ))}
        {/* x axis */}
        <text x={pad} y={h-6} fontSize="9" fill="var(--text3)" fontFamily="var(--mono)">run 1</text>
        <text x={w-10} y={h-6} textAnchor="end" fontSize="9" fill="var(--text3)" fontFamily="var(--mono)">run {n}</text>
        {/* legend */}
        <g transform={`translate(${pad + 6}, 14)`}>
          {series.map((s, i) => (
            <g key={i} transform={`translate(${i * 160}, 0)`}>
              <rect width="10" height="10" fill={s.color} />
              <text x="14" y="9" fontSize="10" fontFamily="var(--mono)" fill="var(--text2)">{s.label}</text>
            </g>
          ))}
        </g>
      </svg>
    </div>
  );
}

function DuelSheet({ run, onClose }) {
  const attempts = run.writer.attempts_history;
  // synthesize draft B as a variant of the first attempt's html
  const draftA = attempts[attempts.length - 1];
  const draftB = {
    model: draftA.model === 'mistral-nemo:12b' ? 'gemma4:e4b' : 'mistral-nemo:12b',
    html: attempts[0].html,
  };

  return (
    <div className="sheet-back" onClick={onClose}>
      <div className="sheet" onClick={e => e.stopPropagation()}>
        <div style={{padding: '22px 26px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems:'center', justifyContent: 'space-between'}}>
          <div>
            <div className="kicker" style={{marginBottom: 6}}>Veredicto del A/B · {run.id}</div>
            <div className="serif" style={{fontSize: 22, fontWeight: 700, maxWidth: 760}}>{run.editor.selected_titulo}</div>
          </div>
          <button className="chip" onClick={onClose} style={{cursor:'pointer'}}><I.x /> cerrar</button>
        </div>

        <div style={{padding: '18px 26px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18}}>
          <div className={'card pad' + (run.selector.winner === draftA.model ? ' bordered-left-gold' : '')} style={{paddingLeft: 18}}>
            <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 12}}>
              <div className="mono t1" style={{fontSize: 13, fontWeight: 600}}>{draftA.model}</div>
              {run.selector.winner === draftA.model && <Chip kind="gold"><I.trophy /> winner</Chip>}
            </div>
            <div className="serif" style={{fontSize: 12, lineHeight: 1.5, maxHeight: 380, overflow: 'auto'}} dangerouslySetInnerHTML={{__html: draftA.html}} />
          </div>
          <div className={'card pad' + (run.selector.winner === draftB.model ? ' bordered-left-gold' : '')} style={{paddingLeft: 18}}>
            <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom: 12}}>
              <div className="mono t1" style={{fontSize: 13, fontWeight: 600}}>{draftB.model}</div>
              {run.selector.winner === draftB.model && <Chip kind="gold"><I.trophy /> winner</Chip>}
            </div>
            <div className="serif" style={{fontSize: 12, lineHeight: 1.5, maxHeight: 380, overflow: 'auto'}} dangerouslySetInnerHTML={{__html: draftB.html}} />
          </div>
        </div>

        <div style={{padding: '0 26px 26px'}}>
          <div className="card pad">
            <div className="kicker" style={{marginBottom: 8}}>Razón del selector</div>
            <div className="serif" style={{fontSize: 15, fontStyle:'italic', color: 'var(--text1)'}}>"{run.selector.reason}"</div>
            <div className="thin-rule" />
            <div style={{display: 'flex', gap: 24, fontFamily:'var(--mono)', fontSize: 11.5}}>
              <span className="t3">scores · </span>
              <span>mistral <b className="t1">{run.selector.scores['mistral-nemo:12b'].toFixed(2)}</b></span>
              <span>gemma <b className="t1">{run.selector.scores['gemma4:e4b'].toFixed(2)}</b></span>
              <span style={{flex: 1}} />
              <span>Δ <b className="t1">{Math.abs(run.selector.scores['mistral-nemo:12b'] - run.selector.scores['gemma4:e4b']).toFixed(2)}</b></span>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ------------------------- SOURCES ------------------------- */
function SourcesView({ runs }) {
  // aggregate per-source
  const sources = window.SOURCES.map(s => {
    const delivered = runs.reduce((a, r) => a + r.collector.candidates.filter(c => c.fuente === s.name).length, 0);
    const selected  = runs.filter(r => r.editor.selected_fuente === s.name).length;
    const published = runs.filter(r => r.editor.selected_fuente === s.name && r.publisher.success).length;
    return { ...s, delivered, selected, published, winrate: delivered ? (selected / delivered * 100) : 0 };
  }).sort((a, b) => b.weight - a.weight);

  const [open, setOpen] = useState2(null);

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <span className="kicker">Performance</span>
          <h1>Matriz de fuentes</h1>
        </div>
        <div className="meta">{sources.length} fuentes · últimas {runs.length} ejecuciones</div>
      </div>

      <div className="card flush">
        <table className="dt">
          <thead>
            <tr>
              <th>Fuente</th>
              <th style={{textAlign:'right'}}>Peso</th>
              <th style={{textAlign:'right'}}>Candidatos</th>
              <th style={{textAlign:'right'}}>Seleccionados</th>
              <th style={{textAlign:'right'}}>Publicados</th>
              <th>Win-rate</th>
            </tr>
          </thead>
          <tbody>
            {sources.map(s => (
              <tr key={s.name} className="row-link" onClick={() => setOpen(s)}>
                <td className="headline" style={{fontSize: 14}}>{s.name}</td>
                <td className="num"><Chip kind={s.weight > 1.2 ? 'gold' : s.weight > 0.9 ? 'info' : ''}>w {s.weight.toFixed(2)}</Chip></td>
                <td className="num">{s.delivered}</td>
                <td className="num">{s.selected}</td>
                <td className="num">{s.published}</td>
                <td style={{width: 180}}>
                  <div style={{display:'flex', alignItems:'center', gap: 10}}>
                    <div style={{flex:1, height: 6, background: 'var(--surface2)', borderRadius: 2, overflow:'hidden'}}>
                      <div style={{width:`${Math.min(100, s.winrate*4)}%`, height:'100%', background: 'var(--gold)'}} />
                    </div>
                    <span className="mono t1" style={{fontSize: 11, minWidth: 42, textAlign:'right'}}>{s.winrate.toFixed(1)}%</span>
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {open && (
        <div className="sheet-back" onClick={() => setOpen(null)}>
          <div className="sheet" onClick={e => e.stopPropagation()} style={{maxWidth: 720}}>
            <div style={{padding: '22px 26px', borderBottom: '1px solid var(--border)', display:'flex', alignItems:'center', justifyContent:'space-between'}}>
              <div>
                <div className="kicker">Fuente</div>
                <div className="serif" style={{fontSize: 28, fontWeight: 700}}>{open.name}</div>
                <div className="mono" style={{fontSize: 11, color: 'var(--text3)', marginTop: 4}}>peso · {open.weight.toFixed(2)} · {open.delivered} candidatos · {open.selected} seleccionados</div>
              </div>
              <button className="chip" onClick={() => setOpen(null)} style={{cursor:'pointer'}}><I.x /> cerrar</button>
            </div>
            <div style={{padding: '18px 26px'}}>
              <div className="kicker" style={{marginBottom: 12}}>Últimos titulares</div>
              <div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
                {runs.flatMap(r => r.collector.candidates.filter(c => c.fuente === open.name).map(c => ({...c, runId: r.id, ts: r.timestamp})))
                  .slice(0, 10).map((c, i) => (
                    <div key={i} className="thin-rule" style={{padding: '6px 0', margin: 0}}>
                      <div className="serif" style={{fontSize: 14, color: 'var(--text1)', lineHeight: 1.35, marginBottom: 4}}>{c.titulo}</div>
                      <div style={{display: 'flex', gap: 14, fontSize: 11, color: 'var(--text3)', fontFamily: 'var(--mono)'}}>
                        <span>score {c.editorial_score.toFixed(3)}</span>
                        <span>{c.runId}</span>
                        <span>{fmtClock(c.ts)}</span>
                      </div>
                    </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ------------------------- SETTINGS ------------------------- */
function SettingsView() {
  const cfg = window.getMockConfig();
  const maxW = Math.max(...Object.values(cfg.source_weights));
  return (
    <div className="page">
      <div className="page-header">
        <div>
          <span className="kicker">Config</span>
          <h1>Settings</h1>
        </div>
        <div className="meta italic-label">Read-only · edita <span className="mono t1">config.py</span> en el backend</div>
      </div>

      <div className="grid" style={{gridTemplateColumns: 'repeat(2, 1fr)'}}>
        <div className="card">
          <div className="card-head"><div><span className="kicker">MODELS</span><span className="title">Modelo por agente</span></div></div>
          <div className="card-body">
            <table className="dt" style={{width: '100%'}}>
              <tbody>
                {Object.entries(cfg.models).map(([agent, model]) => (
                  <tr key={agent}>
                    <td style={{borderBottom: '1px dashed var(--border)', padding: '8px 0'}} className="t2">{agent}</td>
                    <td style={{borderBottom: '1px dashed var(--border)', padding: '8px 0', textAlign:'right'}} className="mono t1">{model}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        <div className="card">
          <div className="card-head"><div><span className="kicker">PIPELINE</span><span className="title">Parámetros</span></div></div>
          <div className="card-body">
            <table className="dt" style={{width: '100%'}}>
              <tbody>
                {Object.entries(cfg.pipeline).map(([k, v]) => (
                  <tr key={k}>
                    <td style={{borderBottom: '1px dashed var(--border)', padding: '8px 0', fontSize: 11}} className="t2 mono">{k}</td>
                    <td style={{borderBottom: '1px dashed var(--border)', padding: '8px 0', textAlign:'right'}} className="mono t1">{String(v)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        <div className="card" style={{gridColumn: '1 / -1'}}>
          <div className="card-head"><div><span className="kicker">SOURCES</span><span className="title">Pesos por fuente</span></div></div>
          <div className="card-body">
            <div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 32px'}}>
              {Object.entries(cfg.source_weights).map(([name, w]) => (
                <div key={name}>
                  <div style={{display:'flex', justifyContent:'space-between', fontSize: 12.5, marginBottom: 5}}>
                    <span className="serif t1">{name}</span>
                    <span className="mono t1">{w.toFixed(2)}</span>
                  </div>
                  <div style={{height: 8, background: 'var(--surface2)', borderRadius: 2, overflow:'hidden'}}>
                    <div style={{width: `${(w/maxW)*100}%`, height:'100%', background: w > 1.2 ? 'var(--gold)' : w > 0.9 ? 'var(--info)' : 'var(--text3)'}} />
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  VisorView, TerminalView, RankingView, ArenaView, SourcesView, SettingsView,
});
