// ur2d-archive.jsx — ALL-TIME ARCHIVE: final standings for every finished season, with
// champions, driver careers and team histories DERIVED from them (never typed in).
// Load order: shared → store → roster → ARCHIVE → home → pages …
//
// Facts: each season's final driver and team standings, read from The UL Mega Table
// (see MEGATABLE.md), fetched live as CSV by gid. gviz is not used here because it nulls
// columns that mix text and numbers.
//   Driver standings: "All results data" (gid 0). One block per season; row order = final
//     position; cols 3 Pts, 24 Rs, 25 Pds, 26 FL, 27 PP, 28 W; col 32 = sheet's WDC flag.
//   Team standings: "All team results data" (gid 564964483). Same shape; cols 3 Pts, 14 Rs,
//     15 Pds, 16 FL, 17 PP, 18 W; col 19 = sheet's WCC flag.
//   Driver→team per round: "All results organized" × "All team results organized". The
//     season's team = the one the driver raced for in the most rounds.
//   Countries: "Countries + ELO Help" (driver → country, country → flag emoji → ISO code).
//   Team full names: "Team info page" cols 25/26.
// Derived: champion = P1 of a FINISHED season (number < the league's current season).
// If the archive's team block is unfinished (S25 team data stops at R6), that season's
// team standings come from its own league workbook (store liveSource Leaderboard).
// Identity: every row is linked to a PERSON (see _arcPersonKey), never to a raw sheet
// string, so all of a driver's seasons hang off one record. Rows themselves stay display-raw.

const UR_ARCHIVE_SOURCES = {
  ul: {
    sheetId: '15fiK0Nic45xvlC0iMc7JilDBYq1KN1Vuo-184SSMp7c',
    gids: { drivers: 0, teams: 564964483, results: 1160425687, teamResults: 1568775495, countries: 954349138, teamInfo: 42828034 },
  },
};
const UR_ARCHIVE_GREY = '#8A9099';

function _arcCsv(t) {
  const R = []; let row = [], f = '', q = false;
  for (let i = 0; i < t.length; i++) {
    const ch = t[i];
    if (q) { if (ch === '"') { if (t[i + 1] === '"') { f += '"'; i++; } else q = false; } else f += ch; }
    else if (ch === '"') q = true;
    else if (ch === ',') { row.push(f); f = ''; }
    else if (ch === '\n') { row.push(f); R.push(row); row = []; f = ''; }
    else if (ch !== '\r') f += ch;
  }
  if (f !== '' || row.length) { row.push(f); R.push(row); }
  return R;
}
async function _arcFetch(sheetId, gid) {
  const res = await fetch(`https://docs.google.com/spreadsheets/d/${sheetId}/export?format=csv&gid=${gid}`);
  if (!res.ok) throw new Error(`archive csv HTTP ${res.status} (gid ${gid})`);
  return _arcCsv(await res.text());
}
// European number format: "231,5" → 231.5, "1.234" → 1234. Blank / "-" → 0.
const _arcNum = (s) => {
  s = String(s == null ? '' : s).trim();
  if (!s || s === '-') return 0;
  const n = parseFloat(s.replace(/\./g, '').replace(',', '.'));
  return isNaN(n) ? 0 : n;
};
const _arcCell = (r, i) => String((r && r[i]) || '').trim();
const _arcNorm = (s) => (typeof rosterNorm === 'function' ? rosterNorm(s) : String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''));
// '🇧🇷' → 'br'; tag-sequence flags (England 🏴…) → 'gb-eng'.
function _arcFlagIso(emoji) {
  if (!emoji) return null;
  const cps = [...emoji].map((c) => c.codePointAt(0));
  const ri = cps.filter((c) => c >= 0x1F1E6 && c <= 0x1F1FF);
  if (ri.length === 2) return ri.map((c) => String.fromCharCode(c - 0x1F1E6 + 97)).join('');
  const tag = cps.filter((c) => c >= 0xE0061 && c <= 0xE007A).map((c) => String.fromCharCode(c - 0xE0000)).join('');
  return tag.length >= 4 ? tag.slice(0, 2) + '-' + tag.slice(2) : null;
}
// Rows grouped by the "Season N" marker in col 0 (the marker sits on the block's first data row).
function _arcBlocks(rows, nameCol) {
  const out = {}; let n = 0;
  rows.forEach((r) => {
    const m = /^Season (\d+)/.exec(_arcCell(r, 0));
    if (m) n = +m[1];
    if (n && _arcCell(r, nameCol)) (out[n] = out[n] || []).push(r);
  });
  return out;
}
const _arcRound = (s) => { const m = /^S(\d+)\s*R(\d+)$/.exec(String(s || '').trim()); return m ? [+m[1], +m[2]] : null; };

