// UR2D Final — DRIVER CAREERS · data.
//   Route #driver/<key>: key = roster handle (one person, ur2d-roster.jsx) | 'db-<id>' (a DB driver with no roster record)
//   | 'n-<name>' (a sheet name — resolved through the index). fddLoadProfile(key) → profile (cached); fddIndex() → every driver.
//   Sources (canonical rule): finished seasons → Bituko's DB (season points/positions from the league Mega Table where it has
//   them; disagreements kept as `disputed`) · current season → the league sheets (shared with team pages; DB fallback) ·
//   people → roster · liveries, logos, colours → our files.
// Load order: after ur2d-final-teamdata.jsx + ur2d-teampage.jsx, before ur2d-final-drivers.jsx.

const FDD_SITE = { UL: 'ul', CL: 'hl', JL: 'jl', AL: 'al' };
const FDD_DBC = { ul: 'UL', hl: 'CL', jl: 'JL', al: 'AL' };
const FDD_TIER = { UL: 0, HL: 1, JL: 2, AL: 3 };
const fddN = (s) => String(s || '').normalize('NFKC').toLowerCase().replace(/[^a-z0-9]/g, '');
const fddMtN = (s) => String(s || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
const fddR1 = (x) => Math.round((Number(x) || 0) * 10) / 10;
const fddLgS = (lg) => (lg === 'HL' ? 'CL' : lg);
const fddSn = (s) => fddLgS(s.lg) + ' ' + s.label;
const fddRoute = (key) => 'driver/' + encodeURIComponent(key);
const fddSd = (site, label) => { const ad = UR2D_ADAPTERS[site]; return (ad && ad.seasons.find((s) => s.label === label)) || null; };
const fddCurLabel = (site) => { const ad = UR2D_ADAPTERS[site]; const c = ad && ad.seasons.find((s) => s.isCurrent); return c ? c.label : null; };
const fddClock = (lg, label) => (typeof fdtChrono === 'function' ? fdtChrono({ lg, sid: label }) : 0);
const fddRangeOf = (x) => { const m = String(x == null ? '' : x).match(/^(\d+)(?:\s*-\s*(\d+))?$/); return m ? Array.from({ length: Math.max(1, (m[2] ? +m[2] : +m[1]) - +m[1] + 1) }, (_, k) => +m[1] + k) : []; };

function fddNum(p, names) {
  if (p && p.carNumber != null) return p.carNumber;
  const reg = window.UR_DRIVER_NUMBER || {};
  for (const x of [p && p.sheetName, p && p.display, p && p.handle, ...((p && p.aliases) || []), ...(names || [])]) {
    if (!x) continue;
    const v = reg[String(x).trim().toLowerCase()];
    if (v != null) return v;
  }
  return null;
}
// Current-season contracts from the roster: [{ site, label, code, role: main | reserve | independent }]
function fddRosterNow(handle) {
  const out = [];
  if (!handle || typeof getRoster !== 'function') return out;
  Object.keys(UR2D_ADAPTERS).forEach((site) => {
    const label = fddCurLabel(site), r = label ? getRoster(site, label) : null;
    if (!r) return;
    Object.entries(r.teams || {}).forEach(([code, t]) => {
      if ((t.mains || []).includes(handle)) out.push({ site, label, code, role: 'main' });
      else if (t.reserve === handle) out.push({ site, label, code, role: 'reserve' });
    });
    if ((r.independents || []).includes(handle)) out.push({ site, label, code: null, role: 'independent' });
  });
  return out;
}

// ── INDEX — every DB driver grouped into people (roster handle, else the DB id) + roster signings with no result yet ──
let _fddIdxP = null, _fddIdx = null;
function fddSummary(g) {
  const p = g.person;
  g.name = (p && (p.sheetName || p.display)) || g.names[0] || g.key;
  g.cc = (p && p.country) || g.cc || 'un';
  g.num = fddNum(p, g.names);
  const best = {};
  g.rows.forEach((r) => {
    const site = FDD_SITE[r.league]; if (!site) return;
    const label = 'S' + r.number + (r.division || ''), k = site + ':' + label;
    if (!best[k] || (Number(r.points) || 0) > (Number(best[k].points) || 0)) best[k] = { ...r, site, label, k };
  });
  g.byK = best;
  g.seasons = Object.values(best).map((r) => { const lg = r.site.toUpperCase(); return { k: r.k, site: r.site, lg, label: r.label, pos: r.standing, pts: Number(r.points) || 0, wins: r.wins || 0, podiums: r.podiums || 0, cur: fddCurLabel(r.site) === r.label, clock: fddClock(lg, r.label) }; })
    .sort((a, b) => a.clock - b.clock || FDD_TIER[b.lg] - FDD_TIER[a.lg]); // oldest first; same raceday → lower league first
  g.titlesBy = {}; g.winsBy = {}; g.ptsBy = {}; g.titles = [];
  g.seasons.forEach((s) => { const L = fddLgS(s.lg); g.winsBy[L] = (g.winsBy[L] || 0) + s.wins; g.ptsBy[L] = fddR1((g.ptsBy[L] || 0) + s.pts); if (s.pos === 1 && !s.cur) { g.titlesBy[L] = (g.titlesBy[L] || 0) + 1; g.titles.push(L + ' ' + s.label); } });
  g.wins = g.seasons.reduce((t, s) => t + s.wins, 0);
  g.podiums = g.seasons.reduce((t, s) => t + s.podiums, 0);
  g.first = g.seasons[0] || null; g.last = g.seasons[g.seasons.length - 1] || null;
  g.leagues = [...new Set(g.seasons.map((s) => fddLgS(s.lg)))];
  g.now = fddRosterNow(p && p.handle);
  g.current = g.seasons.some((s) => s.cur) || g.now.length > 0;
}
function fddIndex() {
  if (!_fddIdxP) {
    _fddIdxP = (async () => {
      const [drivers, stand] = await Promise.all([
        dbGet('drivers?select=id,name,country_code'),
        dbGetAll('v_driver_standings?select=season_id,league,number,division,driver_id,points,wins,podiums,poles,fastest_laps,standing'),
      ]);
      const byKey = {}, keyOfId = {}, nameOfId = {}, ccOfId = {}, byNorm = {};
      const group = (key, person) => byKey[key] || (byKey[key] = { key, person: person || null, ids: [], names: [], cc: null, rows: [] });
      drivers.forEach((d) => {
        const p = typeof resolvePerson === 'function' ? resolvePerson(d.name) : null;
        const key = p ? p.handle : 'db-' + d.id, g = group(key, p), cc = d.country_code ? String(d.country_code).toLowerCase() : null;
        g.ids.push(d.id);
        if (!g.names.includes(d.name)) g.names.push(d.name);
        if (!g.cc && cc) g.cc = cc;
        keyOfId[d.id] = key; nameOfId[d.id] = d.name; ccOfId[d.id] = cc;
        const n = fddN(d.name); if (n && !byNorm[n]) byNorm[n] = key;
      });
      stand.forEach((r) => { const k = keyOfId[r.driver_id]; if (k) byKey[k].rows.push(r); });
      Object.keys(UR2D_ADAPTERS).forEach((site) => { // signed this season, nothing in the DB yet → still gets a page
        const label = fddCurLabel(site), r = label && typeof getRoster === 'function' ? getRoster(site, label) : null;
        if (!r) return;
        const hs = new Set([...(r.independents || []), ...Object.values(r.teams || {}).flatMap((t) => [...(t.mains || []), t.reserve])].filter(Boolean));
        hs.forEach((h) => { const p = getPerson(h); if (!p || !p.sheetName || byKey[h]) return; group(h, p); const n = fddN(p.sheetName); if (n && !byNorm[n]) byNorm[n] = h; });
      });
      Object.values(byKey).forEach(fddSummary);
      const TI = ['UL', 'CL', 'JL', 'AL'];
      const list = Object.values(byKey).sort((a, b) => {
        for (const lg of TI) { const x = (b.titlesBy[lg] || 0) - (a.titlesBy[lg] || 0); if (x) return x; }
        for (const lg of TI) { const x = (b.winsBy[lg] || 0) - (a.winsBy[lg] || 0); if (x) return x; }
        return b.podiums - a.podiums || b.seasons.length - a.seasons.length || a.name.localeCompare(b.name);
      });
      return (_fddIdx = { byKey, keyOfId, nameOfId, ccOfId, byNorm, list });
    })().catch((e) => { _fddIdxP = null; throw e; });
  }
  return _fddIdxP;
}
function useFddIndex() {
  const [st, setSt] = React.useState(() => (_fddIdx ? { status: 'ready', idx: _fddIdx } : { status: 'loading', idx: null }));
  React.useEffect(() => {
    if (_fddIdx) return;
    let alive = true;
    fddIndex().then((idx) => alive && setSt({ status: 'ready', idx })).catch(() => alive && setSt({ status: 'error', idx: null }));
    return () => { alive = false; };
  }, []);
  return st;
}
// Any name form → page key (roster handle → DB driver → the name itself, resolved on the page).
function fddKeyForName(name) {
  const p = typeof resolvePerson === 'function' ? resolvePerson(name) : null;
  if (p) return p.handle;
  const k = _fddIdx && _fddIdx.byNorm[fddN(name)];
  return k || 'n-' + String(name || '').trim();
}
// Team-page people key ('r:<handle>' / 'r:x<norm>') → driver route. Sample profiles and vacant seats → null.
function fddRouteOf(w) {
  const P = window.TP && window.TP.PEOPLE && window.TP.PEOPLE[w];
  if (!P || P.vacant || !/^r:/.test(String(w))) return null;
  const h = String(w).slice(2);
  return fddRoute(window.UR2D_PEOPLE && window.UR2D_PEOPLE[h] ? h : fddKeyForName(P.name));
}
// What we know before the profile loads (loading screen).
function fddQuick(key) {
  const k = String(key || '');
  const p = k && !/^(db|n)-/.test(k) && typeof getPerson === 'function' ? getPerson(k) : null;
  const g = _fddIdx && _fddIdx.byKey[k];
  const name = (g && g.name) || (p && (p.sheetName || p.display)) || (k.startsWith('n-') ? k.slice(2) : null);
  return name ? { name, cc: (p && p.country) || (g && g.cc) || 'un', num: g ? g.num : fddNum(p, []) } : null;
}

// ── TEAMS as raced in one season (name/colour/logo/livery from that season's config, the DB name as fallback) ──
function fddTeamCtx(teams) {
  const byId = {}, byCode = {};
  teams.forEach((t) => { byId[t.id] = t; (byCode[t.short_name] = byCode[t.short_name] || []).push(t); });
  return { byId, byCode, shared: new Set(Object.keys(byCode).filter((c) => byCode[c].length > 1)), cache: {} };
}
function fddIdForCode(TC, code, prefer) {
  const AL = window.FDT_ALIAS || {}, c = [code, ...(AL[code] || [])].flatMap((x) => TC.byCode[x] || []);
  if (!c.length) return null;
  return (c.find((t) => prefer && prefer.has(t.id)) || c[0]).id;
}
function fddTeam(TC, site, label, dbId, codeHint) {
  const ck = site + ':' + label + ':' + (dbId != null ? dbId : 'c' + codeHint);
  if (TC.cache[ck]) return TC.cache[ck];
  const dbt = dbId != null ? TC.byId[dbId] : null, code = (dbt && dbt.short_name) || codeHint || '?';
  const AL = window.FDT_ALIAS || {}, codes = [...new Set([codeHint, code, ...(AL[code] || []), ...(AL[codeHint] || [])].filter(Boolean))];
  const sd = fddSd(site, label), ls = (sd && sd.liveSource) || {};
  const pick = (f) => (sd ? { ...(ls[f] || {}), ...(sd[f] || {}) } : {});
  const names = pick('teamNames'), colors = pick('teamColors'), lv = { ...pick('teamLiveries'), ...pick('driverLiveriesByTeam') }, ln = pick('liveryNames');
  const short = codes.find((c) => names[c]) || codes.find((c) => lv[c]) || code;
  const shared = TC.shared.has(code);
  let name = names[short] || (window.FDT_NAME_FIX || {})[code] || null, color = colors[short] || null;
  // Name = THIS season's config → known fix → the DB name (never another season's/league's newer name). fdtTeamMeta only lends a colour.
  if (!shared && !color && typeof fdtTeamMeta === 'function') color = fdtTeamMeta(site, label, short).color || null;
  name = name || (dbt && dbt.name) || short;
  const lc = codes.find((c) => lv[c]), livery = lc ? lv[lc] : null;
  if (!color && livery && typeof urLiveryColor === 'function') color = urLiveryColor(livery) || null;
  const named = window.UR_TEAM_LOGO_NAMED && window.UR_TEAM_LOGO_NAMED[fdtNrm(name)];
  const logo = named || (!shared && typeof fdtLogo === 'function' ? fdtLogo(short) || fdtLogo(code) : null);
  const sp = (sd && sd.specialLiveries) || {}, spc = codes.find((c) => sp[c]);
  return (TC.cache[ck] = { key: dbId != null ? 'id' + dbId : 'c' + short, id: dbId != null ? dbId : null, code, short, name, color: color || '#8C9198', logo, livery, lname: lc ? ln[lc] || null : null, specials: spc ? sp[spc] : [], shared, independent: false, lg: site });
}
function fddInd(site, label, handle) {
  const sd = fddSd(site, label), ls = (sd && sd.liveSource) || {};
  const iv = { ...(ls.independentLiveries || {}), ...((sd && sd.independentLiveries) || {}) };
  return { key: 'ind', id: null, code: 'IND', short: 'IND', name: 'Independent', color: '#3A3F45', logo: null, livery: (handle && iv[handle]) || null, lname: null, specials: [], independent: true, lg: site };
}
// Team page for a team (current grid → #team/<lg>/<CODE>, former DB team → #team/old/<id>; AL / independents → none).
function fddTeamRoute(t, former) {
  if (!t || t.independent || t.lg === 'al' || typeof fdtCurrentTeams !== 'function') return null;
  const AL = window.FDT_ALIAS || {}, nm = fddN(t.name);
  for (const lg of ['ul', 'hl', 'jl']) {
    const hit = (fdtCurrentTeams(lg) || []).find((x) => fddN(x.name) === nm || (!t.shared && [x.short, ...(AL[x.short] || [])].some((c) => c === t.short || c === t.code)));
    if (hit) return 'team/' + lg + '/' + encodeURIComponent(hit.short);
  }
  return t.id != null && former && former.has(t.id) ? 'team/old/' + t.id : null;
}

// ── SEASONS ──
function fddRound(n, race, team, x, mate) {
  const f = x && x.f != null && x.f > 0 ? x.f : null;
  return {
    n, track: fdtTrack(race, n), date: race && race.date ? new Date(race.date + 'T12:00:00Z') : null, team, absent: !x, dns: !x || f == null,
    q: x && x.q != null && x.q > 0 ? x.q : null, f, pts: x ? fddR1(x.pts) : 0, dnf: !!(x && x.dnf), dsq: !!(x && x.dsq), fl: !!(x && x.fl),
    pole: !!(x && x.q === 1), sprint: x && x.sprint != null ? x.sprint : null, mate: mate && mate.f != null ? mate : null,
  };
}
function fddShell(site, label, live, rounds) {
  const lg = site.toUpperCase();
  return { k: site + ':' + label, site, lg, label, num: label.replace(/^S/, ''), live, clock: fddClock(lg, label), period: typeof fdtDbPeriod === 'function' ? fdtDbPeriod(FDD_DBC[site], label) : '', rounds };
}
const fddMtRank = (T, e) => T.entries.filter((x) => x.mt != null).sort((a, b) => b.mt - a.mt).indexOf(e) + 1;
function fddDbSeason(m, rows, rl, C, stRow, tsBy) {
  const race = rows.filter((r) => r.session === 'race'), spr = rows.filter((r) => r.session === 'sprint');
  const started = race.filter((r) => r.position != null);
  if (!started.length && !stRow) return null;
  const solo = m.site === 'al';
  const tOf = (tid) => (tid == null ? fddInd(m.site, m.label, C.handle) : fddTeam(C.TC, m.site, m.label, tid));
  const mine = new Set(race.map((r) => r.round));
  const allN = [...new Set([...rl.map((r) => r.round), ...mine])].sort((a, b) => a - b);
  const ns = started.length * 2 >= allN.length ? allN : allN.filter((n) => mine.has(n)); // a regular: every round; a stand-in: their rounds
  const rounds = ns.map((n) => {
    const r = race.find((x) => x.round === n) || null, sp = spr.find((x) => x.round === n) || null;
    const rc = fdtDbRace(m.code, m.label, n, rl.find((x) => x.round === n) || { round: n });
    return fddRound(n, rc, r ? tOf(r.team_id) : null,
      r && { q: r.grid, f: r.position, pts: (Number(r.points) || 0) + (sp ? Number(sp.points) || 0 : 0), dnf: r.dnf, dsq: r.dsq, fl: r.fastest_lap, sprint: sp && sp.position != null ? sp.position : null },
      r && r.team_id != null && !solo ? C.mateOf(m.dbSid, r.team_id, n) : null);
  });
  const s = fddShell(m.site, m.label, false, rounds);
  s.src = 'db';
  let pos = stRow ? stRow.standing : null, pts = stRow ? Number(stRow.points) || 0 : rounds.reduce((t, r) => t + r.pts, 0), disputed = null;
  const MS = C.mega[m.site] && C.mega[m.site].seasons && C.mega[m.site].seasons[m.label];
  const T = MS && MS.drivers;
  if (T && !T.incomplete) { // Mega Table by default; the DB where it has nothing; a disagreement is kept, not hidden
    const e = [C.handle && 'h:' + C.handle, ...C.names.map((x) => 'n:' + fddMtN(x))].filter(Boolean).map((k) => T.byKey[k]).find(Boolean);
    if (e && e.mt != null) { pts = e.mt; pos = fddMtRank(T, e); if (e.db != null && Math.abs(e.mt - e.db) > 0.001) disputed = { mt: e.mt, db: e.db }; }
  }
  Object.assign(s, { pos, pts: fddR1(pts), disputed, wins: stRow ? stRow.wins : null, podiums: stRow ? stRow.podiums : null, poles: stRow ? stRow.poles : null, fl: stRow ? stRow.fastest_laps : null });
  const tally = {};
  started.forEach((r) => { if (r.team_id != null) tally[r.team_id] = (tally[r.team_id] || 0) + 1; });
  const mainTid = Object.keys(tally).sort((a, b) => tally[b] - tally[a])[0];
  if (mainTid && !solo) {
    const ts = tsBy[m.dbSid + ':' + mainTid], TT = MS && MS.teams, t = tOf(+mainTid);
    let tpos = ts ? ts.standing : null;
    if (TT && !TT.incomplete) { const e = [t.code, t.short, ...((window.FDT_ALIAS || {})[t.short] || [])].map((c) => TT.byKey[String(c).toUpperCase()]).find(Boolean); if (e && e.mt != null) tpos = fddMtRank(TT, e); }
    s.teamPos = tpos;
  }
  return s;
}
const _fddSheet = {};
function fddSheetData(site, sd) {
  const k = site + ':' + sd.label;
  if (!_fddSheet[k]) {
    const solo = UR2D_ADAPTERS[site] && UR2D_ADAPTERS[site].teamBased === false;
    _fddSheet[k] = (!solo && typeof fdtSeasonData === 'function' ? fdtSeasonData(site, sd) :
      Promise.all([
        loadLeaderboardStandings({ ...sd.liveSource, driverTeamTable: null, roundTeamTable: null, topN: null }).then((raw) => normalizeStandings(raw, { teamBased: false, label: sd.label })),
        loadRaceResults(site, sd.label),
      ]).then(([st, res]) => ({ st, res: res || {}, rmap: {} }))).catch(() => null);
    _fddSheet[k].then((x) => { if (!x) delete _fddSheet[k]; });
  }
  return _fddSheet[k];
}
function fddSheetSeason(site, sd, data, C, isMe, dbr) {
  const { st, res, rmap } = data, label = sd.label, drv = (st && st.drivers) || [];
  const row = drv.find((d) => isMe(d.name)) || null;
  const ns = Object.keys(res || {}).map(Number).filter((n) => n > 0).sort((a, b) => a - b);
  const meIn = (n) => (res[n] || []).find((x) => isMe(x.name)) || null;
  if (!row && !ns.some(meIn)) return null;
  const solo = site === 'al';
  const cc = (getCalendarSeasons().find((c) => c.isCurrent) || {}).id, cal = getCalendar(site, cc), off = sd.suffix === 'B' ? 5 : 0;
  const stTeam = (nm) => { const d = drv.find((x) => fdtNrm(x.name) === fdtNrm(nm)); return d && d.team && d.team !== 'IND' ? d.team : null; };
  const teamOf = (n, nm) => { const m = rmap && rmap[n]; if (m && Object.keys(m).length) { const c = m[fdtNrm(nm)]; return c && c !== 'IND' ? c : null; } return solo ? null : stTeam(nm); };
  const started = ns.filter((n) => meIn(n)), show = started.length * 2 >= ns.length ? ns : started;
  const rounds = show.map((n) => {
    const r = meIn(n), race = cal && cal.races ? cal.races[off + n - 1] : null, code = r ? teamOf(n, r.name) : null;
    const team = r ? (code ? fddTeam(C.TC, site, label, fddIdForCode(C.TC, code, C.myTeams), code) : fddInd(site, label, C.handle)) : null;
    const mr = r && code && !solo ? (res[n] || []).find((x) => x !== r && teamOf(n, x.name) === code) : null, d = dbr[n];
    return fddRound(n, race, team, r && { q: r.quali, f: r.finish, pts: r.points, dnf: d && d.dnf, dsq: d && d.dsq, fl: d && d.fastest_lap, sprint: r.sprint },
      mr ? { ...C.mateByName(mr.name), q: mr.quali, f: mr.finish, pts: fddR1(mr.points) } : null);
  });
  const s = fddShell(site, label, true, rounds), done = rounds.filter((r) => !r.dns);
  Object.assign(s, {
    src: 'sheet', pos: row && started.length ? row.pos : null, pts: fddR1(row ? row.pts : done.reduce((t, r) => t + r.pts, 0)), disputed: null,
    wins: row && row.wins != null ? row.wins : done.filter((r) => r.f === 1).length, podiums: row && row.podiums != null ? row.podiums : done.filter((r) => r.f <= 3).length,
    poles: done.filter((r) => r.pole).length, fl: done.filter((r) => r.fl).length,
  });
  if (row && row.team === 'IND' && row.erTeams && row.erTeams.length) s.erTeams = row.erTeams;
  return s;
}
function fddCar(s, t, file, name, special, rounds, rl) {
  return { key: s.k + ':' + t.key + ':' + (special ? file : 'main'), s, t, file: file || null, name, special, rl, ns: rounds.map((r) => r.n), races: rounds.length,
    pts: fddR1(rounds.reduce((a, r) => a + r.pts, 0)), wins: rounds.filter((r) => r.f === 1).length, podiums: rounds.filter((r) => r.f <= 3).length, poles: rounds.filter((r) => r.pole).length };
}
function fddFinish(s) {
  const st = s.rounds.filter((r) => !r.dns);
  s.starts = st.length;
  if (s.wins == null) s.wins = st.filter((r) => r.f === 1).length;
  if (s.podiums == null) s.podiums = st.filter((r) => r.f <= 3).length;
  if (s.poles == null) s.poles = st.filter((r) => r.pole).length;
  if (s.fl == null) s.fl = st.filter((r) => r.fl).length;
  s.best = st.length ? Math.min(...st.map((r) => r.f)) : null;
  s.top5 = st.filter((r) => r.f <= 5).length; s.top10 = st.filter((r) => r.f <= 10).length; s.dnfs = st.filter((r) => r.dnf).length;
  s.avgF = st.length ? fddR1(st.reduce((t, r) => t + r.f, 0) / st.length) : null;
  const T = {};
  st.forEach((r) => { if (!r.team) return; const o = T[r.team.key] || (T[r.team.key] = { t: r.team, starts: 0, last: 0 }); o.starts++; o.last = r.n; });
  s.teams = Object.values(T).sort((a, b) => b.starts - a.starts || b.last - a.last);
  s.team = s.teams[0] ? s.teams[0].t : ((s.rounds.find((r) => r.team) || {}).team || null);
  s.wdc = !s.live && s.pos === 1;
  s.wcc = !s.live && s.teamPos === 1;
  s.podList = st.filter((r) => r.f <= 3).map((r) => ({ k: s.k, sn: fddSn(s), n: r.n, pos: r.f, map: r.track.map, gp: r.track.gp, circuit: r.track.circuit, team: r.team }));
  const M = {};
  st.forEach((r) => {
    const m = r.mate; if (!m) return;
    const o = M[m.key] || (M[m.key] = { key: m.key, name: m.name, cc: m.cc, num: m.num, team: r.team, n: 0, q: [0, 0], r: [0, 0], pts: [0, 0], podiums: [0, 0], wins: [0, 0] });
    o.n++;
    if (r.q != null && m.q != null && r.q !== m.q) o.q[r.q < m.q ? 0 : 1]++;
    if (r.f !== m.f) o.r[r.f < m.f ? 0 : 1]++;
    o.pts[0] = fddR1(o.pts[0] + r.pts); o.pts[1] = fddR1(o.pts[1] + (m.pts || 0));
    if (r.f <= 3) o.podiums[0]++; if (m.f <= 3) o.podiums[1]++;
    if (r.f === 1) o.wins[0]++; if (m.f === 1) o.wins[1]++;
  });
  s.mates = Object.values(M).sort((a, b) => b.n - a.n);
  s.mate = s.mates[0] || null;
  s.cars = [];
  s.teams.forEach(({ t }) => { // per team: the round specials they drove (latest first), then the season car for the rest
    const mine = st.filter((r) => r.team && r.team.key === t.key), have = new Set(mine.map((r) => r.n)), used = new Set();
    (t.specials || []).map((x) => ({ x, rs: fddRangeOf(x.rounds).filter((n) => have.has(n)) })).filter((o) => o.rs.length && o.x.file).sort((a, b) => b.rs[0] - a.rs[0])
      .forEach(({ x, rs }) => { rs.forEach((n) => used.add(n)); s.cars.push(fddCar(s, t, x.file, x.name || 'Special livery', true, mine.filter((r) => rs.includes(r.n)), x.rounds)); });
    const rest = mine.filter((r) => !used.has(r.n));
    if (rest.length) s.cars.push(fddCar(s, t, t.livery, t.lname || (t.independent ? 'Independent car' : t.short + '-' + s.num), false, rest, null));
  });
}

// ── CAREER — totals, leagues, teams, podiums, racecraft, circuits, teammates, records ──
function fddAll(seasons) {
  const done = seasons.filter((s) => s.starts > 0), chron = done.slice().reverse();
  const flat = chron.flatMap((s) => s.rounds.filter((r) => !r.dns).map((r) => ({ s, r })));
  const sum = (f) => done.reduce((t, s) => t + (Number(s[f]) || 0), 0), pct = (a, b) => (b ? Math.round(a / b * 100) : 0);
  const A = { seasons: done.length, starts: flat.length, wins: sum('wins'), podiums: sum('podiums'), poles: sum('poles'), fl: sum('fl'), pts: fddR1(sum('pts')), top5: sum('top5'), top10: sum('top10'), dnfs: sum('dnfs'), pct };
  A.avgF = flat.length ? fddR1(flat.reduce((t, x) => t + x.r.f, 0) / flat.length) : null;
  const qd = flat.filter((x) => x.r.q != null);
  A.avgQ = qd.length ? fddR1(qd.reduce((t, x) => t + x.r.q, 0) / qd.length) : null;
  A.titles = done.filter((s) => s.wdc); A.wccs = done.filter((s) => s.wcc);
  A.champPods = done.filter((s) => !s.live && (s.pos === 2 || s.pos === 3));
  const ranked = done.filter((s) => s.pos), fin = ranked.filter((s) => !s.live);
  A.bestPos = (fin.length ? fin : ranked).reduce((m, s) => (!m || s.pos < m.pos ? s : m), null);
  A.first = flat[0] || null; A.last = flat[flat.length - 1] || null;
  A.leagues = ['UL', 'HL', 'JL', 'AL'].map((lg) => {
    const L = done.filter((s) => s.lg === lg); if (!L.length) return null;
    const rs = L.flatMap((s) => s.rounds.filter((r) => !r.dns)), pos = L.filter((s) => !s.live && s.pos).map((s) => s.pos), anyPos = L.filter((s) => s.pos).map((s) => s.pos);
    const H = {};
    L.forEach((s) => s.teams.forEach(({ t, starts }) => { const o = H[t.key] || (H[t.key] = { t, starts: 0, seasons: 0, wins: 0 }); o.starts += starts; o.seasons++; }));
    rs.forEach((r) => { if (r.f === 1 && r.team && H[r.team.key]) H[r.team.key].wins++; });
    const home = Object.values(H).sort((a, b) => (a.t.independent ? 1 : 0) - (b.t.independent ? 1 : 0) || b.starts - a.starts)[0] || null;
    return { lg, since: L[L.length - 1].label, seasons: L.length, best: pos.length ? Math.min(...pos) : anyPos.length ? Math.min(...anyPos) : null, starts: rs.length,
      wins: L.reduce((t, s) => t + (s.wins || 0), 0), podiums: L.reduce((t, s) => t + (s.podiums || 0), 0), poles: L.reduce((t, s) => t + (s.poles || 0), 0), fl: L.reduce((t, s) => t + (s.fl || 0), 0),
      wdc: L.filter((s) => s.wdc).length, wcc: L.filter((s) => s.wcc).length, home };
  }).filter(Boolean);
  const TM = {};
  chron.forEach((s) => s.rounds.forEach((r) => {
    if (r.dns || !r.team) return;
    const o = TM[r.team.key] || (TM[r.team.key] = { key: r.team.key, t: r.team, ks: [], starts: 0, pts: 0, wins: 0, podiums: 0 });
    o.t = r.team; if (!o.ks.includes(s.k)) o.ks.push(s.k);
    o.starts++; o.pts = fddR1(o.pts + r.pts); if (r.f === 1) o.wins++; if (r.f <= 3) o.podiums++;
  }));
  A.teams = Object.values(TM).map((o) => {
    const ss = o.ks.map((k) => done.find((s) => s.k === k)), a = ss[0], b = ss[ss.length - 1];
    return { ...o, n: ss.length, span: a === b ? fddSn(a) : fddSn(a) + ' – ' + (a.lg === b.lg ? b.label : fddSn(b)), current: !!b.live, clock: b.clock };
  }).sort((a, b) => b.clock - a.clock || b.starts - a.starts);
  A.podList = chron.flatMap((s) => s.podList);
  A.cars = done.flatMap((s) => s.cars);
  let top = null; A.promos = []; // first season in a league higher than any before it
  chron.forEach((s) => { const t = FDD_TIER[s.lg]; if (top != null && t < top) A.promos.push({ k: s.k, to: s.lg, s }); top = top == null ? t : Math.min(top, t); });
  // racecraft
  const hist = {}; flat.forEach((x) => { const b = Math.min(x.r.f, 21); hist[b] = (hist[b] || 0) + 1; });
  const maxB = Math.max(10, ...Object.keys(hist).map(Number));
  A.hist = Array.from({ length: maxB }, (_, i) => ({ p: i + 1, n: hist[i + 1] || 0, over: i + 1 === 21 }));
  const gq = flat.filter((x) => x.r.q != null);
  A.gainAvg = gq.length ? fddR1(gq.reduce((t, x) => t + (x.r.q - x.r.f), 0) / gq.length) : null;
  A.climb = gq.reduce((m, x) => (x.r.q - x.r.f > (m ? m.g : 0) ? { g: x.r.q - x.r.f, x } : m), null);
  A.frontRows = gq.filter((x) => x.r.q <= 2).length;
  A.frontPods = gq.filter((x) => x.r.q <= 2 && x.r.f <= 3).length;
  A.winsFromBehind = gq.filter((x) => x.r.f === 1 && x.r.q > 3).length;
  const pl = flat.filter((x) => x.r.pole); A.poleConv = { poles: pl.length, wins: pl.filter((x) => x.r.f === 1).length };
  A.ptsRate = pct(flat.filter((x) => x.r.pts > 0).length, flat.length);
  A.finRate = pct(flat.filter((x) => !x.r.dnf && !x.r.dsq).length, flat.length);
  A.ppS = flat.length ? fddR1(A.pts / flat.length) : 0;
  if (flat.length > 1) { const m = flat.reduce((t, x) => t + x.r.f, 0) / flat.length; A.sd = fddR1(Math.sqrt(flat.reduce((t, x) => t + (x.r.f - m) * (x.r.f - m), 0) / flat.length)); } else A.sd = null;
  const sp = flat.filter((x) => x.r.sprint != null);
  A.sprint = sp.length ? { n: sp.length, wins: sp.filter((x) => x.r.sprint === 1).length, podiums: sp.filter((x) => x.r.sprint <= 3).length, avg: fddR1(sp.reduce((t, x) => t + x.r.sprint, 0) / sp.length) } : null;
  const CI = {};
  flat.forEach((x) => {
    const k = x.r.track.map || 'c:' + fddN(x.r.track.circuit); if (/^c:round\d+$/.test(k)) return;
    const o = CI[k] || (CI[k] = { k, track: x.r.track, n: 0, sumF: 0, best: 99, wins: 0, podiums: 0 });
    o.n++; o.sumF += x.r.f; o.best = Math.min(o.best, x.r.f); if (x.r.f === 1) o.wins++; if (x.r.f <= 3) o.podiums++; o.track = x.r.track;
  });
  A.circuits = Object.values(CI).map((o) => ({ ...o, avg: fddR1(o.sumF / o.n) })).sort((a, b) => b.n - a.n || a.avg - b.avg);
  const rep = A.circuits.filter((o) => o.n >= 2);
  A.bestTracks = rep.slice().sort((a, b) => a.avg - b.avg || b.n - a.n).slice(0, 6);
  A.bestCircuit = A.circuits.slice().sort((a, b) => b.podiums - a.podiums || b.wins - a.wins)[0];
  if (A.bestCircuit && !A.bestCircuit.podiums) A.bestCircuit = null;
  const PR = {};
  done.forEach((s) => s.mates.forEach((m) => {
    const o = PR[m.key] || (PR[m.key] = { key: m.key, name: m.name, cc: m.cc, num: m.num, ks: [], n: 0, q: [0, 0], r: [0, 0], pts: [0, 0], podiums: [0, 0], wins: [0, 0] });
    o.ks.push(s.k); o.n += m.n;
    [0, 1].forEach((i) => { o.q[i] += m.q[i]; o.r[i] += m.r[i]; o.pts[i] = fddR1(o.pts[i] + m.pts[i]); o.podiums[i] += m.podiums[i]; o.wins[i] += m.wins[i]; });
  }));
  A.pairs = Object.values(PR).sort((a, b) => b.n - a.n);
  const mr = A.pairs.reduce((t, p) => [t[0] + p.r[0], t[1] + p.r[0] + p.r[1]], [0, 0]), mq = A.pairs.reduce((t, p) => [t[0] + p.q[0], t[1] + p.q[0] + p.q[1]], [0, 0]);
  const ms = done.filter((s) => s.mate && s.mate.n >= 3);
  A.beat = { r: mr, q: mq, seasons: [ms.filter((s) => s.mate.pts[0] > s.mate.pts[1]).length, ms.length] };
  const streak = (f) => { let best = 0, cur = 0, end = null; flat.forEach((x) => { if (f(x.r)) { cur++; if (cur > best) { best = cur; end = x; } } else cur = 0; }); return { n: best, end }; };
  A.winStreak = streak((r) => r.f === 1); A.podStreak = streak((r) => r.f <= 3); A.ptsStreak = streak((r) => r.pts > 0);
  A.firstWin = flat.find((x) => x.r.f === 1) || null; A.firstPod = flat.find((x) => x.r.f <= 3) || null;
  A.lastWin = flat.slice().reverse().find((x) => x.r.f === 1) || null;
  const bf = flat.length ? Math.min(...flat.map((x) => x.r.f)) : null, occ = flat.filter((x) => x.r.f === bf);
  A.best = bf ? { f: bf, count: occ.length, last: occ[occ.length - 1] } : null;
  A.mostWins = done.reduce((m, s) => (!m || s.wins > m.wins ? s : m), null);
  A.bestSeason = ranked.slice().sort((a, b) => a.pos - b.pos || FDD_TIER[a.lg] - FDD_TIER[b.lg] || b.wins - a.wins || b.clock - a.clock)[0] || null;
  return A;
}

// ── PROFILE ──
async function fddBuild(key0) {
  const idx = await fddIndex();
  let key = String(key0 || ''), hint = null;
  if (key.startsWith('n-')) { hint = key.slice(2); const p = typeof resolvePerson === 'function' ? resolvePerson(hint) : null; key = (p && p.handle) || idx.byNorm[fddN(hint)] || key; }
  const g = idx.byKey[key] || null;
  const person = (g && g.person) || (!/^(db|n)-/.test(key) && typeof getPerson === 'function' ? getPerson(key) : null);
  if (!g && !person && !hint) return { missing: true, key };
  const handle = person ? person.handle : null;
  const names = [...new Set([...(g ? g.names : []), hint, person && person.sheetName, person && person.display, ...((person && person.aliases) || [])].filter(Boolean))];
  const norms = new Set(names.map(fddN));
  const isMe = (nm) => { if (!nm) return false; if (handle) { const p = resolvePerson(nm); if (p) return p.handle === handle; } return norms.has(fddN(nm)); };
  const ids = g ? g.ids : [], idSet = new Set(ids);
  const [res, seasons, teams] = await Promise.all([
    ids.length ? dbGetAll('v_result_points?select=season_id,round,session,position,grid,points,dnf,dsq,fastest_lap,team_id,driver_id&driver_id=in.(' + ids.join(',') + ')&order=season_id,round') : Promise.resolve([]),
    dbGet('seasons?select=id,number,division,leagues(code)'),
    dbTeams(),
  ]);
  const TC = fddTeamCtx(teams), sOf = {};
  seasons.forEach((x) => { const code = x.leagues && x.leagues.code, site = FDD_SITE[code]; if (site) sOf[x.id] = { dbSid: x.id, code, site, lg: site.toUpperCase(), label: 'S' + x.number + (x.division || '') }; });
  const bySid = {};
  res.forEach((r) => { (bySid[r.season_id] = bySid[r.season_id] || []).push(r); });
  const sids = Object.keys(bySid).map(Number).filter((sid) => sOf[sid]);
  const myTeams = new Set(res.map((r) => r.team_id).filter((x) => x != null)), tIds = [...myTeams];
  const sites = [...new Set(sids.map((sid) => sOf[sid].site))];
  const curSites = new Set(sids.filter((sid) => sOf[sid].label === fddCurLabel(sOf[sid].site)).map((sid) => sOf[sid].site));
  (handle ? fddRosterNow(handle) : []).forEach((x) => curSites.add(x.site));
  if (hint && !g) Object.keys(UR2D_ADAPTERS).forEach((s) => curSites.add(s));
  const curSd = (site) => UR2D_ADAPTERS[site] && UR2D_ADAPTERS[site].seasons.find((x) => x.isCurrent);
  const [races, mates, tstand, megaL, sheets] = await Promise.all([
    sids.length ? dbGetAll('v_races?select=season_id,round,track,race_date&season_id=in.(' + sids.join(',') + ')&order=season_id,round') : Promise.resolve([]),
    tIds.length ? dbGetAll('v_result_points?select=season_id,round,session,position,grid,points,team_id,driver_id&season_id=in.(' + sids.join(',') + ')&team_id=in.(' + tIds.join(',') + ')').catch(() => []) : Promise.resolve([]),
    tIds.length ? dbGet('v_team_standings?select=season_id,team_id,standing,points&season_id=in.(' + sids.join(',') + ')&team_id=in.(' + tIds.join(',') + ')').catch(() => []) : Promise.resolve([]),
    Promise.all(sites.map((site) => (typeof fdtMega === 'function' ? fdtMega(site) : Promise.resolve(null)))),
    Promise.all([...curSites].map((site) => { const sd = curSd(site); return sd && sd.liveSource ? fddSheetData(site, sd).then((d) => [site, sd, d]).catch(() => null) : Promise.resolve(null); })),
  ]);
  const mega = {}; sites.forEach((s, i) => { mega[s] = megaL[i]; });
  const pairs = new Set(res.filter((r) => r.team_id != null).map((r) => r.season_id + ':' + r.team_id));
  const mrows = mates.filter((r) => !idSet.has(r.driver_id) && pairs.has(r.season_id + ':' + r.team_id));
  const mStarts = {}, mBy = {};
  mrows.forEach((r) => {
    if (r.session === 'race' && r.position != null) { const k = r.season_id + ':' + r.team_id + ':' + r.driver_id; mStarts[k] = (mStarts[k] || 0) + 1; }
    const k = r.season_id + ':' + r.team_id + ':' + r.round, o = mBy[k] || (mBy[k] = { race: [], spr: {} });
    if (r.session === 'race') o.race.push(r); else o.spr[r.driver_id] = (o.spr[r.driver_id] || 0) + (Number(r.points) || 0);
  });
  const info = (did) => { const k = idx.keyOfId[did] || 'db-' + did, gg = idx.byKey[k]; return { key: k, name: (gg && gg.name) || idx.nameOfId[did] || 'Driver ' + did, cc: (gg && gg.cc) || idx.ccOfId[did] || 'un', num: gg ? gg.num : null }; };
  const C = {
    TC, handle, names, mega, idx, myTeams,
    mateOf: (sid, tid, n) => {
      const o = mBy[sid + ':' + tid + ':' + n]; if (!o) return null;
      const r = o.race.filter((x) => x.position != null).sort((a, b) => (mStarts[sid + ':' + tid + ':' + b.driver_id] || 0) - (mStarts[sid + ':' + tid + ':' + a.driver_id] || 0))[0];
      return r ? { ...info(r.driver_id), q: r.grid, f: r.position, pts: fddR1((Number(r.points) || 0) + (o.spr[r.driver_id] || 0)) } : null;
    },
    mateByName: (nm) => { const k = fddKeyForName(nm), gg = idx.byKey[k], p = typeof resolvePerson === 'function' ? resolvePerson(nm) : null; return { key: k, name: (gg && gg.name) || nm, cc: (gg && gg.cc) || (p && p.country) || 'un', num: gg ? gg.num : fddNum(p, [nm]) }; },
  };
  const stBy = {}; // my standings row per DB season (the strongest when a person has two DB ids in one season)
  (g ? g.rows : []).forEach((r) => { if (!stBy[r.season_id] || (Number(r.points) || 0) > (Number(stBy[r.season_id].points) || 0)) stBy[r.season_id] = r; });
  const tsBy = {}; tstand.forEach((r) => { tsBy[r.season_id + ':' + r.team_id] = r; });
  const built = [];
  sids.forEach((sid) => {
    const m = sOf[sid];
    try { const s = fddDbSeason(m, bySid[sid], races.filter((x) => x.season_id === sid), C, stBy[sid] || null, tsBy); if (s) built.push(s); }
    catch (e) { console.warn('[ur2d-driverdata] DB season', m.lg, m.label, e); }
  });
  sheets.filter(Boolean).forEach(([site, sd, data]) => {
    if (!data) return;
    const dbs = sids.find((sid) => sOf[sid].site === site && sOf[sid].label === sd.label), dbr = {};
    (dbs ? bySid[dbs] : []).forEach((r) => { if (r.session === 'race') dbr[r.round] = r; });
    try {
      const s = fddSheetSeason(site, sd, data, C, isMe, dbr);
      if (s) { const i = built.findIndex((x) => x.k === s.k); if (i >= 0) built.splice(i, 1, s); else built.push(s); }
    } catch (e) { console.warn('[ur2d-driverdata] sheet season', site, sd.label, e); }
  });
  built.sort((a, b) => b.clock - a.clock || FDD_TIER[a.lg] - FDD_TIER[b.lg]);
  built.forEach(fddFinish);
  if (handle) built.forEach((s) => { // signed this season but not raced yet (e.g. a reserve) → the contract's team
    if (s.team || !s.live) return;
    const n = fddRosterNow(handle).find((x) => x.site === s.site);
    if (n) s.team = n.code ? fddTeam(TC, s.site, s.label, fddIdForCode(TC, n.code, myTeams), n.code) : fddInd(s.site, s.label, handle);
  });
  const name = (g && g.name) || (person && (person.sheetName || person.display)) || hint || key;
  const id = { key, handle, name, lines: fdtLines(name), cc: (person && person.country) || (g && g.cc) || 'un', num: g ? g.num : fddNum(person, names),
    display: person && person.display && fddN(person.display) !== fddN(name) ? person.display : null };
  const P = window.TP && window.TP.PEOPLE; // TPTimeline / TPH2HBars read names from the team page's people map
  if (P) { P['d:' + key] = { name: id.name, cc: id.cc, num: id.num }; built.forEach((s) => s.mates.forEach((m) => { P['d:' + m.key] = { name: m.name, cc: m.cc, num: m.num }; })); }
  const lead = built.find((s) => s.starts > 0 && s.team && !s.team.independent);
  return { key, id, seasons: built, all: fddAll(built), now: handle ? fddRosterNow(handle) : [], fresh: !built.some((s) => s.starts > 0), color: lead ? lead.team.color : '#E10600' };
}
const fddProfileCache = {}, fddProfileReady = {};
function fddLoadProfile(key) {
  if (!fddProfileCache[key]) {
    fddProfileCache[key] = fddBuild(key)
      .then((p) => { if (p && !p.missing) { fddProfileReady[key] = p; fddProfileReady[p.key] = p; } else delete fddProfileCache[key]; return p; })
      .catch((e) => { console.warn('[ur2d-driverdata] ' + key, e); delete fddProfileCache[key]; return { failed: true, key }; });
  }
  return fddProfileCache[key];
}

Object.assign(window, { FDD_TIER, fddN, fddR1, fddLgS, fddSn, fddRoute, fddCurLabel, fddIndex, useFddIndex, fddKeyForName, fddRouteOf, fddQuick, fddTeamRoute, fddLoadProfile, fddProfileReady });
