// Main app — F1 Apex Optimizer
(function(){
const { TEAMS, DRIVERS, CONSTRUCTORS, SCORING, SESSIONS, DRIVER_BY_NUM, RACE } = window.F1DATA;
const { findTopLineups, driverPoints, constructorPoints } = window.F1OPT;

const STORAGE_KEY = "f1apex_state_v12_mal";
const ANALYSIS_CACHE_KEY_VERSIONED = "f1apex_analysis_mal_v1";
const fmt$ = n => "$" + Math.round(n).toLocaleString();

// Clean up older schema versions on boot — keeps localStorage tidy and prevents
// accidental hydration from incompatible shapes (including state from prior race weekends).
(function cleanupOldStorage() {
  const obsolete = [
    "f1apex_state_v1", "f1apex_state_v2", "f1apex_state_v3", "f1apex_state_v4_can", "f1apex_state_v5_mon", "f1apex_state_v6_bar", "f1apex_state_v7_aut", "f1apex_state_v8_sil", "f1apex_state_v9_bel", "f1apex_state_v10_ned", "f1apex_state_v11_aze",
    "f1opt_state_v1", "f1opt_state_v2",
    "f1apex_analysis_v1", "f1apex_analysis_mon_v1", "f1apex_analysis_bar_v1", "f1apex_analysis_aut_v1", "f1apex_analysis_sil_v1", "f1apex_analysis_bel_v1", "f1apex_analysis_ned_v1", "f1apex_analysis_aze_v1",  // prior race-weekend caches
  ];
  obsolete.forEach(k => { try { if (localStorage.getItem(k)) localStorage.removeItem(k); } catch (e) {} });
})();

// ===========================================================
// Default state
// ===========================================================
function makeInitialLevers() {
  // Default: every driver finishes where they started. The user runs Step ⓪
  // Session Analysis and imports a projection to set the predicted finish.
  // Race-specific opinion overrides intentionally removed.
  const o = {};
  for (const d of DRIVERS) {
    o[d.id] = {
      finish: d.start,
      lapsLed: 0,
      fastestLap: false,
      classified: true,
      lock: false,
      exclude: false,
    };
  }
  return o;
}

function makeInitialConState() {
  const o = {};
  for (const c of CONSTRUCTORS) o[c.team] = { lock: false, exclude: false };
  return o;
}

function loadPersisted() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    const s = JSON.parse(raw);
    return (s && typeof s === "object") ? s : null;
  } catch (e) { return null; }
}
function persist(state) {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}

// ===========================================================
// Driver chip
// ===========================================================
function DriverChip({ driver, levers, dragging, onMouseDown, onTogglePanel, panelOpen, deltaTint }) {
  const team = TEAMS[driver.team];
  const lever = levers[driver.id];
  const pts = driverPoints(driver, levers);
  const isDnf = !lever.classified;
  // Delta tint: pass "up" / "down" / null. Strong if |delta| ≥ 4.
  const delta = driver.start - lever.finish;
  let deltaClass = "";
  if (deltaTint === "up" && delta > 0) deltaClass = "chip-up " + (delta >= 4 ? "chip-up-strong " : "");
  else if (deltaTint === "down" && delta < 0) deltaClass = "chip-down " + (-delta >= 4 ? "chip-down-strong " : "");
  else if (deltaTint === "auto") {
    if (delta > 0) deltaClass = "chip-up " + (delta >= 4 ? "chip-up-strong " : "");
    else if (delta < 0) deltaClass = "chip-down " + (-delta >= 4 ? "chip-down-strong " : "");
  }
  return (
    <div
      className={"chip " + deltaClass + (dragging ? "chip-drag " : "") + (lever.exclude ? "chip-exc " : "") + (lever.lock ? "chip-lock " : "") + (isDnf ? "chip-dnf " : "")}
      onMouseDown={onMouseDown}
      style={{ borderLeftColor: team.color }}>
      <div className="chip-photo" style={{ background: team.color + "22" }}>
        <img src={driver.photo} alt={driver.name} draggable={false} />
      </div>
      {lever.fastestLap && <div className="chip-fl-badge" title="Fastest lap">FL</div>}
      {isDnf && <div className="chip-dnf-stamp">DNF</div>}
      <div className="chip-body">
        <div className="chip-row1">
          <span className="chip-num" style={{ color: team.color }}>{String(driver.num).padStart(2,"0")}</span>
          <span className="chip-name">{driver.name}</span>
        </div>
        <div className="chip-row2">
          <span className="chip-team">{team.short}</span>
          <span className="chip-sal">{fmt$(driver.salary)}</span>
        </div>
      </div>
      <div className="chip-meta">
        <div className={"chip-pts " + (pts >= 30 ? "hot" : pts >= 18 ? "warm" : "")}>{pts.toFixed(1)}</div>
        <div className="chip-pts-lbl">PTS</div>
      </div>
      <button className={"chip-gear " + (panelOpen ? "on" : "")} onClick={(e) => { e.stopPropagation(); onTogglePanel(); }} title="Levers">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
      </button>
    </div>
  );
}

// ===========================================================
// Step nav bar — sticky breadcrumb that highlights the active section.
// ===========================================================
function StepNavBar({ activeStep, steps, onClick }) {
  return (
    <div className="step-nav-bar">
      <span className="snb-label-prefix">FLOW</span>
      {steps.map((s, i) => (
        <React.Fragment key={s.target}>
          <button
            className={"snb-item " + (s.done ? "done " : "") + (i === activeStep ? "active " : "")}
            onClick={() => onClick(s.target)}
            title={s.desc}>
            <span className="snb-num">{s.done && i !== activeStep ? "✓" : s.num}</span>
            <span>{s.label}</span>
          </button>
          {i < steps.length - 1 && <span className="snb-divider" />}
        </React.Fragment>
      ))}
    </div>
  );
}

// ===========================================================
// Lever panel — read-only breakdown + lock / exclude only.
// Laps led, fastest lap, and DNF live in the Race Inputs section (Step 2).
// ===========================================================
function LeverPanel({ driver, levers, onChange, onClose }) {
  const team = TEAMS[driver.team];
  const lever = levers[driver.id];
  const finIdx = Math.max(1, Math.min(22, lever.finish)) - 1;
  const posPts = SCORING.positionPoints[finIdx] || 0;
  const placeDiff = (driver.start - lever.finish) * SCORING.placeDiffPerPos;
  const lapsPts = (lever.lapsLed || 0) * SCORING.lapsLedPerLap;
  const flPts = lever.fastestLap ? SCORING.fastestLapBonus : 0;
  const clsPts = lever.classified ? SCORING.classifiedBonus : 0;
  const teammate = DRIVERS.find(dd => dd.team === driver.team && dd.id !== driver.id);
  const tFinish = teammate ? levers[teammate.id].finish : null;
  const beat = (teammate && lever.finish < tFinish) ? SCORING.defeatedTeammateBonus : 0;
  const total = posPts + placeDiff + lapsPts + flPts + clsPts + beat;
  return (
    <div className="lever-panel" onClick={e => e.stopPropagation()}>
      <div className="lever-head">
        <span className="lever-title">LEVERS · <span style={{ color: team.color }}>{driver.first} {driver.name}</span></span>
        <button className="x" onClick={onClose}>×</button>
      </div>
      <div className="lever-grid">
        <label className="lever-tog">
          <input type="checkbox" checked={lever.lock}
            onChange={e => onChange({ lock: e.target.checked, exclude: e.target.checked ? false : lever.exclude })} />
          <span>Lock in lineup</span>
        </label>
        <label className="lever-tog">
          <input type="checkbox" checked={lever.exclude}
            onChange={e => onChange({ exclude: e.target.checked, lock: e.target.checked ? false : lever.lock })} />
          <span>Exclude from pool</span>
        </label>
      </div>
      <div className="lever-breakdown">
        <div className="lbd-title">POINTS BREAKDOWN · finish P{lever.finish}</div>
        <div className="lbd-row">
          <span>P{lever.finish} position pts</span>
          <span className="v">{posPts.toFixed(1)}</span>
        </div>
        <div className="lbd-row">
          <span>Place diff (P{driver.start}→P{lever.finish})</span>
          <span className={"v " + (placeDiff > 0 ? "pos" : placeDiff < 0 ? "neg" : "")}>
            {placeDiff > 0 ? "+" : ""}{placeDiff.toFixed(1)}
          </span>
        </div>
        {lever.lapsLed > 0 && (
          <div className="lbd-row"><span>{lever.lapsLed} laps led × 0.25</span><span className="v">+{lapsPts.toFixed(2)}</span></div>
        )}
        {flPts > 0 && (
          <div className="lbd-row"><span>Fastest lap</span><span className="v">+{flPts}</span></div>
        )}
        {clsPts > 0 && (
          <div className="lbd-row"><span>Classified</span><span className="v">+{clsPts}</span></div>
        )}
        {beat > 0 && teammate && (
          <div className="lbd-row"><span>Beat teammate ({teammate.name} P{tFinish})</span><span className="v">+{beat}</span></div>
        )}
        <div className="lbd-row total"><span>TOTAL</span><span className="v">{total.toFixed(1)}</span></div>
        {driver.fppr != null && (
          <div className="lbd-row fppr"><span>CSV FPPR (reference)</span><span className="v">{driver.fppr.toFixed(2)}</span></div>
        )}
      </div>
    </div>
  );
}

// ===========================================================
// Race inputs row — single driver in the bulk-edit grid.
// ===========================================================
function RaceInputRow({ driver, lever, onChange, onSetFastestLap }) {
  const t = TEAMS[driver.team];
  return (
    <div className={"ri-row " + (!lever.classified ? "ri-dnf-on " : "")}>
      <span className="ri-pos" style={{ color: t.color }}>P{lever.finish}</span>
      <span className="ri-name">{driver.name}</span>
      <input
        type="number" min="0" max="100" step="1"
        className="ri-laps"
        value={lever.lapsLed}
        onChange={e => onChange({ lapsLed: Math.max(0, +e.target.value || 0) })}
        title="Laps led" />
      <button
        className={"ri-fl " + (lever.fastestLap ? "on" : "")}
        onClick={onSetFastestLap}
        title="Fastest lap of the race (only one driver)">
        FL
      </button>
      <button
        className={"ri-dnf " + (!lever.classified ? "on" : "")}
        onClick={() => onChange({ classified: !lever.classified })}
        title="Did not finish (unclassified)">
        DNF
      </button>
    </div>
  );
}

// ===========================================================
// Projection methods — see research notes for citations.
// Each takes the per-driver session ranks + FP1 fastest-lap times and
// returns { [driverId]: score } where lower = better predicted finish.
// ===========================================================
function _randn() { // Box-Muller standard normal
  const u = Math.random() || 1e-9, v = Math.random() || 1e-9;
  return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}