// Team identity: full name from the sheet's team list, else any league season's teamNames;
// colour from the league seasons' teamColors (newest first), else neutral grey.
// Colours for archived teams no current season lists (sampled from their liveries).
const UR_ARCHIVE_TEAM_COLORS = { FGR: '#D3B312', NSR: '#7603BF' }; // FGR gold (UL S8 car) · NSR Neon Sunset purple (UL S16/S17 cars)
function _arcTeamIdentity(league, sheetNames) {
  const names = {}, colors = {};
  ((UR2D_ADAPTERS[league] || {}).seasons || []).forEach((s) => {
    const src = s.liveSource || {};
    Object.entries(src.teamNames || {}).forEach(([k, v]) => { if (!names[k]) names[k] = v; });
    Object.entries(src.teamColors || {}).forEach(([k, v]) => { if (!colors[k]) colors[k] = v; });
  });
  return (short) => short ? { short, name: sheetNames[short] || names[short] || short, color: colors[short] || UR_ARCHIVE_TEAM_COLORS[short] || UR_ARCHIVE_GREY } : null;
}

function _arcBuild(league, raw) {
  const adapter = UR2D_ADAPTERS[league] || { seasons: [] };
  const cur = adapter.seasons.find((s) => s.isCurrent);
  const currentNumber = cur ? cur.number : Infinity;

  const emoji = {}, countryOf = {};
  raw.countries.slice(1).forEach((r) => { if (_arcCell(r, 4)) emoji[_arcCell(r, 4)] = _arcCell(r, 5); });
  raw.countries.slice(1).forEach((r) => { if (_arcCell(r, 0)) countryOf[_arcCell(r, 0)] = _arcFlagIso(emoji[_arcCell(r, 1)]); });
  const sheetTeamNames = {};
  raw.teamInfo.forEach((r) => { const k = _arcCell(r, 25); if (k && k !== 'Shortcode' && _arcCell(r, 26)) sheetTeamNames[k] = _arcCell(r, 26); });
  const team = _arcTeamIdentity(league, sheetTeamNames);

  // Per season: rounds raced + who drove for which team how often.
  const teamRows = {};
  raw.teamResults.forEach((r) => { const k = _arcRound(r[0]); if (k) teamRows[k.join('|')] = r; });
  const roundsRaced = {}, votes = {};
  raw.results.forEach((r) => {
    const k = _arcRound(r[0]);
    if (!k || !_arcCell(r, 2)) return;
    const [s] = k;
    roundsRaced[s] = (roundsRaced[s] || 0) + 1;
    const t = teamRows[k.join('|')];
    if (!t) return;
    for (let j = 2; j <= 21; j++) {
      const nm = _arcCell(r, j), tm = _arcCell(t, j);
      if (!nm || !tm) continue;
      const v = ((votes[s] = votes[s] || {})[nm] = votes[s][nm] || {});
      v[tm] = (v[tm] || 0) + 1;
    }
  });
  const seasonTeam = (s, nm) => { const v = (votes[s] || {})[nm]; return v ? Object.entries(v).sort((a, b) => b[1] - a[1])[0][0] : null; };

  const dBlocks = _arcBlocks(raw.drivers, 2), tBlocks = _arcBlocks(raw.teams, 2);
  const seasons = {};
  Object.keys(dBlocks).map(Number).sort((a, b) => a - b).forEach((n) => {
    const drivers = dBlocks[n].map((r, i) => {
      const name = _arcCell(r, 2);
      return {
        pos: i + 1, name, country: countryOf[name] || null, team: seasonTeam(n, name),
        pts: _arcNum(r[3]), starts: _arcNum(r[24]), podiums: _arcNum(r[25]), fastestLaps: _arcNum(r[26]),
        poles: _arcNum(r[27]), wins: _arcNum(r[28]), sheetChampion: _arcCell(r, 32) === '1',
      };
    });
    const tb = tBlocks[n] || [];
    let teamRounds = 0;
    for (let c = 4; c <= 13; c++) if (tb.some((r) => _arcNum(r[c]) !== 0)) teamRounds++;
    const teams = tb.map((r, i) => ({
      pos: i + 1, short: _arcCell(r, 2), pts: _arcNum(r[3]), starts: _arcNum(r[14]), podiums: _arcNum(r[15]),
      fastestLaps: _arcNum(r[16]), poles: _arcNum(r[17]), wins: _arcNum(r[18]), sheetChampion: _arcCell(r, 19) === '1',
    }));
    seasons[n] = {
      number: n, label: 'S' + n, rounds: roundsRaced[n] || 0, finished: n < currentNumber,
      drivers, teams, teamsComplete: tb.length > 0 && teamRounds >= (roundsRaced[n] || 0), teamsSource: 'archive',
    };
  });
  return { league, seasons, countryOf, team };
}

