const fs = require("fs");
const path = require("path");

const PROFILE_SAVE_INTERVAL_MS = 5000;
const SEEN_COUNT_INTERVAL_MS = 10 * 60 * 1000;
const STALE_AFTER_MS = 15000;
const HOLD_LAST_GOOD_MS = 45000;
const PROFILE_TTL_MS = 90 * 24 * 60 * 60 * 1000;
const EXACT_PROFILE_MIN_SEEN = 2;
const FALLBACK_PROFILE_MIN_SEEN = 10;
// Battle watch: adjacent cars this close, for this many consecutive updates
// spanning at least this long, count as a sustained battle (not a blip).
const BATTLE_GAP_MS = 600;
const BATTLE_MIN_UPDATES = 3;
const BATTLE_MIN_DURATION_MS = 2500;
// The leader crosses the line before the chaser, so a nose-to-tail pair reads
// "-1 LAP" for a beat every lap; streaks survive blips shorter than this.
const BATTLE_GRACE_MS = 4000;
const LIST_KEYS = ["competitors", "lineup", "runningOrder", "qualifyingOrder", "raceOrder", "results"];

function cleanText(value) {
  return String(value == null ? "" : value).replace(/\s+/g, " ").trim();
}

function normalizeDriverName(value) {
  const text = cleanText(value);
  if (!text) return "";
  return text.replace(/[^\s]+/gu, token => {
    if (token.length <= 1) return token;
    const hasLetter = /\p{L}/u.test(token);
    if (!hasLetter || token !== token.toLocaleUpperCase() || token === token.toLocaleLowerCase()) return token;
    return token
      .split(/([-'’])/u)
      .map(part => /[-'’]/u.test(part) || !part ? part : part.charAt(0).toLocaleUpperCase() + part.slice(1).toLocaleLowerCase())
      .join("");
  });
}

function isLikelyVehicleDescription(value) {
  const text = cleanText(value).toLowerCase();
  if (!text) return false;
  const hasModelYear = /\b(?:19|20)\d{2}\b/.test(text);
  const vehicleWords = /\b(?:porsche|chevrolet|chevy|ford|toyota|honda|dodge|mustang|camaro|corvette|supra|bmw|audi|nissan|mazda|mercedes|clubsport|chassis|crate|engine|motor|modified|late model|street stock|sportsman|gt2|gt3|rs)\b/i;
  const specPattern = /\b(?:gt[234]|rs|zr1|z06|ss|lt1|ls[0-9])\b/i;
  return (hasModelYear && vehicleWords.test(text)) || /\bclubsport\b/i.test(text) || (specPattern.test(text) && vehicleWords.test(text));
}

const CAR_COLOR_ONLY_PATTERN = /^(?:(?:black|white|red|blue|green|yellow|orange|purple|silver|gold|gray|grey|maroon|pink|teal|navy|copper|chrome|charcoal|tan|cream|burgundy|lime|camo|neon)[\s\/&,.-]*)+$/i;

function isLikelyCarColor(value) {
  return CAR_COLOR_ONLY_PATTERN.test(String(value || "").trim());
}

function cleanHometown(value) {
  const text = cleanText(value);
  return isLikelyVehicleDescription(text) || isLikelyCarColor(text) ? "" : text;
}

function normalizeKey(value) {
  return cleanText(value)
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, " ")
    .trim()
    .replace(/\s+/g, " ");
}

function numberKey(value) {
  return cleanText(value).toUpperCase().replace(/\s+/g, "");
}

function profileKey(className, number) {
  return `${normalizeKey(className) || "general"}|${numberKey(number)}`;
}

function profileSeenCount(profile = {}) {
  const seen = Number(profile.seenCount || 0);
  return Number.isFinite(seen) ? seen : 0;
}

function profileLastSeenMs(profile = {}) {
  const seen = Date.parse(profile.lastSeen || profile.firstSeen || "");
  return Number.isFinite(seen) ? seen : 0;
}

function profileFreshEnough(profile = {}, now = Date.now()) {
  const lastSeen = profileLastSeenMs(profile);
  return !lastSeen || now - lastSeen <= PROFILE_TTL_MS;
}

function usableProfile(profile, number, minSeen, now = Date.now()) {
  return Boolean(profile
    && profileFreshEnough(profile, now)
    && profileSeenCount(profile) >= minSeen
    && isUsableProfileName(profile.name, number));
}

function newestProfile(a, b) {
  return profileLastSeenMs(b) - profileLastSeenMs(a);
}

function lapTimeMs(value) {
  if (value == null || value === "") return null;
  if (typeof value === "number") return Number.isFinite(value) && value > 0 ? value * 1000 : null;
  const text = String(value).trim();
  if (!text || /^(-|n\/a|na)$/i.test(text)) return null;
  const numeric = Number(text);
  if (Number.isFinite(numeric) && numeric > 0) return numeric * 1000;
  const parts = text.split(":").map(part => part.trim());
  if (parts.some(part => part === "" || Number.isNaN(Number(part)))) return null;
  let seconds = 0;
  if (parts.length === 3) seconds = Number(parts[0]) * 3600 + Number(parts[1]) * 60 + Number(parts[2]);
  else if (parts.length === 2) seconds = Number(parts[0]) * 60 + Number(parts[1]);
  else if (parts.length === 1) seconds = Number(parts[0]);
  else return null;
  return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : null;
}

function formatDuration(ms) {
  if (!Number.isFinite(ms) || ms <= 0) return "";
  const totalSeconds = ms / 1000;
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds - minutes * 60;
  if (minutes > 0) return `${minutes}:${seconds.toFixed(3).padStart(6, "0")}`;
  return seconds.toFixed(3);
}

function driverNumber(driver = {}) {
  return cleanText(driver.displayNumber || driver.number || driver.carNumber);
}

function driverName(driver = {}) {
  const joined = [driver.firstName, driver.lastName].map(cleanText).filter(Boolean).join(" ");
  return normalizeDriverName(driver.name || driver.fallbackName || joined);
}

function driverKey(driver = {}) {
  return `${driverNumber(driver)}|${normalizeKey(driverName(driver))}`;
}

function hasLetters(value) {
  return /[a-z]/i.test(cleanText(value));
}

function isGeneratedName(name, number) {
  const text = cleanText(name);
  const numberText = cleanText(number);
  if (!text) return true;
  if (/^(unknown|n\/a|na|-|--|driver)$/i.test(text)) return true;
  if (/^car\s+/i.test(text)) return true;
  if (numberText && normalizeKey(text) === normalizeKey(numberText)) return true;
  return false;
}

function isBrokenName(name, number) {
  const text = cleanText(name);
  if (isGeneratedName(text, number)) return true;
  if (!hasLetters(text)) return true;
  const letterCount = text.replace(/[^a-z]/gi, "").length;
  if (letterCount <= 2) return true;
  if (/^[A-Z]\.?\s*\d+[A-Z]?$/i.test(text)) return true;
  return false;
}

function isUsableProfileName(name, number) {
  const text = cleanText(name);
  if (isBrokenName(text, number)) return false;
  const letterCount = text.replace(/[^a-z]/gi, "").length;
  return letterCount >= 3;
}

function parseProfileName(name) {
  const text = normalizeDriverName(name);
  const parts = text.split(/\s+/).filter(Boolean);
  if (parts.length < 2) return { firstName: "", lastName: text };
  return {
    firstName: parts[0],
    lastName: parts.slice(1).join(" ")
  };
}

function cloneDriver(driver) {
  return driver && typeof driver === "object" ? { ...driver } : driver;
}

function cloneList(list) {
  return Array.isArray(list) ? list.map(cloneDriver) : [];
}

function eventKey(view = {}) {
  const meta = view.meta || {};
  const key = [
    meta.source,
    meta.trackName,
    meta.className,
    meta.session,
    meta.mode
  ].map(normalizeKey).filter(Boolean).join("|");
  return key || "unknown";
}

function modeFromView(view = {}) {
  const meta = view.meta || {};
  const text = `${meta.mode || ""} ${meta.session || ""} ${meta.className || ""}`.toLowerCase();
  if (/\b(qual\w*|practice|hot\s*laps?|hotlaps|time trials?|time trial|warm\s*up|best lap|competitors)\b/.test(text)) return "qualifying";
  if (/\b(race|feature|heat|main|dash|semi|consi|final|b-main|a-main)\b/.test(text)) return "race";
  if (Array.isArray(view.qualifyingOrder) && view.qualifyingOrder.length && !view.raceOrder?.length) return "qualifying";
  if (Array.isArray(view.raceOrder) && view.raceOrder.length) return "race";
  return "auto";
}

function rowSource(view = {}) {
  return Array.isArray(view.runningOrder) && view.runningOrder.length
    ? view.runningOrder
    : Array.isArray(view.raceOrder) && view.raceOrder.length
      ? view.raceOrder
      : Array.isArray(view.qualifyingOrder) && view.qualifyingOrder.length
        ? view.qualifyingOrder
        : Array.isArray(view.competitors) ? view.competitors : [];
}

function hasTimedData(driver = {}) {
  const laps = Number(driver.laps);
  return (Number.isFinite(laps) && laps > 0) || lapTimeMs(driver.lastLap) || lapTimeMs(driver.bestLap);
}

function deriveRaceState(view = {}, options = {}) {
  const meta = view.meta || {};
  const rows = rowSource(view);
  const driverCount = rows.filter(driver => driverNumber(driver) || driverName(driver)).length;
  const mode = modeFromView(view);
  const lastUpdatedMs = new Date(meta.lastUpdated || meta.live?.lastPoll || "").getTime();
  const timingAgeSeconds = Number.isFinite(lastUpdatedMs) ? Math.max(0, Math.round((Date.now() - lastUpdatedMs) / 1000)) : null;
  const isStale = timingAgeSeconds == null || timingAgeSeconds > (options.staleAfterMs || STALE_AFTER_MS) / 1000;
  const hasTimedLaps = rows.some(hasTimedData);
  const flagText = cleanText(meta.flagLabel || meta.flag || meta.flagCode).toLowerCase();
  // Missing laps-to-go must stay unknown: Number("") is 0, and reading an
  // absent value as "0 to go" declared time-based races finished while live.
  const lapsToGoText = cleanText(meta.lapsToGo).replace(/[^\d.-]/g, "");
  const lapsToGoNumber = lapsToGoText ? Number(lapsToGoText) : NaN;
  const finished = /\b(finish|finished|checkered|complete|complete race)\b/.test(flagText)
    || (Number.isFinite(lapsToGoNumber) && lapsToGoNumber === 0 && driverCount > 0 && hasTimedLaps);
  let phase = "no-data";
  let label = "No driver data";
  if (driverCount && mode === "qualifying") {
    phase = hasTimedLaps ? "qualifying" : "pre-race";
    label = hasTimedLaps ? "Qualifying live" : "Qualifying field loaded";
  } else if (driverCount && finished) {
    phase = "finished";
    label = "Race finished";
  } else if (driverCount && mode === "race" && hasTimedLaps) {
    phase = "live";
    label = "Race live";
  } else if (driverCount) {
    phase = "pre-race";
    label = "Field loaded";
  }
  if (driverCount && isStale && phase === "live") {
    phase = "stale";
    label = "Timing may be stale";
  }
  if (options.heldLastGood) label = `${label} (held safe)`;
  return {
    phase,
    label,
    mode,
    source: cleanText(meta.source || meta.live?.provider || "none"),
    timingAgeSeconds,
    hasDrivers: driverCount > 0,
    driverCount,
    hasTimedLaps,
    isStale,
    heldLastGood: Boolean(options.heldLastGood),
    canUseTiming: driverCount > 0 && !isStale,
    shouldHoldLastGood: driverCount === 0 || isStale
  };
}

function profileStoreTemplate() {
  return {
    version: 1,
    updatedAt: "",
    profiles: {}
  };
}

function loadProfileStore(profilePath) {
  if (!profilePath) return profileStoreTemplate();
  try {
    const parsed = JSON.parse(fs.readFileSync(profilePath, "utf8"));
    if (parsed && typeof parsed === "object" && parsed.profiles && typeof parsed.profiles === "object") {
      return { version: 1, updatedAt: "", ...parsed };
    }
  } catch (_err) {
    // A missing driver profile file is expected on a fresh install.
  }
  return profileStoreTemplate();
}

function findProfile(store, className, number) {
  const numKey = numberKey(number);
  if (!numKey) return null;
  const now = Date.now();
  const classKey = normalizeKey(className);
  const exact = store.profiles[profileKey(className, number)];
  if (usableProfile(exact, number, EXACT_PROFILE_MIN_SEEN, now)) return exact;

  const profiles = Object.values(store.profiles);
  const classMatches = classKey
    ? profiles
      .filter(profile => normalizeKey(profile.className) === classKey)
      .filter(profile => numberKey(profile.number) === numKey && usableProfile(profile, number, EXACT_PROFILE_MIN_SEEN, now))
      .sort(newestProfile)
    : [];
  const classNames = new Set(classMatches.map(profile => normalizeKey(profile.name)).filter(Boolean));
  if (classNames.size === 1) return classMatches[0];

  const matches = profiles
    .filter(profile => numberKey(profile.number) === numKey && usableProfile(profile, number, FALLBACK_PROFILE_MIN_SEEN, now))
    .sort(newestProfile);
  const uniqueNames = new Set(matches.map(profile => normalizeKey(profile.name)).filter(Boolean));
  return uniqueNames.size === 1 ? matches[0] : null;
}

function pruneProfileStore(store, now = Date.now()) {
  const profiles = store && store.profiles && typeof store.profiles === "object" ? store.profiles : {};
  let removed = 0;
  Object.keys(profiles).forEach(key => {
    if (!profileFreshEnough(profiles[key], now)) {
      delete profiles[key];
      removed += 1;
    }
  });
  return removed;
}

function makeStoryItem(type, title, detail, tone = "info") {
  return {
    type,
    title,
    detail,
    tone
  };
}

class RaceIntelligence {
  constructor(options = {}) {
    this.profilePath = options.profilePath || "";
    this.profileStore = loadProfileStore(this.profilePath);
    this.profileDirty = pruneProfileStore(this.profileStore) > 0;
    this.lastProfileSave = 0;
    this.lastGoodByEvent = new Map();
    this.sessionHistory = new Map();
    this.lastSaveError = "";
    this.saveProfiles(true);
  }

  saveProfiles(force = false) {
    if (!this.profileDirty || !this.profilePath) return;
    const now = Date.now();
    if (!force && now - this.lastProfileSave < PROFILE_SAVE_INTERVAL_MS) return;
    try {
      fs.mkdirSync(path.dirname(this.profilePath), { recursive: true });
      this.profileStore.updatedAt = new Date().toISOString();
      // Temp file + rename keeps the driver database intact if the process dies mid-write.
      const tempPath = `${this.profilePath}.tmp-${process.pid}`;
      fs.writeFileSync(tempPath, `${JSON.stringify(this.profileStore, null, 2)}\n`);
      fs.renameSync(tempPath, this.profilePath);
      this.profileDirty = false;
      this.lastProfileSave = now;
      this.lastSaveError = "";
    } catch (err) {
      this.lastSaveError = err.message || String(err);
    }
  }

  learnFromView(view = {}) {
    const meta = view.meta || {};
    const learned = new Set();
    let learnedThisRun = 0;
    const allDrivers = [];
    LIST_KEYS.forEach(key => {
      if (Array.isArray(view[key])) allDrivers.push(...view[key]);
    });
    if (view.leader) allDrivers.push(view.leader);
    allDrivers.forEach(driver => {
      if (!driver || typeof driver !== "object") return;
      const number = driverNumber(driver);
      const name = driverName(driver);
      if (!number || !isUsableProfileName(name, number)) return;
      const className = cleanText(driver.className || meta.className);
      const key = profileKey(className, number);
      if (learned.has(key)) return;
      learned.add(key);
      const existing = this.profileStore.profiles[key] || {};
      const parsedName = parseProfileName(name);
      // Timing broadcasts arrive many times per second; only count a new sighting
      // (and touch lastSeen) every SEEN_COUNT_INTERVAL_MS so seenCount approximates
      // distinct appearances instead of broadcast frames.
      const lastSeenMs = profileLastSeenMs(existing);
      const seenRecently = lastSeenMs && Date.now() - lastSeenMs < SEEN_COUNT_INTERVAL_MS;
      const next = {
        ...existing,
        number,
        name,
        firstName: normalizeDriverName(driver.firstName) || existing.firstName || parsedName.firstName,
        lastName: normalizeDriverName(driver.lastName) || existing.lastName || parsedName.lastName,
        hometown: cleanHometown(driver.hometown) || existing.hometown || "",
        className: className || existing.className || "",
        firstSeen: existing.firstSeen || new Date().toISOString(),
        lastSeen: seenRecently ? existing.lastSeen : new Date().toISOString(),
        seenCount: seenRecently ? profileSeenCount(existing) : profileSeenCount(existing) + 1,
        sources: Array.from(new Set([...(existing.sources || []), cleanText(meta.source || meta.live?.provider || "timing")].filter(Boolean))).slice(0, 8)
      };
      if (JSON.stringify(existing) !== JSON.stringify(next)) {
        this.profileStore.profiles[key] = next;
        this.profileDirty = true;
        learnedThisRun += existing.name ? 0 : 1;
      }
    });
    this.saveProfiles(false);
    return learnedThisRun;
  }

  applyProfiles(view = {}) {
    const meta = view.meta || {};
    // Cache the profile lookups, never the enhanced driver copies: the same
    // driver appears in several lists with list-specific computed fields
    // (interval, gap, adjusted position), and a copy cached from one list
    // used to wipe those fields from every other list.
    const profileCache = new Map();
    const fallbackCounted = new Set();
    let fallbacksApplied = 0;
    const enhanceDriver = driver => {
      if (!driver || typeof driver !== "object") return driver;
      const copy = cloneDriver(driver);
      const number = driverNumber(copy);
      const currentName = driverName(copy);
      const lookupKey = `${copy.className || meta.className || ""}|${number}`;
      let profile = profileCache.get(lookupKey);
      if (profile === undefined) {
        profile = findProfile(this.profileStore, copy.className || meta.className, number) || null;
        profileCache.set(lookupKey, profile);
      }
      if (profile) {
        if (isBrokenName(currentName, number)) {
          copy.name = profile.name;
          copy.fallbackName = profile.name;
          copy.firstName = copy.firstName || profile.firstName || "";
          copy.lastName = copy.lastName || profile.lastName || "";
          copy.driverProfileApplied = true;
          if (!fallbackCounted.has(lookupKey)) {
            fallbackCounted.add(lookupKey);
            fallbacksApplied += 1;
          }
        }
        if (!copy.hometown && profile.hometown) copy.hometown = profile.hometown;
      }
      return copy;
    };
    const next = { ...view };
    LIST_KEYS.forEach(key => {
      if (Array.isArray(view[key])) next[key] = view[key].map(enhanceDriver);
    });
    next.leader = view.leader ? enhanceDriver(view.leader) : view.leader;
    return { view: next, fallbacksApplied };
  }

  applySafeHold(view = {}) {
    const key = eventKey(view);
    const hasStableEvent = key !== "unknown";
    const rows = rowSource(view);
    const usableRows = rows.filter(driver => driverNumber(driver) || driverName(driver));
    if (hasStableEvent && usableRows.length) {
      this.lastGoodByEvent.set(key, {
        at: Date.now(),
        view: LIST_KEYS.reduce((memo, listKey) => {
          memo[listKey] = cloneList(view[listKey]);
          return memo;
        }, { leader: cloneDriver(view.leader) })
      });
      return { view, heldLastGood: false };
    }
    const held = hasStableEvent ? this.lastGoodByEvent.get(key) : null;
    if (held && Date.now() - held.at < HOLD_LAST_GOOD_MS) {
      const next = { ...view };
      LIST_KEYS.forEach(listKey => {
        next[listKey] = cloneList(held.view[listKey]);
      });
      next.leader = cloneDriver(held.view.leader);
      return { view: next, heldLastGood: true };
    }
    return { view, heldLastGood: false };
  }

  sessionFor(view = {}) {
    const key = eventKey(view);
    if (!this.sessionHistory.has(key)) {
      this.sessionHistory.set(key, {
        initialPositions: new Map(),
        lastLeaderKey: "",
        leaderChanges: 0,
        battleStreaks: new Map(),
        lapsLed: new Map(),
        lastLeaderLap: null
      });
      if (this.sessionHistory.size > 20) {
        const first = this.sessionHistory.keys().next().value;
        this.sessionHistory.delete(first);
      }
    }
    return this.sessionHistory.get(key);
  }

  buildStory(view = {}, raceState = {}) {
    const rows = rowSource(view);
    const session = this.sessionFor(view);
    rows.forEach(driver => {
      const key = driverKey(driver);
      const position = Number(driver.position);
      if (key && Number.isFinite(position) && position > 0 && !session.initialPositions.has(key)) {
        session.initialPositions.set(key, position);
      }
    });
    const items = [];
    const leader = rows[0] || view.leader || null;
    const leaderKey = leader ? driverKey(leader) : "";
    if (leader && session.lastLeaderKey && leaderKey && session.lastLeaderKey !== leaderKey && raceState.phase === "live") {
      session.leaderChanges += 1;
      items.push(makeStoryItem("lead-change", "New leader", `${driverName(leader)} moved to the front`, "live"));
    }
    if (leaderKey) session.lastLeaderKey = leaderKey;

    // Laps led: attribute new leader laps to whoever holds P1 when the lap
    // count advances. buildStory runs several times per timing tick, so only
    // a lap-count change counts - repeat calls with the same laps are no-ops.
    if (!session.lapsLed) session.lapsLed = new Map();
    const leaderLaps = Number(leader && leader.laps);
    if (leaderKey && Number.isFinite(leaderLaps) && raceState.mode !== "qualifying") {
      if (session.lastLeaderLap == null) {
        session.lastLeaderLap = leaderLaps;
      } else if (leaderLaps !== session.lastLeaderLap) {
        const gained = Math.max(0, Math.min(5, leaderLaps - session.lastLeaderLap));
        if (gained > 0) session.lapsLed.set(leaderKey, (session.lapsLed.get(leaderKey) || 0) + gained);
        session.lastLeaderLap = leaderLaps;
      }
    }
    let lapsLedTop = null;
    session.lapsLed.forEach((count, key) => {
      if (!lapsLedTop || count > lapsLedTop.count) lapsLedTop = { key, count };
    });
    const lapsLedDriver = lapsLedTop && lapsLedTop.count >= 2
      ? rows.find(driver => driverKey(driver) === lapsLedTop.key) || null
      : null;

    // Battle watch: track how long each adjacent pair has been nose-to-tail;
    // pairs that separate (or the field reordering) drop out of the map.
    let battle = null;
    if (raceState.phase === "live" && raceState.mode !== "qualifying") {
      const previousStreaks = session.battleStreaks || new Map();
      const nextStreaks = new Map();
      const now = Date.now();
      for (let index = 1; index < rows.length; index += 1) {
        const ahead = rows[index - 1];
        const behind = rows[index];
        const aheadKey = driverKey(ahead);
        const behindKey = driverKey(behind);
        if (!aheadKey || !behindKey) continue;
        const pairKey = `${aheadKey}|${behindKey}`;
        const value = cleanText(behind.interval || (index === 1 ? behind.gap : ""));
        const ms = lapTimeMs(value);
        const close = Number.isFinite(ms) && ms > 0 && ms <= BATTLE_GAP_MS;
        const previous = previousStreaks.get(pairKey);
        if (close) {
          nextStreaks.set(pairKey, {
            count: (previous ? previous.count : 0) + 1,
            firstAt: previous ? previous.firstAt : now,
            lastCloseAt: now,
            ms,
            value
          });
        } else if (previous && now - (previous.lastCloseAt || previous.firstAt) < BATTLE_GRACE_MS) {
          nextStreaks.set(pairKey, previous);
        }
        const active = nextStreaks.get(pairKey);
        if (active && active.count >= BATTLE_MIN_UPDATES && now - active.firstAt >= BATTLE_MIN_DURATION_MS
          && Number.isFinite(active.ms)) {
          const position = Number(ahead.position) || index;
          if (!battle || active.ms < battle.ms) {
            battle = { ahead, behind, ms: active.ms, value: active.value, position };
          }
        }
      }
      session.battleStreaks = nextStreaks;
    } else if (session.battleStreaks && session.battleStreaks.size) {
      session.battleStreaks = new Map();
    }
    if (battle) {
      const aheadNumber = driverNumber(battle.ahead);
      const behindNumber = driverNumber(battle.behind);
      items.push({
        ...makeStoryItem(
          "battle",
          battle.position === 1 ? "Battle for the lead" : `Battle for P${battle.position}`,
          `${aheadNumber ? `#${aheadNumber} ` : ""}${driverName(battle.ahead)} vs ${behindNumber ? `#${behindNumber} ` : ""}${driverName(battle.behind)}, ${battle.value} apart`,
          "live"
        ),
        // Machine-readable pair so the graphics can mark both battling rows.
        aheadNumber,
        behindNumber
      });
    }

    const timed = rows
      .map(driver => ({ driver, ms: lapTimeMs(driver.bestLap) }))
      .filter(item => Number.isFinite(item.ms));
    const fastestLap = timed.sort((a, b) => a.ms - b.ms)[0] || null;
    if (fastestLap) {
      items.push(makeStoryItem("fastest-lap", "Fastest lap", `${driverName(fastestLap.driver)} ${formatDuration(fastestLap.ms)}`, "info"));
    }

    const closestBattle = battle ? null : rows
      .slice(1)
      .map(driver => {
        const value = cleanText(driver.interval || driver.gap);
        const seconds = lapTimeMs(value);
        return { driver, seconds, value };
      })
      .filter(item => Number.isFinite(item.seconds) && item.seconds > 0 && item.seconds < 10000)
      .sort((a, b) => a.seconds - b.seconds)[0] || null;
    if (closestBattle) {
      items.push(makeStoryItem("closest-battle", "Closest battle", `${driverName(closestBattle.driver)} is ${closestBattle.value} back`, "info"));
    }

    const biggestMover = rows
      .map(driver => {
        const key = driverKey(driver);
        const start = session.initialPositions.get(key);
        const current = Number(driver.position);
        const gained = Number.isFinite(start) && Number.isFinite(current) ? start - current : 0;
        return { driver, gained, start, current };
      })
      .filter(item => item.gained > 0)
      .sort((a, b) => b.gained - a.gained)[0] || null;
    if (biggestMover) {
      items.push(makeStoryItem("biggest-mover", "Biggest mover", `${driverName(biggestMover.driver)} +${biggestMover.gained}`, "live"));
    }

    if (raceState.phase === "finished" && leader) {
      items.unshift(makeStoryItem("winner", "Winner", driverName(leader), "live"));
    } else if (raceState.mode === "qualifying" && fastestLap) {
      items.unshift(makeStoryItem("top-qualifier", "Top qualifier", `${driverName(fastestLap.driver)} ${formatDuration(fastestLap.ms)}`, "live"));
    }

    return {
      primary: items[0] || null,
      items: items.slice(0, 6),
      leader: leader ? {
        name: driverName(leader),
        number: driverNumber(leader),
        position: leader.position || 1
      } : null,
      fastestLap: fastestLap ? {
        name: driverName(fastestLap.driver),
        number: driverNumber(fastestLap.driver),
        time: formatDuration(fastestLap.ms)
      } : null,
      closestBattle: closestBattle ? {
        name: driverName(closestBattle.driver),
        number: driverNumber(closestBattle.driver),
        gap: closestBattle.value
      } : null,
      biggestMover: biggestMover ? {
        name: driverName(biggestMover.driver),
        number: driverNumber(biggestMover.driver),
        gained: biggestMover.gained
      } : null,
      lapsLed: lapsLedDriver ? {
        name: driverName(lapsLedDriver),
        number: driverNumber(lapsLedDriver),
        laps: lapsLedTop.count
      } : null,
      updatedAt: new Date().toISOString()
    };
  }

  enhance(view = {}) {
    const learnedThisRun = this.learnFromView(view);
    const profiled = this.applyProfiles(view);
    const held = this.applySafeHold(profiled.view);
    const raceState = deriveRaceState(held.view, { heldLastGood: held.heldLastGood });
    const raceStory = this.buildStory(held.view, raceState);
    return {
      ...held.view,
      raceState,
      raceStory,
      driverDatabase: {
        profileCount: Object.keys(this.profileStore.profiles || {}).length,
        learnedThisRun,
        fallbacksApplied: profiled.fallbacksApplied,
        profilePath: this.profilePath,
        saveError: this.lastSaveError
      }
    };
  }

  profiles() {
    return {
      ...this.profileStore,
      profilePath: this.profilePath,
      saveError: this.lastSaveError
    };
  }
}

function createRaceIntelligence(options = {}) {
  return new RaceIntelligence(options);
}

module.exports = {
  createRaceIntelligence,
  deriveRaceState,
  lapTimeMs,
  formatDuration
};