function _weightedRank(ranks, w) {
  const wSum = (w.quali + w.sq + w.sprint + w.fp1) || 1;
  const out = {};
  for (const d of DRIVERS) {
    out[d.id] = (w.quali * ranks.q[d.id] + w.sq * ranks.sq[d.id] + w.sprint * ranks.sp[d.id] + w.fp1 * ranks.fp[d.id]) / wSum;
  }
  return out;
}

// Hunter MM fit for Plackett–Luce (Annals of Statistics 2004).
// `orders` = list of permutations of driver IDs (ascending rank).
function _fitPlackettLuce(orders, ids, iters = 60) {
  const lambda = {}; ids.forEach(d => lambda[d] = 1);
  for (let it = 0; it < iters; it++) {
    const w = {}; const denom = {};
    ids.forEach(d => { w[d] = 0; denom[d] = 0; });
    for (const order of orders) {
      const n = order.length;
      // sumFrom[k] = Σ_{j≥k} λ_{order[j]}
      const sumFrom = new Array(n + 1).fill(0);
      for (let k = n - 1; k >= 0; k--) sumFrom[k] = sumFrom[k + 1] + lambda[order[k]];
      // w_i = times driver finished above last (i.e. index < n-1 always; we count appearances)
      for (let r = 0; r < n - 1; r++) w[order[r]] += 1;
      // d_i = Σ_{k ≤ rank_i} 1 / Σ_{j≥k} λ_j
      for (let r = 0; r < n; r++) {
        for (let k = 0; k <= r; k++) {
          if (sumFrom[k] > 0) denom[order[r]] += 1 / sumFrom[k];
        }
      }
    }
    const next = {};
    let sum = 0;
    for (const d of ids) {
      next[d] = denom[d] > 0 ? (w[d] || 1e-6) / denom[d] : 1e-6;
      sum += next[d];
    }
    for (const d of ids) lambda[d] = next[d] / sum * ids.length;
  }
  return lambda;
}

// Sample Plackett–Luce order via the exponential-race trick (Yellott 1977).
// Repeated N times with team-correlated log-normal noise → mean rank per driver.
function _monteCarloAvgRank(lambda, ids, teamOf, opts = {}) {
  const N = opts.N || 6000;
  const sigD = opts.sigD ?? 0.4;
  const sigT = opts.sigT ?? 0.7;
  const dnfP = opts.dnf ?? 0.05;
  const teams = [...new Set(ids.map(d => teamOf[d]))];
  const sumPos = {}; ids.forEach(d => sumPos[d] = 0);
  for (let sim = 0; sim < N; sim++) {
    const tNoise = {};
    teams.forEach(t => tNoise[t] = _randn() * sigT);
    const e = ids.map(d => {
      if (Math.random() < dnfP) {
        return { d, e: 1e9 + Math.random() }; // DNFs land at the tail
      }
      const lam = lambda[d] * Math.exp(tNoise[teamOf[d]] + _randn() * sigD);
      return { d, e: -Math.log(Math.random() || 1e-12) / Math.max(lam, 1e-9) };
    });
    e.sort((a, b) => a.e - b.e);
    e.forEach((x, idx) => sumPos[x.d] += idx + 1);
  }
  const out = {}; ids.forEach(d => out[d] = sumPos[d] / N);
  return out;
}

// Distributional Monte Carlo — returns per-driver position probability distribution
// plus win/podium/top-10/DNF rates. Per-driver DNF prior optional (scales by grid
// position when not provided: P1-5 ~4%, P6-10 ~6%, P11-15 ~8%, P16+ ~12%).
function _monteCarloFull(lambda, ids, teamOf, opts = {}) {
  const N = opts.N || 12000;
  const sigD = opts.sigD ?? 0.4;
  const sigT = opts.sigT ?? 0.7;
  const dnfPrior = opts.dnfPrior || {};
  const teams = [...new Set(ids.map(d => teamOf[d]))];
  const posCount = {}; ids.forEach(d => posCount[d] = new Array(22).fill(0));
  const dnfCount = {}; ids.forEach(d => dnfCount[d] = 0);
  for (let sim = 0; sim < N; sim++) {
    const tNoise = {};
    teams.forEach(t => tNoise[t] = _randn() * sigT);
    const survivors = [];
    const dnfs = [];
    for (const d of ids) {
      const dnf = dnfPrior[d] ?? 0.05;
      if (Math.random() < dnf) { dnfs.push(d); dnfCount[d]++; }
      else survivors.push(d);
    }
    const e = survivors.map(d => {
      const lam = lambda[d] * Math.exp(tNoise[teamOf[d]] + _randn() * sigD);
      return { d, e: -Math.log(Math.random() || 1e-12) / Math.max(lam, 1e-9) };
    });
    e.sort((a, b) => a.e - b.e);
    e.forEach((x, idx) => posCount[x.d][idx]++);
    // DNFs land randomly in the tail
    const sLen = survivors.length;
    const shuffled = dnfs.slice();
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    shuffled.forEach((d, idx) => {
      const slot = sLen + idx;
      if (slot < 22) posCount[d][slot]++;
    });
  }
  const stats = {};
  for (const d of ids) {
    const counts = posCount[d];
    const avgRank = counts.reduce((s, c, i) => s + (i + 1) * c, 0) / N;
    const winPct = counts[0] / N;
    const podiumPct = (counts[0] + counts[1] + counts[2]) / N;
    const top10Pct = counts.slice(0, 10).reduce((a, b) => a + b, 0) / N;
    const dnfPct = dnfCount[d] / N;
    let modeIdx = 0, modeMax = -1;
    counts.forEach((c, i) => { if (c > modeMax) { modeMax = c; modeIdx = i; } });
    stats[d] = {
      avgRank, winPct, podiumPct, top10Pct, dnfPct,
      modeRank: modeIdx + 1,
      probs: counts.map(c => c / N),
    };
  }
  return stats;
}

// Pace-based λ from FP1 lap times (Fry/Brighton/Fanzon "time-rank duality" 2024).
function _pacePL(lapTimes, ids) {
  const valid = ids.map(d => lapTimes[d]).filter(t => t != null && t > 0);
  if (valid.length < 5) {
    const out = {}; ids.forEach((d, i) => out[d] = i + 1);
    return out;
  }
  const mean = valid.reduce((a, b) => a + b, 0) / valid.length;
  const std = Math.sqrt(valid.reduce((s, t) => s + (t - mean) ** 2, 0) / valid.length) || 1;
  const lambda = {};
  ids.forEach(d => {
    const t = lapTimes[d];
    lambda[d] = t != null ? Math.exp(-3 * ((t - mean) / std)) : 1e-3;
  });
  return lambda;
}

// Convert a {driver: λ} or {driver: avg_position} dict to a projection rank.
function _scoresToRanks(scores, ids, ascending) {
  const sorted = [...ids].sort((a, b) => ascending ? scores[a] - scores[b] : scores[b] - scores[a]);
  const out = {};
  sorted.forEach((d, i) => out[d] = i + 1);
  return out;
}

// Per-driver DNF prior scaled by grid position. Front-runners are mechanically
// safer (better cars) and statistically less crash-prone.
function _dnfPriorByGrid() {
  const out = {};
  for (const d of (window.F1DATA?.DRIVERS || [])) {
    if (d.start <= 5)       out[d.id] = 0.04;
    else if (d.start <= 10) out[d.id] = 0.06;
    else if (d.start <= 15) out[d.id] = 0.08;
    else                    out[d.id] = 0.12;
  }
  return out;
}

// Run all 6 projection methods + a distributional MC, return aggregated forecast.
// `levers` is the user's curated prediction (used for "expected pts" via the DK formula).
function _buildForecast({ ranks, lapTimes, levers, drivers, teams }) {
  const ids = drivers.map(d => d.id);
  const teamOf = {}; drivers.forEach(d => teamOf[d.id] = d.team);

  // Method ranks (lower = better)
  const blend   = _scoresToRanks(_weightedRank(ranks, { quali: 30, sq: 20, sprint: 40, fp1: 10 }), ids, true);
  const qmirror = _scoresToRanks(ranks.q, ids, true);
  const qblend  = _scoresToRanks(_weightedRank(ranks, { quali: 1.56, sq: 0.38, sprint: 0.37, fp1: 0.29 }), ids, true);
  const sortedByRank = (rd) => [...ids].sort((a, b) => rd[a] - rd[b]);
  const plLambda = _fitPlackettLuce(
    [sortedByRank(ranks.q), sortedByRank(ranks.sq), sortedByRank(ranks.sp), sortedByRank(ranks.fp)],
    ids
  );
  const pl   = _scoresToRanks(plLambda, ids, false);
  const pace = _scoresToRanks(_pacePL(lapTimes, ids), ids, false);

  // MC: baseline λ from Q-blended → exponential curve. Use per-driver DNF prior.
  const baseQB = _weightedRank(ranks, { quali: 1.56, sq: 0.38, sprint: 0.37, fp1: 0.29 });
  const lambda = {}; ids.forEach(id => lambda[id] = Math.exp(-(baseQB[id] - 11) * 0.18));
  const dnfPrior = _dnfPriorByGrid();
  const mcStats = _monteCarloFull(lambda, ids, teamOf, { N: 15000, dnfPrior });
  const mc = {};
  ids.forEach(id => mc[id] = Math.round(mcStats[id].avgRank));

  const methodRanks = { blend, qmirror, qblend, pl, pace, mc };

  // Per-driver aggregation
  const perDriver = {};
  for (const d of drivers) {
    const ranksByMethod = {
      blend:   methodRanks.blend[d.id],
      qmirror: methodRanks.qmirror[d.id],
      qblend:  methodRanks.qblend[d.id],
      pl:      methodRanks.pl[d.id],
      pace:    methodRanks.pace[d.id],
      mc:      methodRanks.mc[d.id],
    };
    const arr = Object.values(ranksByMethod);
    const meanRank = arr.reduce((a, b) => a + b, 0) / arr.length;
    const variance = arr.reduce((s, r) => s + (r - meanRank) ** 2, 0) / arr.length;
    const stdRank = Math.sqrt(variance);
    perDriver[d.id] = {
      driver: d,
      ranksByMethod,
      meanRank, stdRank,
      mc: mcStats[d.id],
      userFinish: levers[d.id]?.finish ?? d.start,
      expectedPts: window.F1OPT.driverPoints(d, levers),
    };
  }

  // Sort by user's predicted finish (their authoritative call) for the main list.
  const ranked = drivers.map(d => perDriver[d.id])
    .sort((a, b) => a.userFinish - b.userFinish)
    .map((p, i) => ({ ...p, displayRank: i + 1 }));

  // Ensemble agreement: average pairwise rank distance, normalized to [0, 1] where 1 = full agreement.
  const allStds = drivers.map(d => perDriver[d.id].stdRank);
  const meanStd = allStds.reduce((a, b) => a + b, 0) / allStds.length;
  const agreement = Math.max(0, Math.min(1, 1 - meanStd / 6)); // 6 = arbitrary scale
  const mostContested = [...drivers].map(d => perDriver[d.id])
    .sort((a, b) => b.stdRank - a.stdRank)[0];
  const mostAgreed = [...drivers].map(d => perDriver[d.id])
    .sort((a, b) => a.stdRank - b.stdRank)[0];

  return { ranked, perDriver, methodRanks, agreement, mostContested, mostAgreed };
}