// Unfinished archive team blocks → that season's own league workbook, when the store has one.
async function _arcPatchTeams(league, data) {
  const adapter = UR2D_ADAPTERS[league];
  if (!adapter) return;
  await Promise.all(Object.values(data.seasons).filter((s) => !s.teamsComplete).map(async (s) => {
    const def = adapter.seasons.find((d) => d.label === s.label && d.liveSource);
    if (!def || typeof loadLeaderboardStandings !== 'function') return;
    try {
      const st = await loadLeaderboardStandings(def.liveSource);
      if (!st || !st.teams || !st.teams.length) return;
      s.teams = [...st.teams].sort((a, b) => b.pts - a.pts).map((t, i) => ({
        pos: i + 1, short: t.short, pts: t.pts, starts: null, podiums: t.podiums ?? null,
        fastestLaps: null, poles: null, wins: t.wins ?? null, sheetChampion: false,
      }));
      s.teamsComplete = true;
      s.teamsSource = 'league-sheet';
    } catch (e) { /* keep the archive block, still flagged incomplete */ }
  }));
}

// Indexes + champions, computed once per load.
function _arcDerive(data) {
  const people = {}, byTeam = {}, champions = [];
  const driverTitles = {}, teamTitles = {};
  const list = Object.values(data.seasons).sort((a, b) => a.number - b.number);
  list.forEach((s) => {
    const wdc = s.finished ? s.drivers[0] || null : null;
    const wcc = s.finished && s.teamsComplete ? s.teams[0] || null : null;
    const wdcKey = wdc ? _arcPersonKey(wdc.name) : null;
    if (wdc) driverTitles[wdcKey] = (driverTitles[wdcKey] || 0) + 1;
    if (wcc) teamTitles[wcc.short] = (teamTitles[wcc.short] || 0) + 1;
    if (wdc && !wdc.sheetChampion) console.warn(`[archive] ${s.label}: P1 ${wdc.name} is not the sheet's WDC-flagged row`);
    if (wcc && s.teamsSource === 'archive' && !wcc.sheetChampion) console.warn(`[archive] ${s.label}: P1 team ${wcc.short} is not the sheet's WCC-flagged row`);
    s.drivers.forEach((d) => {
      const key = _arcPersonKey(d.name);
      const p = people[key] || (people[key] = { key, handle: key.startsWith('h:') ? key.slice(2) : null, names: [], rows: [] });
      if (!p.names.includes(d.name)) p.names.push(d.name);
      p.rows.push({ season: s.label, number: s.number, finished: s.finished, ...d, champion: d === wdc });
    });
    s.teams.forEach((t) => {
      (byTeam[t.short] = byTeam[t.short] || []).push({ season: s.label, number: s.number, finished: s.finished, teamsSource: s.teamsSource, ...t, champion: t === wcc });
    });
    if (wdc) champions.push({ s, wdc, wdcKey, wcc });
  });
  data.people = people; data.byTeam = byTeam;
  data.driverTitles = driverTitles; data.teamTitles = teamTitles;
  const years = {};
  (typeof getCalendarSeasons === 'function' ? getCalendarSeasons() : []).forEach((c) => { years[c.label] = c.year || null; });
  data.champions = champions.reverse().map(({ s, wdc, wdcKey, wcc }) => ({
    number: s.number, season: s.label, year: years[s.label] || null,
    wdc: { ...wdc, team: data.team(wdc.team) || { short: '—', name: '—', color: UR_ARCHIVE_GREY }, titles: driverTitles[wdcKey], person: wdcKey },
    wcc: wcc ? { ...data.team(wcc.short), pts: wcc.pts, wins: wcc.wins, podiums: wcc.podiums, titles: teamTitles[wcc.short], source: s.teamsSource } : null,
  }));
  return data;
}

// PRE-BAKED SNAPSHOT — data/<league>-archive.js sets window.UR_ARCHIVE_SNAPSHOT[league]. Finished seasons never
// change, so the Hall of Champions (and careers) hydrate from it instantly at load with no network.
// Regenerate after a season ends: open the site, run `await loadArchive('ul', { live: true })`, then
// `UR_ARCHIVE_EXPORT('ul')` → JSON for data/ul-archive.js.
function _arcHydrate(league, snap) {
  const adapter = UR2D_ADAPTERS[league] || { seasons: [] };
  const cur = adapter.seasons.find((s) => s.isCurrent);
  const currentNumber = cur ? cur.number : Infinity;
  const seasons = {};
  snap.seasons.forEach((s) => {
    seasons[s.number] = { ...s, drivers: s.drivers.map((d) => ({ ...d })), teams: s.teams.map((t) => ({ ...t })), finished: s.number < currentNumber };
  });
  return _arcDerive({ league, seasons, countryOf: { ...(snap.countryOf || {}) }, team: _arcTeamIdentity(league, snap.teamNames || {}) });
}
function UR_ARCHIVE_EXPORT(league) {
  const e = _archiveCache[league];
  if (!e || !e.data) return null;
  const d = e.data, codes = new Set(), teamNames = {};
  Object.values(d.seasons).forEach((s) => { s.teams.forEach((t) => codes.add(t.short)); s.drivers.forEach((x) => { if (x.team) codes.add(x.team); }); });
  codes.forEach((c) => { const t = d.team(c); if (t && t.name && t.name !== c) teamNames[c] = t.name; });
  return {
    schema: 'ur2d-archive/1', league, builtAt: new Date().toISOString(), source: UR_ARCHIVE_SOURCES[league].sheetId,
    seasons: Object.values(d.seasons).sort((a, b) => a.number - b.number).map(({ number, label, rounds, drivers, teams, teamsComplete, teamsSource }) => ({ number, label, rounds, drivers, teams, teamsComplete, teamsSource })),
    countryOf: d.countryOf, teamNames,
  };
}

const _archiveCache = {}; // league → { status, data, error, promise }
function loadArchive(league, opts) {
  const src = UR_ARCHIVE_SOURCES[league];
  if (!src) return Promise.resolve(null);
  const live = !!(opts && opts.live);
  const c = _archiveCache[league];
  if (c && c.promise && !(live && c.fromSnapshot)) return c.promise;
  const snap = !live && window.UR_ARCHIVE_SNAPSHOT && window.UR_ARCHIVE_SNAPSHOT[league];
  if (snap) {
    try {
      const data = _arcHydrate(league, snap);
      _archiveCache[league] = { status: 'ready', data, error: null, fromSnapshot: true, promise: Promise.resolve(data) };
      return _archiveCache[league].promise;
    } catch (e) { console.warn('[archive] snapshot hydrate failed, reading live:', e); }
  }
  const entry = { status: 'loading', data: null, error: null };
  entry.promise = (async () => {
    try {
      const keys = Object.keys(src.gids);
      const tabs = await Promise.all(keys.map((k) => _arcFetch(src.sheetId, src.gids[k])));
      const raw = Object.fromEntries(keys.map((k, i) => [k, tabs[i]]));
      const data = _arcBuild(league, raw);
      await _arcPatchTeams(league, data);
      entry.data = _arcDerive(data);
      entry.status = 'ready';
    } catch (e) {
      entry.status = 'error';
      entry.error = String(e && e.message || e);
      entry.promise = null; // allow a retry on next use
    }
    return entry.data;
  })();
  _archiveCache[league] = entry;
  return entry.promise;
}
function useArchive(league) {
  const [, force] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => {
    let alive = true;
    loadArchive(league).then(() => { if (alive) force(); });
    return () => { alive = false; };
  }, [league]);
  if (!UR_ARCHIVE_SOURCES[league]) return { status: 'empty', data: null, error: null };
  const e = _archiveCache[league];
  return e ? { status: e.status, data: e.data, error: e.error } : { status: 'loading', data: null, error: null };
}