const PROJECTION_METHODS = [
  { id: "blend",    label: "Weighted Blend",  short: "BLEND",   desc: "Weighted-rank average of all 4 sessions. Tune the sliders to bias toward whichever signal you trust." },
  { id: "qmirror",  label: "Q-Mirror",        short: "Q ONLY",  desc: "Use the qualifying grid as the projection. Pole→P1, P22→P22. Highest single-signal raw accuracy in published lit (ρ≈0.76 with finish, pole win-rate 43–70%)." },
  { id: "qblend",   label: "Q-Blended",       short: "QBLEND",  desc: "Ordinal-logit weights from arXiv 2507.10966 (2025). Q=1.56 dominates FP3/SQ=0.38 / FP2/Sprint=0.37 / FP1=0.29." },
  { id: "pl",       label: "Plackett–Luce",   short: "PL MLE",  desc: "Hunter MM fit (Annals of Statistics 2004) of latent skill λᵢ to all 4 observed session orders. Sort by λ descending." },
  { id: "pace",     label: "Pace-based",      short: "PACE",    desc: "λᵢ = exp(−3·z(FP1 fastest lap)) per Fry/Brighton/Fanzon \"time-rank duality\" (2024)." },
  { id: "mc",       label: "Monte Carlo",     short: "MC SIM",  desc: "15,000 PL sims on top of Q-blended baseline with team-correlated noise (σ_t=0.7, σ_d=0.4) and grid-scaled DNF prior. Best for distributional metrics." },
];

// ===========================================================
// Session Analysis (Step ⓪) — fetches FP1 / Sprint Quali / Sprint / Quali
// from the OpenF1 API, computes a weighted-rank projection, and offers
// a one-click import to the predicted-finish grid below.
// ===========================================================
// Versioned per race weekend so cached session data doesn't leak across races.
const ANALYSIS_CACHE_KEY = ANALYSIS_CACHE_KEY_VERSIONED;

function formatRelativeTime(ts) {
  if (!ts) return "—";
  const diff = Date.now() - ts;
  const s = Math.floor(diff / 1000);
  if (s < 60) return `${s}s ago`;
  const m = Math.floor(s / 60);
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  return new Date(ts).toLocaleString();
}