// Person key for a sheet name. Roster people (resolvePerson checks handle, sheetName, display,
// aliases) → 'h:<handle>'. Anyone the roster doesn't know (most pre-S24 drivers) →
// 'n:<normalized name>', so case/spacing variants of one name (Nomscar / nomscar) are one person.
function _arcPersonKey(name) {
  const p = typeof resolvePerson === 'function' ? resolvePerson(name) : null;
  return p ? 'h:' + p.handle : 'n:' + _arcNorm(name);
}
// Any name form → person key present in this archive (or null).
function _arcResolveKey(data, anyName) {
  if (!anyName) return null;
  const k = _arcPersonKey(anyName);
  if (data.people[k]) return k;
  const p = typeof resolvePerson === 'function' ? resolvePerson(anyName) : null;
  for (const c of p ? [p.sheetName, ...(p.aliases || []), p.display] : []) {
    const k2 = c && 'n:' + _arcNorm(c);
    if (k2 && data.people[k2]) return k2;
  }
  return null;
}
const _arcSum = (rows, k) => rows.reduce((t, r) => t + (r[k] || 0), 0);

// ─── PUBLIC API ──────────────────────────────────────────────────────────────
// getChampions(league) → { status, seasons (newest first), driverTitles, teamTitles }
//   season = { number, season:'S25', year|null, wdc:{ name, country, team:{short,name,color},
//   pts, starts, wins, podiums, poles, fastestLaps, titles }, wcc:{ short, name, color, pts,
//   wins, podiums, titles, source } | null }
function getChampions(league) {
  const e = _archiveCache[league];
  if (!UR_ARCHIVE_SOURCES[league]) return { status: 'empty', seasons: [], driverTitles: {}, teamTitles: {} };
  if (!e || !e.data) return { status: e ? e.status : 'loading', seasons: [], driverTitles: {}, teamTitles: {} };
  return { status: 'ready', seasons: e.data.champions, driverTitles: e.data.driverTitles, teamTitles: e.data.teamTitles };
}
function useChampions(league) { useArchive(league); return getChampions(league); }

// getDriverCareer(league, anyName) → null | { key, handle|null, name (latest sheet name),
//   names (every sheet spelling seen), display, country, champion, titles:['S16',…],
//   seasons:[{ season, number, finished, name, pos, pts, starts, wins, podiums, poles,
//   fastestLaps, team, champion }], totals:{ seasons, starts, wins, podiums, poles, fastestLaps, pts, titles } }
function getDriverCareer(league, anyName) {
  const e = _archiveCache[league];
  if (!e || !e.data) return null;
  const key = _arcResolveKey(e.data, anyName);
  if (!key) return null;
  const p = e.data.people[key];
  const rows = p.rows;
  const titles = rows.filter((r) => r.champion).map((r) => r.season);
  const name = rows[rows.length - 1].name;
  const person = p.handle && typeof resolvePerson === 'function' ? resolvePerson(p.handle) : null;
  return {
    key, handle: p.handle, name, names: p.names, display: person ? person.display : name,
    country: e.data.countryOf[name] || p.names.map((n) => e.data.countryOf[n]).find(Boolean) || null,
    champion: titles.length > 0, titles, seasons: rows,
    totals: { seasons: new Set(rows.map((r) => r.season)).size, starts: _arcSum(rows, 'starts'), wins: _arcSum(rows, 'wins'), podiums: _arcSum(rows, 'podiums'),
      poles: _arcSum(rows, 'poles'), fastestLaps: _arcSum(rows, 'fastestLaps'), pts: _arcSum(rows, 'pts'), titles: titles.length },
  };
}
function useDriverCareer(league, anyName) { useArchive(league); return getDriverCareer(league, anyName); }

// getTeamCareer(league, short) → null | { short, name, color, wcc:['S21',…],
//   wdc:[{ season, driver }] (drivers' titles won while racing for the team), seasons:[…] }
function getTeamCareer(league, short) {
  const e = _archiveCache[league];
  if (!e || !e.data || !e.data.byTeam[short]) return null;
  const rows = e.data.byTeam[short];
  return {
    ...e.data.team(short), seasons: rows,
    wcc: rows.filter((r) => r.champion).map((r) => r.season),
    wdc: e.data.champions.filter((c) => c.wdc.team.short === short).map((c) => ({ season: c.season, driver: c.wdc.name })).reverse(),
  };
}
function useTeamCareer(league, short) { useArchive(league); return getTeamCareer(league, short); }

// Hydrate every snapshot league now, so the first render already has the data.
Object.keys(window.UR_ARCHIVE_SNAPSHOT || {}).forEach((l) => { if (UR_ARCHIVE_SOURCES[l]) loadArchive(l); });

Object.assign(window, {
  UR_ARCHIVE_SOURCES, loadArchive, useArchive, UR_ARCHIVE_EXPORT,
  getChampions, useChampions, getDriverCareer, useDriverCareer, getTeamCareer, useTeamCareer,
});