function SessionAnalysis({ levers, setLevers }) {
  const [data, setData] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem(ANALYSIS_CACHE_KEY) || "null"); } catch (e) { return null; }
  });
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [weights, setWeights] = React.useState({ quali: 30, sq: 20, sprint: 40, fp1: 10 });
  const [collapsed, setCollapsed] = React.useState(false);
  const [method, setMethod] = React.useState("blend");

  const fetchAll = async () => {
    setLoading(true);
    setError(null);
    // OpenF1 caps at 3 requests/second. Fetch sequentially with a small spacing
    // and retry once on a rate-limit response.
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    const fetchJson = async (url, attempt = 0) => {
      const res = await fetch(url);
      const body = await res.json();
      if (!Array.isArray(body) && body && body.error) {
        if (attempt < 2) { await sleep(800); return fetchJson(url, attempt + 1); }
        throw new Error(body.error + (body.detail ? ` (${body.detail})` : ""));
      }
      return body;
    };
    try {
      const quali = await fetchJson(`https://api.openf1.org/v1/session_result?session_key=${SESSIONS.quali}`);
      await sleep(350);
      const sq = await fetchJson(`https://api.openf1.org/v1/session_result?session_key=${SESSIONS.sq}`);
      await sleep(350);
      const sprint = await fetchJson(`https://api.openf1.org/v1/session_result?session_key=${SESSIONS.sprint}`);
      await sleep(350);
      const fp1Laps = await fetchJson(`https://api.openf1.org/v1/laps?session_key=${SESSIONS.fp1}`);
      // Reduce laps → fastest lap per driver (filter out unrealistic times)
      const fp1Best = {};
      for (const l of fp1Laps) {
        const n = l.driver_number, dur = l.lap_duration;
        if (!n || !dur || dur < 80 || dur > 110) continue;
        if (fp1Best[n] == null || dur < fp1Best[n]) fp1Best[n] = dur;
      }
      const fp1Ranked = Object.entries(fp1Best)
        .sort((a, b) => a[1] - b[1])
        .map(([n, t], i) => ({ driver_number: +n, position: i + 1, lap: t }));
      const next = { quali, sq, sprint, fp1: fp1Ranked, fetchedAt: Date.now() };
      setData(next);
      try { localStorage.setItem(ANALYSIS_CACHE_KEY, JSON.stringify(next)); } catch (e) {}
      window.dispatchEvent(new CustomEvent("f1apex:analysisLoaded"));
    } catch (e) {
      setError(e.message || String(e));
    }
    setLoading(false);
  };

  React.useEffect(() => { if (!data) fetchAll(); }, []);

  // Build per-driver row: ranks across all 4 sessions + projection from selected method
  const rows = React.useMemo(() => {
    if (!data) return [];
    const rankFor = (arr, key = "position") => {
      const m = {};
      if (!Array.isArray(arr)) return m;
      for (const r of arr) {
        const id = DRIVER_BY_NUM[r.driver_number];
        if (id && r[key] != null) m[id] = r[key];
      }
      return m;
    };
    // OpenF1 occasionally returns position=null for drivers promoted through a
    // knockout without a clean lap (Hadjar). Fall back to verified grid `start`.
    const qR = rankFor(data.quali);
    for (const d of DRIVERS) if (qR[d.id] == null) qR[d.id] = d.start;
    const sqR = rankFor(data.sq);
    const spR = rankFor(data.sprint);
    const fp1R = rankFor(data.fp1);
    // Fill missing ranks (DNF / no time) with P22 so the math has a number.
    for (const d of DRIVERS) {
      if (sqR[d.id] == null) sqR[d.id] = 22;
      if (spR[d.id] == null) spR[d.id] = 22;
      if (fp1R[d.id] == null) fp1R[d.id] = 22;
    }
    const ranks = { q: qR, sq: sqR, sp: spR, fp: fp1R };
    const lapTimes = {};
    if (Array.isArray(data.fp1)) for (const r of data.fp1) {
      const id = DRIVER_BY_NUM[r.driver_number];
      if (id && r.lap != null) lapTimes[id] = r.lap;
    }
    const ids = DRIVERS.map(d => d.id);
    const teamOf = {}; DRIVERS.forEach(d => teamOf[d.id] = d.team);

    let scoreMap; // {id: numeric score, lower = better}
    let scoreLabel = "SCORE";

    if (method === "blend") {
      scoreMap = _weightedRank(ranks, weights);
    } else if (method === "qmirror") {
      scoreMap = {};
      ids.forEach(id => scoreMap[id] = qR[id]);
      scoreLabel = "Q";
    } else if (method === "qblend") {
      scoreMap = _weightedRank(ranks, { quali: 1.56, sq: 0.38, sprint: 0.37, fp1: 0.29 });
    } else if (method === "pl") {
      const sortedByRank = (rd) => [...ids].sort((a, b) => rd[a] - rd[b]);
      const orders = [sortedByRank(qR), sortedByRank(sqR), sortedByRank(spR), sortedByRank(fp1R)];
      const lambda = _fitPlackettLuce(orders, ids);
      // Higher λ = better, so flip sign for "lower = better" convention
      scoreMap = {}; ids.forEach(id => scoreMap[id] = -Math.log(Math.max(lambda[id], 1e-9)));
      scoreLabel = "−log λ";
    } else if (method === "pace") {
      const lambda = _pacePL(lapTimes, ids);
      scoreMap = {}; ids.forEach(id => scoreMap[id] = -Math.log(Math.max(lambda[id], 1e-9)));
      scoreLabel = "−log λ";
    } else if (method === "mc") {
      // Base λ from Q-blended ranks → exp curve
      const base = _weightedRank(ranks, { quali: 1.56, sq: 0.38, sprint: 0.37, fp1: 0.29 });
      const lambda = {};
      ids.forEach(id => lambda[id] = Math.exp(-(base[id] - 11) * 0.18));
      scoreMap = _monteCarloAvgRank(lambda, ids, teamOf);
      scoreLabel = "AVG POS";
    }

    return DRIVERS.map(d => {
      return {
        driver: d, q: qR[d.id], sq: sqR[d.id], sp: spR[d.id], fp: fp1R[d.id],
        score: scoreMap[d.id],
      };
    }).sort((a, b) => a.score - b.score)
      .map((r, i) => ({ ...r, projected: i + 1, scoreLabel }));
  }, [data, weights, method]);

  const importToGrid = () => {
    if (!rows.length) return;
    setLevers(prev => {
      const next = { ...prev };
      for (const r of rows) {
        next[r.driver.id] = { ...prev[r.driver.id], finish: r.projected };
      }
      return next;
    });
  };

  const setW = (k, v) => setWeights(w => ({ ...w, [k]: Math.max(0, Math.min(100, +v || 0)) }));

  return (
    <div className="session-analysis">
      <div className="step-marker sa-head">
        <span className="step-num">⓪</span>
        <span className="step-title">SESSION ANALYSIS</span>
        <span className="step-sub">live OpenF1 data · weighted rank → predicted finish</span>
        <div className="sa-head-actions">
          <button className="ghost" onClick={fetchAll} disabled={loading}>
            {loading ? "FETCHING…" : data ? "REFRESH" : "FETCH"}
          </button>
          {data && (
            <button className="ghost" onClick={() => setCollapsed(c => !c)}>
              {collapsed ? "EXPAND" : "COLLAPSE"}
            </button>
          )}
        </div>
      </div>

      {error && (
        <div className="sa-error">
          ⚠ {error}
          <button className="sa-retry" onClick={fetchAll}>RETRY</button>
        </div>
      )}

      {data && !collapsed && (
        <div className="sa-meta">
          <span className={"dot " + (Date.now() - (data.fetchedAt || 0) > 30 * 60 * 1000 ? "stale" : "")} />
          <span>OpenF1 · {data.quali?.length || 0} drivers · fetched {formatRelativeTime(data.fetchedAt)}</span>
          <span style={{ color: "var(--muted-2)" }}>·</span>
          <span>FP1 · SQ · SPRINT · QUALI all loaded</span>
        </div>
      )}

      {!collapsed && data && (
        <>
          <div className="sa-templates">
            <span className="sa-templates-label">PROJECTION MODEL</span>
            <div className="sa-template-pills">
              {PROJECTION_METHODS.map(m => (
                <button
                  key={m.id}
                  className={"sa-pill " + (method === m.id ? "on" : "")}
                  onClick={() => setMethod(m.id)}
                  title={m.desc}>
                  {m.short}
                </button>
              ))}
            </div>
          </div>
          <div className="sa-method-desc">
            {PROJECTION_METHODS.find(m => m.id === method)?.desc}
          </div>

          <div className="sa-controls">
            {method === "blend" ? (
              <div className="sa-weights">
                {[
                  ["quali",  RACE.sessionLabels.quali,  RACE.sessionLabelsSub.quali],
                  ["sq",     RACE.sessionLabels.sq,     RACE.sessionLabelsSub.sq],
                  ["sprint", RACE.sessionLabels.sprint, RACE.sessionLabelsSub.sprint],
                  ["fp1",    RACE.sessionLabels.fp1,    RACE.sessionLabelsSub.fp1],
                ].map(([k, label, sub]) => (
                  <div key={k} className="sa-weight">
                    <div className="saw-label">{label}<span className="saw-sub">{sub}</span></div>
                    <input type="range" min="0" max="100" value={weights[k]} onChange={e => setW(k, e.target.value)} />
                    <div className="saw-val">{weights[k]}%</div>
                  </div>
                ))}
              </div>
            ) : (
              <div className="sa-method-locked">
                <span className="saml-tag">FIXED FORMULA</span>
                <span className="saml-text">This template uses a published model — sliders disabled. Switch to <strong>BLEND</strong> for tunable weights.</span>
              </div>
            )}
            <button className="sa-import" onClick={importToGrid}>
              ↓ IMPORT TO PREDICTED FINISH
            </button>
          </div>

          <div className="sa-table-wrap">
            <table className="sa-table">
              <thead>
                <tr>
                  <th>PROJ</th>
                  <th>DRIVER</th>
                  <th>TEAM</th>
                  <th className="r">{RACE.sessionLabelsShort.quali}</th>
                  <th className="r">{RACE.sessionLabelsShort.sq}</th>
                  <th className="r">{RACE.sessionLabelsShort.sprint}</th>
                  <th className="r">{RACE.sessionLabelsShort.fp1}</th>
                  <th className="r">{rows[0]?.scoreLabel || "SCORE"}</th>
                </tr>
              </thead>
              <tbody>
                {rows.map(r => {
                  const t = TEAMS[r.driver.team];
                  return (
                    <tr key={r.driver.id}>
                      <td className="r-rank">P{r.projected}</td>
                      <td className="r-cpt"><span className="r-dot" style={{ background: t.color }}></span>{r.driver.name}</td>
                      <td className="r-con">{t.short}</td>
                      <td className="r right">{r.q}</td>
                      <td className="r right">{r.sq}</td>
                      <td className="r right">{r.sp}</td>
                      <td className="r right">{r.fp}</td>
                      <td className="r right hi">{r.score != null ? r.score.toFixed(2) : "—"}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </>
      )}
    </div>
  );
}

// ===========================================================
// MAIN APP
// ===========================================================
function App() {
  const persisted = React.useMemo(() => loadPersisted(), []);
  const [levers, setLevers] = React.useState(() => persisted?.levers || makeInitialLevers());
  const [conState, setConState] = React.useState(() => persisted?.conState || makeInitialConState());
  const [salaryCap, setSalaryCap] = React.useState(() => persisted?.salaryCap || 50000);
  const [topN, setTopN] = React.useState(() => persisted?.topN || 25);
  const [openPanel, setOpenPanel] = React.useState(null);
  const [drag, setDrag] = React.useState(null);
  const [results, setResults] = React.useState(null);
  const [running, setRunning] = React.useState(false);
  const [view, setView] = React.useState("grid");

  const dragRef = React.useRef(null);
  dragRef.current = drag;

  React.useEffect(() => { persist({ levers, conState, salaryCap, topN }); }, [levers, conState, salaryCap, topN]);

  const updateLever = (id, patch) => setLevers(prev => ({ ...prev, [id]: { ...prev[id], ...patch } }));
  const updateConState = (team, patch) => setConState(prev => ({ ...prev, [team]: { ...prev[team], ...patch } }));

  // Set fastest lap on a single driver — clears it from everyone else.
  const setFastestLap = (id) => setLevers(prev => {
    const next = {};
    const target = !prev[id].fastestLap;
    for (const d of DRIVERS) {
      next[d.id] = { ...prev[d.id], fastestLap: d.id === id ? target : false };
    }
    return next;
  });

  // Auto-fill laps-led + fastest-lap based on empirical 2022-2024 ground-effect-era data.
  //
  // Two key findings from the research:
  //   1. Laps-led correlates with START position more than FINISH (ρ −0.62 vs −0.55).
  //      A driver who starts pole but DNFs at lap 50 still led 50 laps. We blend 65%
  //      start / 35% finish.
  //   2. FL goes to the race winner ~36% of races (modal outcome). For a deterministic
  //      single-pick auto-fill, winner is the right answer. User can manually re-assign
  //      via the Step 2 panel for "punt FL" scenarios.
  //
  // Per-rank share array is sourced from STATS F1 season totals (2022-2024) cross-
  // referenced with WDC standings. Sums to 1.0 across all 22 ranks.
  const autoFillRaceInputs = () => {
    const TOTAL_LAPS = RACE.totalLaps; // race-specific
    const SHARE = [
      0.520, 0.165, 0.095, 0.060, 0.045,
      0.030, 0.020, 0.015, 0.012, 0.010,
      0.008, 0.006, 0.004, 0.003, 0.002,
      0.002, 0.001, 0.001, 0.001, 0.000,
      0.000, 0.000,
    ];
    const startWeight = 0.65;
    const finishWeight = 0.35;
    const ordered = [...DRIVERS].sort((a, b) => levers[a.id].finish - levers[b.id].finish);
    const winnerId = ordered[0]?.id;

    // Blended per-driver share: 65% from start position, 35% from finish position.
    const rawShare = {};
    for (const d of DRIVERS) {
      const sIdx = Math.min(21, Math.max(0, d.start - 1));
      const fIdx = Math.min(21, Math.max(0, levers[d.id].finish - 1));
      rawShare[d.id] = startWeight * SHARE[sIdx] + finishWeight * SHARE[fIdx];
    }
    // Normalize and convert to integer lap counts.
    const total = Object.values(rawShare).reduce((a, b) => a + b, 0) || 1;
    const laps = {};
    let assigned = 0;
    for (const d of DRIVERS) {
      laps[d.id] = Math.round((rawShare[d.id] / total) * TOTAL_LAPS);
      assigned += laps[d.id];
    }
    // Reconcile rounding error onto the projected winner.
    if (winnerId) laps[winnerId] = Math.max(0, laps[winnerId] + (TOTAL_LAPS - assigned));

    setLevers(prev => {
      const next = {};
      for (const d of DRIVERS) {
        next[d.id] = { ...prev[d.id], lapsLed: laps[d.id], fastestLap: d.id === winnerId };
      }
      return next;
    });
  };

  const onChipMouseDown = (e, driverId) => {
    if (e.button !== 0) return;
    if (e.target.closest(".chip-gear")) return;
    e.preventDefault();
    const rect = e.currentTarget.getBoundingClientRect();
    setDrag({
      driverId,
      x: e.clientX, y: e.clientY,
      ox: e.clientX - rect.left, oy: e.clientY - rect.top,
      w: rect.width, h: rect.height,
      hoverPos: null
    });
  };

  React.useEffect(() => {
    if (!drag) return;
    const onMove = (e) => {
      const slot = document.elementFromPoint(e.clientX, e.clientY)?.closest("[data-finish-slot]");
      const hoverPos = slot ? +slot.dataset.finishSlot : null;
      setDrag(d => d ? { ...d, x: e.clientX, y: e.clientY, hoverPos } : d);
    };
    const onUp = () => {
      const cur = dragRef.current;
      if (cur && cur.hoverPos != null) {
        const newPos = cur.hoverPos;
        // Shift-style reorder (NOT swap): drivers between source and target slide
        // toward the source by one position to make room.
        setLevers(prev => {
          const oldPos = prev[cur.driverId].finish;
          if (oldPos === newPos) return prev;
          const byFinish = {};
          for (const d of DRIVERS) byFinish[prev[d.id].finish] = d.id;
          const next = { ...prev };
          if (oldPos > newPos) {
            // Moving up: drivers in [newPos, oldPos-1] shift down by 1
            for (let p = oldPos - 1; p >= newPos; p--) {
              const dId = byFinish[p];
              if (dId) next[dId] = { ...prev[dId], finish: p + 1 };
            }
          } else {
            // Moving down: drivers in [oldPos+1, newPos] shift up by 1
            for (let p = oldPos + 1; p <= newPos; p++) {
              const dId = byFinish[p];
              if (dId) next[dId] = { ...prev[dId], finish: p - 1 };
            }
          }
          next[cur.driverId] = { ...prev[cur.driverId], finish: newPos };
          return next;
        });
      }
      setDrag(null);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    return () => {
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
  }, [drag != null]);

  const execute = () => {
    setRunning(true);
    setView("results");
    setTimeout(() => {
      const r = findTopLineups({ levers, conState, salaryCap, topN });
      setResults(r);
      setRunning(false);
    }, 350);
  };

  const resetFinish = () => {
    setLevers(prev => {
      const n = { ...prev };
      for (const d of DRIVERS) n[d.id] = { ...prev[d.id], finish: d.start };
      return n;
    });
  };

  const totalDelta = DRIVERS.reduce((s, d) => s + Math.abs(d.start - levers[d.id].finish), 0);

  return (
    <div className="app">
      <Header view={view} setView={setView} hasResults={!!results} />

      {view === "grid" && (
        <GridView
          levers={levers} updateLever={updateLever} setLevers={setLevers}
          setFastestLap={setFastestLap} autoFillRaceInputs={autoFillRaceInputs}
          conState={conState} updateConState={updateConState}
          openPanel={openPanel} setOpenPanel={setOpenPanel}
          onChipMouseDown={onChipMouseDown}
          drag={drag}
          salaryCap={salaryCap} setSalaryCap={setSalaryCap}
          topN={topN} setTopN={setTopN}
          execute={execute}
          resetFinish={resetFinish}
          totalDelta={totalDelta}
        />
      )}

      {view === "forecast" && (
        <ForecastView levers={levers} onBack={() => setView("grid")} />
      )}

      {view === "results" && (
        <ResultsView
          results={results} running={running}
          salaryCap={salaryCap}
          onBack={() => setView("grid")}
        />
      )}

      {drag && (() => {
        const driver = DRIVERS.find(d => d.id === drag.driverId);
        return (
          <div className="drag-ghost" style={{
            left: drag.x - drag.ox, top: drag.y - drag.oy,
            width: drag.w, height: drag.h
          }}>
            <DriverChip
              driver={driver} levers={levers}
              dragging onMouseDown={() => {}} onTogglePanel={() => {}}
            />
          </div>
        );
      })()}
    </div>
  );
}

// ===========================================================
// HEADER
// ===========================================================
function Header({ view, setView, hasResults }) {
  return (
    <header className="hdr">
      <div className="hdr-left">
        <div className="logo-block">
          <div className="logo-mark"><span></span><span></span><span></span></div>
          <div>
            <div className="logo-title">APEX OPTIMIZER</div>
            <div className="logo-sub">DK · F1 CLASSIC · {RACE.name.toUpperCase()} {RACE.year} · LOCK {RACE.lockTimeET}</div>
          </div>
        </div>
      </div>
      <div className="hdr-tabs">
        <button className={view === "grid" ? "on" : ""} onClick={() => setView("grid")}>① GRID & INPUTS</button>
        <button className={view === "forecast" ? "on" : ""} onClick={() => setView("forecast")}>② RACE FORECAST</button>
        <button className={view === "results" ? "on" : ""} onClick={() => setView("results")} disabled={!hasResults}>③ TOP LINEUPS</button>
      </div>
      <div className="hdr-right">
        <div className="status-pill"><span className="dot"></span>LIVE PROJECTIONS</div>
      </div>
    </header>
  );
}

// ===========================================================
// GRID VIEW
// ===========================================================
function GridView(props) {
  const {
    levers, updateLever, setLevers, setFastestLap, autoFillRaceInputs,
    conState, updateConState,
    openPanel, setOpenPanel, onChipMouseDown,
    drag, salaryCap, setSalaryCap, topN, setTopN,
    execute, resetFinish, totalDelta
  } = props;

  const startSorted = [...DRIVERS].sort((a, b) => a.start - b.start);
  const driverByFinish = {};
  for (const d of DRIVERS) driverByFinish[levers[d.id].finish] = d;

  // ============== STEP NAV STATE ==============
  const [activeStep, setActiveStep] = React.useState(0);
  const [analysisTick, setAnalysisTick] = React.useState(0);
  React.useEffect(() => {
    const handler = () => setAnalysisTick(t => t + 1);
    window.addEventListener("f1apex:analysisLoaded", handler);
    return () => window.removeEventListener("f1apex:analysisLoaded", handler);
  }, []);
  React.useEffect(() => {
    const sels = [".session-analysis", ".grid-cols", ".race-inputs", ".con-strip"];
    const els = sels.map(s => document.querySelector(s)).filter(Boolean);
    if (!els.length) return;
    const obs = new IntersectionObserver((entries) => {
      // Pick the entry closest to the top that's intersecting
      const visible = entries.filter(e => e.isIntersecting)
        .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      if (visible.length) {
        const idx = sels.indexOf("." + visible[0].target.className.split(" ")[0]);
        if (idx >= 0) setActiveStep(idx);
      }
    }, { rootMargin: "-30% 0px -55% 0px", threshold: 0 });
    els.forEach(el => obs.observe(el));
    return () => obs.disconnect();
  }, []);

  // Done-state heuristics for step indicators
  const stepDone = {
    0: false, // session analysis: needs API data — set below from SessionAnalysis result
    1: DRIVERS.some(d => levers[d.id].finish !== d.start),
    2: DRIVERS.some(d => levers[d.id].lapsLed > 0 || levers[d.id].fastestLap || !levers[d.id].classified),
    3: true, // constructor pool is auto-inferred so always "done"
  };
  const stepNavSteps = [
    { num: "0", label: "ANALYSIS",     target: ".session-analysis", done: !!localStorage.getItem(ANALYSIS_CACHE_KEY), desc: "Live OpenF1 weekend signals + projection templates" },
    { num: "1", label: "FINISH ORDER", target: ".grid-cols",        done: stepDone[1], desc: "Drag drivers to set the predicted finishing order" },
    { num: "2", label: "RACE INPUTS",  target: ".race-inputs",      done: stepDone[2], desc: "Laps led · fastest lap · DNFs" },
    { num: "3", label: "CONSTRUCTORS", target: ".con-strip",        done: stepDone[3], desc: "Auto-fired bonuses · lock or exclude" },
  ];
  const scrollToStep = (sel) => {
    const el = document.querySelector(sel);
    if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
  };

  const startRefs = React.useRef({});
  const finRefs = React.useRef({});
  const containerRef = React.useRef(null);
  const [tick, setTick] = React.useState(0);
  React.useEffect(() => {
    const onResize = () => setTick(t => t + 1);
    window.addEventListener("resize", onResize);
    const ro = new ResizeObserver(onResize);
    if (containerRef.current) ro.observe(containerRef.current);
    return () => { window.removeEventListener("resize", onResize); ro.disconnect(); };
  }, []);
  React.useEffect(() => { setTick(t => t + 1); }, [levers, drag]);

  const lines = React.useMemo(() => {
    if (!containerRef.current) return [];
    const cb = containerRef.current.getBoundingClientRect();
    const out = [];
    for (const d of DRIVERS) {
      const sEl = startRefs.current[d.id];
      const fEl = finRefs.current[levers[d.id].finish];
      if (!sEl || !fEl) continue;
      const sR = sEl.getBoundingClientRect();
      const fR = fEl.getBoundingClientRect();
      const x1 = sR.right - cb.left;
      const y1 = sR.top + sR.height / 2 - cb.top;
      const x2 = fR.left - cb.left;
      const y2 = fR.top + fR.height / 2 - cb.top;
      out.push({ id: d.id, color: TEAMS[d.team].color, x1, y1, x2, y2, delta: d.start - levers[d.id].finish });
    }
    return out;
  }, [levers, tick]);

  // Race inputs sort = predicted finish order
  const finishOrdered = [...DRIVERS].sort((a, b) => levers[a.id].finish - levers[b.id].finish);
  const totalLapsLed = DRIVERS.reduce((s, d) => s + (levers[d.id].lapsLed || 0), 0);
  const flDriver = DRIVERS.find(d => levers[d.id].fastestLap);
  const dnfCount = DRIVERS.filter(d => !levers[d.id].classified).length;

  return (
    <div className="grid-wrap" ref={containerRef}>

      <StepNavBar activeStep={activeStep} steps={stepNavSteps} onClick={scrollToStep} />

      <svg className="lines-svg" preserveAspectRatio="none">
        {lines.map(l => {
          const cx1 = l.x1 + (l.x2 - l.x1) * 0.5;
          const path = `M ${l.x1} ${l.y1} C ${cx1} ${l.y1} ${cx1} ${l.y2} ${l.x2} ${l.y2}`;
          const isMoved = l.delta !== 0;
          return (
            <path key={l.id} d={path} stroke={l.color}
              strokeWidth={isMoved ? 2 : 1}
              strokeOpacity={isMoved ? 0.55 : 0.18}
              fill="none"
              strokeDasharray={isMoved ? "" : "3 4"} />
          );
        })}
      </svg>

      <SessionAnalysis levers={levers} setLevers={setLevers} />

      <div className="step-marker">
        <span className="step-num">①</span>
        <span className="step-title">SET PREDICTED FINISH ORDER</span>
        <span className="step-sub">drag drivers in the right column · everyone else slides into place</span>
      </div>

      <div className="grid-cols">

        <div className="col col-start">
          <div className="col-head">
            <div className="col-title-row">
              <svg className="lock-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><rect x="4" y="11" width="16" height="11" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
              <span className="col-title">STARTING GRID</span>
            </div>
            <div className="col-sub">official qualifying result</div>
            <div className="lock-badge">
              <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"><rect x="4" y="11" width="16" height="11" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
              LOCKED
            </div>
          </div>
          <div className="rows">
            {startSorted.map(d => (
              <div key={d.id} className="row" ref={el => startRefs.current[d.id] = el}>
                <div className="pos-cell">
                  <div className="pos-num">{String(d.start).padStart(2, "0")}</div>
                  {d.start <= 3 && <div className={"pos-medal m" + d.start}></div>}
                </div>
                <DriverChip
                  driver={d} levers={levers}
                  dragging={drag && drag.driverId === d.id}
                  onMouseDown={(e) => onChipMouseDown(e, d.id)}
                  onTogglePanel={() => setOpenPanel(openPanel === d.id ? null : d.id)}
                  panelOpen={openPanel === d.id}
                />
              </div>
            ))}
          </div>
        </div>

        <div className="col col-mid">
          <div className="mid-track">
            {Array.from({ length: 22 }).map((_, i) => {
              const pos = i + 1;
              const d = DRIVERS.find(dd => dd.start === pos);
              const moved = d && (levers[d.id].finish !== d.start);
              const delta = d ? d.start - levers[d.id].finish : 0;
              return (
                <div key={pos} className="mid-row">
                  {moved && (
                    <div className={"delta-pill " + (delta > 0 ? "pos" : "neg")}>
                      {delta > 0 ? "▲" : "▼"} {Math.abs(delta)}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>

        <div className="col col-finish">
          <div className="col-head">
            <div className="col-title">PREDICTED FINISH</div>
            <div className="col-sub">drag drivers to reorder · <button className="link" onClick={resetFinish}>reset</button></div>
          </div>
          <div className="rows">
            {Array.from({ length: 22 }).map((_, i) => {
              const pos = i + 1;
              const d = driverByFinish[pos];
              const isHover = drag && drag.hoverPos === pos;
              return (
                <div key={pos}
                  className={"row finish-row " + (isHover ? "hover " : "")}
                  ref={el => finRefs.current[pos] = el}
                  data-finish-slot={pos}>
                  <div className="pos-cell">
                    <div className="pos-num">{String(pos).padStart(2, "0")}</div>
                    {pos <= 3 && <div className={"pos-medal m" + pos}></div>}
                  </div>
                  {d ? (
                    <DriverChip
                      driver={d} levers={levers}
                      dragging={drag && drag.driverId === d.id}
                      onMouseDown={(e) => onChipMouseDown(e, d.id)}
                      onTogglePanel={() => setOpenPanel(openPanel === d.id ? null : d.id)}
                      panelOpen={openPanel === d.id}
                      deltaTint="auto"
                    />
                  ) : (
                    <div className="empty-slot">drop driver</div>
                  )}
                  {openPanel && d && openPanel === d.id && (
                    <div className="panel-mount">
                      <LeverPanel driver={d} levers={levers}
                        onChange={p => updateLever(d.id, p)}
                        onClose={() => setOpenPanel(null)} />
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>

      </div>

      {/* ============== STEP 2 — RACE INPUTS ============== */}
      <div className="race-inputs">
        <div className="step-marker">
          <span className="step-num">②</span>
          <span className="step-title">ASSIGN RACE INPUTS</span>
          <span className="step-sub">set laps led + fastest lap + DNFs · everything else is auto-inferred from the finish order</span>
        </div>
        <div className="ri-summary-bar">
          <div className="ri-stat">
            <span className="ri-stat-l">LAPS ASSIGNED</span>
            <span className="ri-stat-v">{totalLapsLed} / {RACE.totalLaps}</span>
          </div>
          <div className="ri-laps-progress">
            <div className="ri-laps-bar">
              <div className={"ri-laps-fill " + (totalLapsLed === RACE.totalLaps ? "" : totalLapsLed < RACE.totalLaps ? "under" : "over")}
                style={{ width: Math.min(100, (totalLapsLed / RACE.totalLaps) * 100) + "%" }} />
            </div>
            <span className="ri-laps-target">{totalLapsLed === RACE.totalLaps ? "✓ FULL RACE" : totalLapsLed < RACE.totalLaps ? `${RACE.totalLaps - totalLapsLed} UNASSIGNED` : `+${totalLapsLed - RACE.totalLaps} OVER`}</span>
          </div>
          <div className="ri-stat"><span className="ri-stat-l">FASTEST LAP</span><span className="ri-stat-v">{flDriver ? flDriver.name : "— none —"}</span></div>
          <div className="ri-stat"><span className="ri-stat-l">DNFs</span><span className="ri-stat-v">{dnfCount}</span></div>
          <button className="ri-autofill" onClick={autoFillRaceInputs}
            title={`Distribute ${RACE.totalLaps} race laps across drivers blended 65% from grid position + 35% from predicted finish (empirical 2022-2024 share), and assign fastest lap to the projected winner.`}>
            ⚡ AUTO-FILL FROM FINISH ORDER
          </button>
        </div>
        <div className="ri-grid">
          {finishOrdered.map(d => (
            <RaceInputRow
              key={d.id}
              driver={d} lever={levers[d.id]}
              onChange={p => updateLever(d.id, p)}
              onSetFastestLap={() => setFastestLap(d.id)}
            />
          ))}
        </div>
      </div>

      {/* ============== CONSTRUCTORS ============== */}
      <div className="con-strip">
        <div className="step-marker">
          <span className="step-num">③</span>
          <span className="step-title">CONSTRUCTOR POOL</span>
          <span className="step-sub">bonuses auto-fire from finish order · use lock / exclude to bias the optimizer</span>
        </div>
        <div className="con-grid">
          {CONSTRUCTORS.map(c => {
            const t = TEAMS[c.team];
            const cs = conState[c.team];
            const teamDrvs = DRIVERS.filter(d => d.team === c.team)
              .sort((a, b) => levers[a.id].finish - levers[b.id].finish);
            const allClassified = teamDrvs.every(d => levers[d.id].classified);
            const allTop10 = teamDrvs.every(d => levers[d.id].finish <= 10);
            const allPodium = teamDrvs.every(d => levers[d.id].finish <= 3);
            const pts = constructorPoints(c, levers);
            return (
              <div key={c.team} className={"con-card " + (cs.lock ? "lock " : "") + (cs.exclude ? "exc " : "")}
                style={{ borderTopColor: t.color }}>
                <div className="con-head">
                  <img src={t.logo} alt={t.name} className="con-logo" />
                  <div className="con-name">
                    <div className="cn">{t.short}</div>
                    <div className="cs">{fmt$(c.salary)} · {pts.toFixed(1)} PTS</div>
                  </div>
                </div>
                <div className="con-drivers">
                  {teamDrvs.map(d => {
                    const lev = levers[d.id];
                    const posClass = lev.finish <= 3 ? "podium" : lev.finish <= 10 ? "top10" : "";
                    return (
                      <div key={d.id} className="con-drv">
                        <div className="con-drv-photo" style={{ background: t.color + "22" }}>
                          <img src={d.photo} alt={d.name} draggable={false} />
                        </div>
                        <span className="con-drv-name">{d.name}</span>
                        <span className={"con-drv-pos " + posClass}>P{lev.finish}</span>
                        <span className={"con-drv-status " + (!lev.classified ? "dnf" : "")}>
                          {!lev.classified ? "DNF" : lev.fastestLap ? "FL" : ""}
                        </span>
                      </div>
                    );
                  })}
                </div>
                <div className="con-bonuses-readout">
                  <span className={"bonus-badge " + (allClassified ? "on" : "")} title="Both cars classified +2">BC +2</span>
                  <span className={"bonus-badge " + (allClassified && allTop10 ? "on" : "")} title="Both cars in points (top 10) +5">T10 +5</span>
                  <span className={"bonus-badge " + (allClassified && allPodium ? "on" : "")} title="Both cars on podium +3">POD +3</span>
                </div>
                <div className="con-actions">
                  <button className={"mini " + (cs.lock ? "on" : "")} onClick={() => updateConState(c.team, { lock: !cs.lock, exclude: false })}>LOCK</button>
                  <button className={"mini " + (cs.exclude ? "on" : "")} onClick={() => updateConState(c.team, { exclude: !cs.exclude, lock: false })}>EXC</button>
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* ============== EXECUTE ============== */}
      <div className="exec-bar">
        <div className="exec-stats">
          <div className="es"><div className="es-l">SALARY CAP</div><div className="es-v">
            <input type="number" value={salaryCap} step="500" onChange={e => setSalaryCap(+e.target.value || 50000)} />
          </div></div>
          <div className="es"><div className="es-l">RETURN TOP</div><div className="es-v">
            <input type="number" value={topN} min="1" max="100" onChange={e => setTopN(+e.target.value || 25)} />
          </div></div>
          <div className="es"><div className="es-l">TOTAL Δ POSITIONS</div><div className="es-v num">{totalDelta}</div></div>
        </div>
        <button className="exec-btn" onClick={execute}>
          <span className="exec-arrow">▶</span> EXECUTE OPTIMIZER
        </button>
      </div>
    </div>
  );
}

// ===========================================================
// RACE FORECAST VIEW — full-screen multi-sim outcome visualization
// Combines: ensemble of 6 projection methods (consensus rank + disagreement),
// 15k Monte Carlo distribution (win/podium/top10/DNF probabilities),
// and the user's curated finish prediction (expected DK points).
// ===========================================================
const FORECAST_METHODS = [
  { id: "blend",   short: "BLEND",  full: "Weighted Blend" },
  { id: "qmirror", short: "Q",      full: "Q-Mirror" },
  { id: "qblend",  short: "QB",     full: "Q-Blended" },
  { id: "pl",      short: "PL",     full: "Plackett-Luce" },
  { id: "pace",    short: "PACE",   full: "Pace-based" },
  { id: "mc",      short: "MC",     full: "Monte Carlo" },
];

function ForecastView({ levers, onBack }) {
  const [analysisData, setAnalysisData] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem(ANALYSIS_CACHE_KEY) || "null"); } catch (e) { return null; }
  });
  React.useEffect(() => {
    const handler = () => {
      try { setAnalysisData(JSON.parse(localStorage.getItem(ANALYSIS_CACHE_KEY) || "null")); } catch (e) {}
    };
    window.addEventListener("f1apex:analysisLoaded", handler);
    return () => window.removeEventListener("f1apex:analysisLoaded", handler);
  }, []);

  const forecast = React.useMemo(() => {
    if (!analysisData) return null;
    // Build ranks dict from cached session results
    const rankFor = (arr, key = "position") => {
      const m = {};
      if (!Array.isArray(arr)) return m;
      for (const r of arr) {
        const id = DRIVER_BY_NUM[r.driver_number];
        if (id && r[key] != null) m[id] = r[key];
      }
      return m;
    };
    const qR = rankFor(analysisData.quali);
    for (const d of DRIVERS) if (qR[d.id] == null) qR[d.id] = d.start;
    const sqR = rankFor(analysisData.sq);
    const spR = rankFor(analysisData.sprint);
    const fp1R = rankFor(analysisData.fp1);
    for (const d of DRIVERS) {
      if (sqR[d.id] == null) sqR[d.id] = 22;
      if (spR[d.id] == null) spR[d.id] = 22;
      if (fp1R[d.id] == null) fp1R[d.id] = 22;
    }
    const ranks = { q: qR, sq: sqR, sp: spR, fp: fp1R };
    const lapTimes = {};
    if (Array.isArray(analysisData.fp1)) for (const r of analysisData.fp1) {
      const id = DRIVER_BY_NUM[r.driver_number];
      if (id && r.lap != null) lapTimes[id] = r.lap;
    }
    return _buildForecast({ ranks, lapTimes, levers, drivers: DRIVERS, teams: TEAMS });
  }, [analysisData, levers]);

  if (!forecast) {
    return (
      <div className="forecast-wrap">
        <div className="empty">
          <div className="empty-title">NO SESSION DATA</div>
          <div className="empty-sub">Visit ① GRID & INPUTS first to fetch FP1 / Sprint / Quali sessions from OpenF1.</div>
          <button className="ghost" onClick={onBack}>← BACK TO GRID</button>
        </div>
      </div>
    );
  }

  // Constructor outlook
  const conOutlook = CONSTRUCTORS.map(c => {
    const t = TEAMS[c.team];
    const teamDrvs = DRIVERS.filter(d => d.team === c.team);
    const ensembleAvg = teamDrvs.reduce((s, d) => s + forecast.perDriver[d.id].meanRank, 0) / teamDrvs.length;
    const podiumPct = teamDrvs.reduce((s, d) => s + forecast.perDriver[d.id].mc.podiumPct, 0) / teamDrvs.length;
    const winSum = teamDrvs.reduce((s, d) => s + forecast.perDriver[d.id].mc.winPct, 0);
    return { team: c.team, name: t.short, color: t.color, logo: t.logo, ensembleAvg, podiumPct, winSum };
  }).sort((a, b) => a.ensembleAvg - b.ensembleAvg);

  return (
    <div className="forecast-wrap">
      <div className="fc-hero">
        <div className="fc-hero-left">
          <div className="fc-hero-tag">RACE FORECAST · MULTI-SIM ENSEMBLE</div>
          <div className="fc-hero-title">{RACE.name.toUpperCase()} <span className="fc-hero-year">{RACE.year}</span></div>
          <div className="fc-hero-sub">6 projection models · 15,000 Monte Carlo simulations · {DRIVERS.length} drivers · {RACE.totalLaps} laps</div>
        </div>
        <div className="fc-hero-stats">
          <div className="fc-stat">
            <div className="fc-stat-num">{(forecast.agreement * 100).toFixed(0)}%</div>
            <div className="fc-stat-lbl">ENSEMBLE AGREEMENT</div>
          </div>
          <div className="fc-stat">
            <div className="fc-stat-num">{forecast.mostContested.driver.name.slice(0, 8).toUpperCase()}</div>
            <div className="fc-stat-lbl">MOST CONTESTED · ±{forecast.mostContested.stdRank.toFixed(1)}</div>
          </div>
          <div className="fc-stat">
            <div className="fc-stat-num">{forecast.mostAgreed.driver.name.slice(0, 8).toUpperCase()}</div>
            <div className="fc-stat-lbl">STRONGEST CONSENSUS · ±{forecast.mostAgreed.stdRank.toFixed(1)}</div>
          </div>
        </div>
      </div>

      <div className="fc-cols">
        <div className="fc-left">
          <div className="fc-section-title">PREDICTED FINISH</div>
          <div className="fc-cards">
            {forecast.ranked.map(p => <DriverForecastCard key={p.driver.id} entry={p} />)}
          </div>
        </div>

        <div className="fc-right">
          <div className="fc-section-title">MODEL COMPARISON · WHICH MODEL PUTS EACH DRIVER WHERE</div>
          <div className="fc-matrix-wrap">
            <table className="fc-matrix">
              <thead>
                <tr>
                  <th>DRIVER</th>
                  <th title="Weighted Blend">BLEND</th>
                  <th title={RACE.sessionLabelsShort.quali}>{RACE.sessionLabelsShort.quali}</th>
                  <th title="Q-Blended (ordinal logit)">QB</th>
                  <th title="Plackett-Luce MLE">PL</th>
                  <th title="Pace-based">PACE</th>
                  <th title="Monte Carlo distribution average">MC</th>
                  <th title="Standard deviation across 6 methods">±σ</th>
                </tr>
              </thead>
              <tbody>
                {forecast.ranked.map(p => {
                  const t = TEAMS[p.driver.team];
                  return (
                    <tr key={p.driver.id}>
                      <td className="fcm-drv">
                        <span className="fcm-dot" style={{ background: t.color }}></span>
                        {p.driver.name}
                      </td>
                      {FORECAST_METHODS.map(m => {
                        const r = p.ranksByMethod[m.id];
                        const consensus = Math.round(p.meanRank);
                        const dev = Math.abs(r - consensus);
                        const cls = dev <= 1 ? "ok" : dev <= 3 ? "warn" : "bad";
                        return <td key={m.id} className={"fcm-cell " + cls}>{r}</td>;
                      })}
                      <td className={"fcm-std " + (p.stdRank < 1.5 ? "ok" : p.stdRank < 3 ? "warn" : "bad")}>
                        {p.stdRank.toFixed(1)}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>

          <div className="fc-section-title" style={{ marginTop: 24 }}>CONSTRUCTOR OUTLOOK</div>
          <div className="fc-con-list">
            {conOutlook.map(c => (
              <div key={c.team} className="fc-con-row">
                <div className="fc-con-head">
                  <img src={c.logo} alt={c.name} className="fc-con-logo" />
                  <span className="fc-con-name" style={{ color: c.color }}>{c.name}</span>
                  <span className="fc-con-rank">avg P{c.ensembleAvg.toFixed(1)}</span>
                </div>
                <div className="fc-con-bars">
                  <div className="fc-con-bar" style={{ background: c.color, width: (c.podiumPct * 100) + "%" }}>
                    <span>{(c.podiumPct * 100).toFixed(0)}% pod</span>
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

function DriverForecastCard({ entry }) {
  const { driver, userFinish, meanRank, stdRank, mc, expectedPts, displayRank } = entry;
  const team = TEAMS[driver.team];
  const ensembleDelta = Math.round(meanRank) - userFinish;
  return (
    <div className={"fc-card " + (displayRank === 1 ? "fc-card-winner " : displayRank <= 3 ? "fc-card-podium " : "")}
      style={{ borderLeftColor: team.color }}>
      <div className="fc-card-rank">P{userFinish}</div>
      <div className="fc-card-photo" style={{ background: team.color + "33", boxShadow: `inset 0 -3px 0 ${team.color}` }}>
        <img src={driver.photo} alt={driver.name} draggable={false} />
      </div>
      <div className="fc-card-body">
        <div className="fc-card-name">{driver.name}</div>
        <div className="fc-card-team" style={{ color: team.color }}>{team.short}</div>
        <div className="fc-card-pts">
          <span className="fc-card-pts-num">{expectedPts.toFixed(1)}</span>
          <span className="fc-card-pts-lbl">PROJ DK PTS</span>
        </div>
        <div className="fc-card-bars">
          <ProbBar label="WIN"   value={mc.winPct}    color="var(--lime)" />
          <ProbBar label="POD"   value={mc.podiumPct} color="var(--good)" />
          <ProbBar label="T10"   value={mc.top10Pct}  color="var(--accent)" />
          <ProbBar label="DNF"   value={mc.dnfPct}    color="var(--bad)" inverted />
        </div>
        <div className="fc-card-meta">
          <span>ENSEMBLE P{meanRank.toFixed(1)} ±{stdRank.toFixed(1)}</span>
          {ensembleDelta !== 0 && (
            <span className={ensembleDelta < 0 ? "fc-meta-up" : "fc-meta-down"}>
              {ensembleDelta < 0 ? "▲" : "▼"} {Math.abs(ensembleDelta)} vs your call
            </span>
          )}
        </div>
      </div>
    </div>
  );
}

function ProbBar({ label, value, color, inverted }) {
  const pct = (value * 100).toFixed(value < 0.1 ? 1 : 0);
  return (
    <div className="fc-pb">
      <div className="fc-pb-row">
        <span className="fc-pb-lbl">{label}</span>
        <span className="fc-pb-val">{pct}%</span>
      </div>
      <div className="fc-pb-track">
        <div className="fc-pb-fill" style={{ width: Math.min(100, value * 100) + "%", background: color }}></div>
      </div>
    </div>
  );
}

// ===========================================================
// RESULTS VIEW
// ===========================================================
function ResultsView({ results, running, salaryCap, onBack }) {
  const [resView, setResView] = React.useState("table");
  const [sortKey, setSortKey] = React.useState("pts");
  const [sortDir, setSortDir] = React.useState("desc");
  const [pSortKey, setPSortKey] = React.useState("count");
  const [pSortDir, setPSortDir] = React.useState("desc");
  const [cSortKey, setCSortKey] = React.useState("count");
  const [cSortDir, setCSortDir] = React.useState("desc");

  const rows = React.useMemo(() => {
    if (!results || !results.kept) return [];
    return results.kept.map((lu, i) => {
      const cptDriver = DRIVERS.find(d => d.id === lu.captain.id);
      const flexAll = [...lu.lockedFlex, ...lu.flex];
      const conTeam = TEAMS[lu.constructor.team];
      const value = lu.totalSalary > 0 ? (lu.totalSalary / lu.totalPts) : 0;
      const ptsPer1k = lu.totalSalary > 0 ? (lu.totalPts / (lu.totalSalary / 1000)) : 0;
      const leftover = salaryCap - lu.totalSalary;
      return {
        idx: i, rank: i + 1, lu,
        captain: cptDriver.name, captainTeam: cptDriver.team,
        flex: flexAll.map(f => DRIVERS.find(d => d.id === f.id).name).join(" · "),
        constructor: conTeam.short,
        salary: lu.totalSalary, pts: lu.totalPts,
        value, ptsPer1k, leftover,
      };
    });
  }, [results, salaryCap]);

  const sortedRows = React.useMemo(() => {
    const arr = [...rows];
    arr.sort((a, b) => {
      let av = a[sortKey], bv = b[sortKey];
      if (typeof av === "string") { av = av.toLowerCase(); bv = bv.toLowerCase(); }
      if (av < bv) return sortDir === "asc" ? -1 : 1;
      if (av > bv) return sortDir === "asc" ? 1 : -1;
      return 0;
    });
    return arr;
  }, [rows, sortKey, sortDir]);

  const playerStats = React.useMemo(() => {
    if (!results || !results.kept) return [];
    const m = new Map();
    for (const d of DRIVERS) m.set(d.id, { driver: d, count: 0, cptCount: 0, sumPts: 0, sumSal: 0, raw: null });
    results.kept.forEach(lu => {
      const cptId = lu.captain.id;
      const cptObj = m.get(cptId);
      cptObj.count++; cptObj.cptCount++;
      cptObj.sumPts += lu.captain.pts * SCORING.captainMult;
      cptObj.sumSal += lu.captain.salary * SCORING.captainMult;
      cptObj.raw = lu.captain;
      [...lu.lockedFlex, ...lu.flex].forEach(f => {
        const o = m.get(f.id);
        o.count++; o.sumPts += f.pts; o.sumSal += f.salary; o.raw = f;
      });
    });
    const total = results.kept.length;
    return [...m.values()]
      .filter(o => o.count > 0)
      .map(o => ({
        id: o.driver.id, name: o.driver.name, team: o.driver.team, salary: o.driver.salary,
        pts: o.raw ? o.raw.pts : 0,
        count: o.count, exposure: o.count / total,
        cptCount: o.cptCount, cptExposure: o.cptCount / total,
        avgPts: o.sumPts / o.count,
        ptsPer1k: o.driver.salary > 0 ? (o.raw.pts / (o.driver.salary / 1000)) : 0,
      }));
  }, [results]);

  const conStats = React.useMemo(() => {
    if (!results || !results.kept) return [];
    const m = new Map();
    for (const c of CONSTRUCTORS) m.set(c.team, { team: c.team, salary: c.salary, count: 0, sumPts: 0, raw: null });
    results.kept.forEach(lu => {
      const o = m.get(lu.constructor.team);
      o.count++; o.sumPts += lu.constructor.pts; o.raw = lu.constructor;
    });
    const total = results.kept.length;
    return [...m.values()]
      .filter(o => o.count > 0)
      .map(o => ({
        team: o.team, name: TEAMS[o.team].short, color: TEAMS[o.team].color, logo: TEAMS[o.team].logo,
        salary: o.salary, pts: o.raw ? o.raw.pts : 0,
        count: o.count, exposure: o.count / total,
        ptsPer1k: o.salary > 0 ? ((o.raw ? o.raw.pts : 0) / (o.salary / 1000)) : 0,
      }));
  }, [results]);

  const sortBy = (arr, k, dir) => {
    const a = [...arr];
    a.sort((x, y) => {
      let av = x[k], bv = y[k];
      if (typeof av === "string") { av = av.toLowerCase(); bv = bv.toLowerCase(); }
      if (av < bv) return dir === "asc" ? -1 : 1;
      if (av > bv) return dir === "asc" ? 1 : -1;
      return 0;
    });
    return a;
  };
  const sortedPlayers = React.useMemo(() => sortBy(playerStats, pSortKey, pSortDir), [playerStats, pSortKey, pSortDir]);
  const sortedCons = React.useMemo(() => sortBy(conStats, cSortKey, cSortDir), [conStats, cSortKey, cSortDir]);

  if (running) {
    return (
      <div className="results-wrap">
        <div className="loader">
          <div className="loader-spin"></div>
          <div className="loader-text">SOLVING LINEUP COMBINATIONS…</div>
          <div className="loader-sub">enumerating captain × constructor × flex permutations</div>
        </div>
      </div>
    );
  }
  if (!results || !results.kept || results.kept.length === 0) {
    return (
      <div className="results-wrap">
        <div className="empty">
          <div className="empty-title">NO VALID LINEUPS</div>
          <div className="empty-sub">{results?.error || "Check salary cap, locks/excludes, and team-rule conflicts."}</div>
          <button className="ghost" onClick={onBack}>← BACK TO GRID</button>
        </div>
      </div>
    );
  }

  const onSort = (k) => {
    if (sortKey === k) setSortDir(d => d === "asc" ? "desc" : "asc");
    else { setSortKey(k); setSortDir(k === "value" ? "asc" : "desc"); }
  };
  const toggleSort = (cur, dir, setK, setD, k, defaultDir = "desc") => {
    if (cur === k) setD(dir === "asc" ? "desc" : "asc");
    else { setK(k); setD(defaultDir); }
  };
  const SortHead = ({ k, children, align }) => (
    <th onClick={() => onSort(k)} className={"sortable " + (align === "right" ? "r" : "")}>
      <span>{children}</span>
      <span className="sort-ind">{sortKey === k ? (sortDir === "asc" ? "▲" : "▼") : ""}</span>
    </th>
  );

  return (
    <div className="results-wrap">
      <div className="results-head">
        <button className="ghost" onClick={onBack}>← BACK TO GRID</button>
        <div className="view-toggle">
          <button className={resView === "table" ? "on" : ""} onClick={() => setResView("table")}>LINEUPS</button>
          <button className={resView === "players" ? "on" : ""} onClick={() => setResView("players")}>DRIVERS</button>
          <button className={resView === "cons" ? "on" : ""} onClick={() => setResView("cons")}>CONSTRUCTORS</button>
          <button className={resView === "cards" ? "on" : ""} onClick={() => setResView("cards")}>CARDS</button>
        </div>
        <div className="results-stats">
          <div><span className="muted">EVALUATED</span> <strong>{results.evaluated.toLocaleString()}</strong></div>
          <div><span className="muted">RETURNED</span> <strong>{results.kept.length}</strong></div>
          <div><span className="muted">CAP</span> <strong>{fmt$(salaryCap)}</strong></div>
        </div>
      </div>

      {resView === "table" ? (
        <div className="lu-table-wrap">
          <table className="lu-table">
            <thead>
              <tr>
                <SortHead k="rank">#</SortHead>
                <SortHead k="captain">CAPTAIN</SortHead>
                <th>FLEX (4)</th>
                <SortHead k="constructor">CONSTRUCTOR</SortHead>
                <SortHead k="salary" align="right">SALARY</SortHead>
                <SortHead k="leftover" align="right">LEFT</SortHead>
                <SortHead k="pts" align="right">PROJ PTS</SortHead>
                <SortHead k="ptsPer1k" align="right">PTS/$1K</SortHead>
                <SortHead k="value" align="right">$/PT</SortHead>
              </tr>
            </thead>
            <tbody>
              {sortedRows.map(r => {
                const cptT = TEAMS[r.captainTeam];
                return (
                  <tr key={r.idx} className={r.rank === 1 ? "row-1" : r.rank === 2 ? "row-2" : r.rank === 3 ? "row-3" : ""}>
                    <td className="r-rank">{String(r.rank).padStart(2, "0")}</td>
                    <td className="r-cpt">
                      <span className="r-dot" style={{ background: cptT.color }}></span>
                      {r.captain}
                    </td>
                    <td className="r-flex">{r.flex}</td>
                    <td className="r-con">{r.constructor}</td>
                    <td className="r right">{fmt$(r.salary)}</td>
                    <td className={"r right " + (r.leftover < 0 ? "bad" : "muted")}>{fmt$(r.leftover)}</td>
                    <td className="r right hi">{r.pts.toFixed(1)}</td>
                    <td className="r right">{r.ptsPer1k.toFixed(2)}</td>
                    <td className="r right">${r.value.toFixed(0)}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      ) : resView === "players" ? (
        <div className="lu-table-wrap">
          <table className="lu-table">
            <thead>
              <tr>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "name", "asc")} className="sortable">DRIVER <span className="sort-ind">{pSortKey==="name"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th>TEAM</th>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "salary")} className="sortable r">SALARY <span className="sort-ind">{pSortKey==="salary"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "pts")} className="sortable r">PROJ PTS <span className="sort-ind">{pSortKey==="pts"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "ptsPer1k")} className="sortable r">PTS/$1K <span className="sort-ind">{pSortKey==="ptsPer1k"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "count")} className="sortable r">LINEUPS <span className="sort-ind">{pSortKey==="count"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "exposure")} className="sortable r">EXPOSURE <span className="sort-ind">{pSortKey==="exposure"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(pSortKey, pSortDir, setPSortKey, setPSortDir, "cptCount")} className="sortable r">CAPT × <span className="sort-ind">{pSortKey==="cptCount"?(pSortDir==="asc"?"▲":"▼"):""}</span></th>
              </tr>
            </thead>
            <tbody>
              {sortedPlayers.map(p => {
                const t = TEAMS[p.team];
                return (
                  <tr key={p.id}>
                    <td className="r-cpt"><span className="r-dot" style={{ background: t.color }}></span>{p.name}</td>
                    <td className="r-con">{t.short}</td>
                    <td className="r right">{fmt$(p.salary)}</td>
                    <td className="r right hi">{p.pts.toFixed(1)}</td>
                    <td className="r right">{p.ptsPer1k.toFixed(2)}</td>
                    <td className="r right">{p.count}</td>
                    <td className="r right">
                      <div className="exp-bar"><div className="exp-fill" style={{ width: (p.exposure*100)+"%", background: t.color }}></div><span>{(p.exposure*100).toFixed(0)}%</span></div>
                    </td>
                    <td className="r right">{p.cptCount > 0 ? p.cptCount : <span className="muted">—</span>}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      ) : resView === "cons" ? (
        <div className="lu-table-wrap">
          <table className="lu-table">
            <thead>
              <tr>
                <th onClick={() => toggleSort(cSortKey, cSortDir, setCSortKey, setCSortDir, "name", "asc")} className="sortable">CONSTRUCTOR <span className="sort-ind">{cSortKey==="name"?(cSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(cSortKey, cSortDir, setCSortKey, setCSortDir, "salary")} className="sortable r">SALARY <span className="sort-ind">{cSortKey==="salary"?(cSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(cSortKey, cSortDir, setCSortKey, setCSortDir, "pts")} className="sortable r">PROJ PTS <span className="sort-ind">{cSortKey==="pts"?(cSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(cSortKey, cSortDir, setCSortKey, setCSortDir, "ptsPer1k")} className="sortable r">PTS/$1K <span className="sort-ind">{cSortKey==="ptsPer1k"?(cSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(cSortKey, cSortDir, setCSortKey, setCSortDir, "count")} className="sortable r">LINEUPS <span className="sort-ind">{cSortKey==="count"?(cSortDir==="asc"?"▲":"▼"):""}</span></th>
                <th onClick={() => toggleSort(cSortKey, cSortDir, setCSortKey, setCSortDir, "exposure")} className="sortable r">EXPOSURE <span className="sort-ind">{cSortKey==="exposure"?(cSortDir==="asc"?"▲":"▼"):""}</span></th>
              </tr>
            </thead>
            <tbody>
              {sortedCons.map(c => (
                <tr key={c.team}>
                  <td className="r-cpt"><span className="r-dot" style={{ background: c.color }}></span>{c.name}</td>
                  <td className="r right">{fmt$(c.salary)}</td>
                  <td className="r right hi">{c.pts.toFixed(1)}</td>
                  <td className="r right">{c.ptsPer1k.toFixed(2)}</td>
                  <td className="r right">{c.count}</td>
                  <td className="r right">
                    <div className="exp-bar"><div className="exp-fill" style={{ width: (c.exposure*100)+"%", background: c.color }}></div><span>{(c.exposure*100).toFixed(0)}%</span></div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : (
        <div className="lineups">
          {results.kept.map((lu, i) => <LineupCard key={i} lineup={lu} rank={i + 1} salaryCap={salaryCap} />)}
        </div>
      )}
    </div>
  );
}

function LineupCard({ lineup, rank, salaryCap }) {
  const cpt = lineup.captain;
  const cptDriver = DRIVERS.find(d => d.id === cpt.id);
  const flexDrivers = [...lineup.lockedFlex, ...lineup.flex].map(f => DRIVERS.find(d => d.id === f.id));
  const con = lineup.constructor;
  const conTeam = TEAMS[con.team];
  const salaryPct = lineup.totalSalary / salaryCap;

  return (
    <div className={"lu-card " + (rank === 1 ? "podium-1 " : rank === 2 ? "podium-2 " : rank === 3 ? "podium-3 " : "")}>
      <div className="lu-rank">
        <div className="rk-num">{String(rank).padStart(2, "0")}</div>
        <div className="rk-lbl">RANK</div>
      </div>
      <div className="lu-mid">
        <div className="lu-slots">
          <SlotCard kind="CPT" driver={cptDriver} salary={cpt.salary * 1.5} pts={cpt.pts * 1.5} />
          {flexDrivers.map(d => {
            const f = [...lineup.flex, ...lineup.lockedFlex].find(x => x.id === d.id);
            return <SlotCard key={d.id} kind="DRV" driver={d} salary={f.salary} pts={f.pts} />;
          })}
          <SlotCard kind="CON" team={conTeam} salary={con.salary} pts={con.pts} />
        </div>
        <div className="lu-bar">
          <div className="lu-bar-fill" style={{ width: (salaryPct * 100) + "%" }}></div>
          <div className="lu-bar-label">{fmt$(lineup.totalSalary)} / {fmt$(salaryCap)} <span className="muted">· {(salaryPct * 100).toFixed(1)}%</span></div>
        </div>
      </div>
      <div className="lu-pts">
        <div className="pt-num">{lineup.totalPts.toFixed(1)}</div>
        <div className="pt-lbl">PROJ PTS</div>
      </div>
    </div>
  );
}

function SlotCard({ kind, driver, team, salary, pts }) {
  if (kind === "CON") {
    return (
      <div className="slot slot-con" style={{ borderTopColor: team.color }}>
        <div className="slot-tag">CONSTRUCTOR</div>
        <img src={team.logo} className="slot-logo" />
        <div className="slot-name">{team.short}</div>
        <div className="slot-meta"><span>{fmt$(salary)}</span><span className="pts">{pts.toFixed(1)}</span></div>
      </div>
    );
  }
  const t = TEAMS[driver.team];
  const isCpt = kind === "CPT";
  return (
    <div className={"slot " + (isCpt ? "slot-cpt" : "slot-drv")} style={{ borderTopColor: t.color }}>
      <div className="slot-tag">{isCpt ? "CAPTAIN · 1.5×" : "DRIVER"}</div>
      <div className="slot-photo" style={{ background: t.color + "22" }}>
        <img src={driver.photo} alt={driver.name} />
      </div>
      <div className="slot-name">{driver.name}</div>
      <div className="slot-team" style={{ color: t.color }}>{t.short}</div>
      <div className="slot-meta"><span>{fmt$(salary)}</span><span className="pts">{pts.toFixed(1)}</span></div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
})();
