const http = require("http");
const https = require("https");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { spawn } = require("child_process");
const { createHttpClient } = require("./src/httpClient");
const { loadProductConfig, createLicenseManager } = require("./src/licenseManager");
const { splitRaceMonitorCommands, parseRaceMonitorLine, applyRaceMonitorCommand, buildViews } = require("./src/raceMonitor");
const { parseTickerCsv, normalizeTickerRows } = require("./src/myRacePass");
const { createRaceIntelligence } = require("./src/raceIntelligence");
const WebSocketRuntime = typeof WebSocket === "function" ? WebSocket : require("ws");

const PORT = Number(process.env.PORT || 4000);
const HOST = process.env.HOST || "0.0.0.0";
const PUBLIC_DIR = path.join(__dirname, "public");
const PACKAGE_UPLOAD_DIR = path.join(__dirname, "..", "Logos and Images");
// Inside the Mac app bundle the user-visible logos folder sits beside the .app;
// the bundle check keeps this path inert in the Windows package.
const APP_BUNDLE_UPLOAD_DIR = __dirname.includes("Liberty Raceworks Pro.app")
  ? path.join(__dirname, "..", "..", "..", "..", "Logos and Images")
  : "";
const LOCAL_UPLOAD_DIR = path.join(__dirname, "Logos and Images");

function resolveUploadDir() {
  if (process.env.LIBERTY_LOGO_DIR) return process.env.LIBERTY_LOGO_DIR;
  if (APP_BUNDLE_UPLOAD_DIR && fs.existsSync(APP_BUNDLE_UPLOAD_DIR)) return APP_BUNDLE_UPLOAD_DIR;
  if (fs.existsSync(PACKAGE_UPLOAD_DIR)) return PACKAGE_UPLOAD_DIR;
  return LOCAL_UPLOAD_DIR;
}

const UPLOAD_DIR = resolveUploadDir();
const LEGACY_UPLOAD_DIR = path.join(PUBLIC_DIR, "uploads");
// "Logos and Images" is for the customer's logos/photos only. All program working
// files (settings, presets, logs, license, driver data, support reports) live in a
// sibling "Program Resources" folder so the image folder stays clean.
const RESOURCES_DIR = process.env.LIBERTY_RESOURCES_DIR || path.join(path.dirname(UPLOAD_DIR), "Program Resources");

// ---- Commercial distribution: product config, licensing, support ----------
const PRODUCT_CONFIG = loadProductConfig(__dirname);
const APP_NAME = process.env.APP_NAME || PRODUCT_CONFIG.appName || "Liberty Raceworks Pro";
// Customer-facing edition name; the site sells "Core", not "Standard".
const EDITION_LABEL = PRODUCT_CONFIG.editionLabel || (PRODUCT_CONFIG.tier === "pro" ? "Pro" : "Core");
const SUPPORT_DIR = path.join(RESOURCES_DIR, "Support Reports");
const SUPPORT_LOG_FILE = path.join(SUPPORT_DIR, "support-log.jsonl");
const SUPPORT_WEBHOOK_URL = String(process.env.SUPPORT_WEBHOOK_URL || "").trim();
const SUPPORT_ALERT_LEVELS = new Set(
  String(process.env.SUPPORT_ALERT_LEVELS || "error")
    .split(",")
    .map(item => item.trim().toLowerCase())
    .filter(Boolean)
);
const SUPPORT_ALERT_COOLDOWN_MS = Number(process.env.SUPPORT_ALERT_COOLDOWN_MS || 60000);

const ALLOWED_PROGRAM_GRAPHICS = new Set(PRODUCT_CONFIG.allowedGraphics || []);
const ALL_BANNER_TEMPLATES = ["classic", "reference-premium", "light-theme", "redline-pro", "steel-night", "silverline-c"];
const ALLOWED_BANNER_TEMPLATES = (() => {
  const fromConfig = Array.isArray(PRODUCT_CONFIG.allowedBannerTemplates)
    ? PRODUCT_CONFIG.allowedBannerTemplates.filter(value => ALL_BANNER_TEMPLATES.includes(value))
    : [];
  return fromConfig.length ? fromConfig : [...ALL_BANNER_TEMPLATES];
})();
const GRAPHIC_BY_PAGE = new Map([
  ["/board.html", "lineup"],
  ["/current-leaders.html", "leaders"],
  ["/results.html", "top5"],
  ["/top3-podium.html", "podium"],
  ["/top3-podium-premium.html", "podium"],
  ["/weather.html", "weather"],
  ["/ticker.html", "ticker"],
  ["/story-spotlight.html", "story"],
  ["/story-examples.html", "story"],
  ["/top-banner.html", "topbanner"],
  ["/side-banner.html", "sidebanner"],
  ["/lineup.html", "lineup"],
  ["/running.html", "running"],
  ["/lower-third.html", "story"]
]);

function normalizeProgramGraphic(graphic) {
  const key = String(graphic || "blank").trim().toLowerCase();
  if (key === "running") return "lineup";
  if (key === "top3" || key === "top-3") return "podium";
  return key || "blank";
}

function graphicAllowed(graphic) {
  return ALLOWED_PROGRAM_GRAPHICS.has(normalizeProgramGraphic(graphic));
}

function pageAllowed(pathname) {
  const graphic = GRAPHIC_BY_PAGE.get(pathname);
  return !graphic || graphicAllowed(graphic);
}

const LICENSE_OPEN_GET_PATHS = new Set([
  "/license.html",
  "/api/health",
  "/api/network",
  "/api/product",
  "/api/license"
]);
const LICENSE_OPEN_POST_PATHS = new Set([
  "/api/client-error",
  "/api/license/trial",
  "/api/license/activate",
  "/api/license/refresh",
  "/api/license/deactivate"
]);
const LICENSE_OPEN_STATIC_PREFIXES = ["/assets/", "/fonts/"];

function licenseState() {
  return licenseManager.publicState();
}

function licenseAllowsProgram(state = licenseState()) {
  return state.licenseRequired === false || Boolean(state.active);
}

function licenseMessage(state = licenseState()) {
  if (state.active) return "License is active.";
  if (state.status === "expired") return "Your trial or license has expired.";
  if (state.status === "check-in-required") return "This license needs to check in online before the program can run.";
  if (state.status === "pending") return state.message || "This product key has not been verified yet.";
  if (state.status === "trial") return "The trial period is no longer current.";
  return state.message || "Start a trial or enter a valid product key to run the program.";
}

function licenseRequestOpen(req, url) {
  if (req.method === "GET" && LICENSE_OPEN_GET_PATHS.has(url.pathname)) return true;
  if (req.method === "POST" && LICENSE_OPEN_POST_PATHS.has(url.pathname)) return true;
  if (req.method === "GET" && LICENSE_OPEN_STATIC_PREFIXES.some(prefix => url.pathname.startsWith(prefix))) return true;
  return false;
}
// ---- end commercial distribution config ------------------------------------

const SERVER_STARTED_AT = new Date();
const REQUEST_BODY_LIMIT_BYTES = 100_000_000;
const REQUEST_BODY_TIMEOUT_MS = 15000;
const FETCH_TIMEOUT_MS = 8000;
const STYLE_PROFILE_VERSION = "1";
const STYLE_PROFILE_ALIASES = new Set([STYLE_PROFILE_VERSION, "2026-05-16"]);
const STYLE_STATE_FILE = path.join(RESOURCES_DIR, "style-state.json");
const STYLE_PRESETS_FILE = path.join(RESOURCES_DIR, "style-presets.json");
const RUNTIME_STATE_FILE = path.join(os.tmpdir(), `liberty-raceworks-pro-runtime-state-${PORT}.json`);
const RECENT_RUN_MARKER_FILE = path.join(RESOURCES_DIR, ".last-run");
const RECENT_RUN_MAX_AGE_MS = 6 * 60 * 60 * 1000;
const LOG_DIR = path.join(RESOURCES_DIR, "logs");
const LOG_RETENTION_DAYS = 14;
let logFileWarned = false;
const RACE_MONITOR_RECONNECT_INITIAL_MS = 3000;
const RACE_MONITOR_RECONNECT_MAX_MS = 30000;
const RACE_MONITOR_STALE_SOCKET_MS = 90000;
const RACE_MONITOR_WATCHDOG_INTERVAL_MS = 15000;
const BROADCAST_COALESCE_MS = 80;
const SSE_HEARTBEAT_MS = 25000;
const { fetchJson, fetchText, fetchHttpText } = createHttpClient({ timeoutMs: FETCH_TIMEOUT_MS });
const SAT_LOGO_IMAGE = process.env.LIBERTY_SAT_LOGO_PATH || "/tmp/sports-action-tv-logo.png";
const DEFAULT_REFERENCE_IMAGE = path.join(PUBLIC_DIR, "assets", "liberty-raceworks-logo.png");
const FONT_FILES = {
  "/fonts/Teko-SemiBold.ttf": [path.join(PUBLIC_DIR, "fonts", "Teko-SemiBold.ttf")],
  "/fonts/Teko-Bold.ttf": [path.join(PUBLIC_DIR, "fonts", "Teko-Bold.ttf")],
  "/fonts/BarlowCondensed-SemiBold.ttf": [path.join(PUBLIC_DIR, "fonts", "BarlowCondensed-SemiBold.ttf")],
  "/fonts/BarlowCondensed-Bold.ttf": [path.join(PUBLIC_DIR, "fonts", "BarlowCondensed-Bold.ttf")],
  "/fonts/BarlowCondensed-BlackItalic.ttf": [path.join(PUBLIC_DIR, "fonts", "BarlowCondensed-BlackItalic.ttf")],
  "/fonts/AirstrikeBold3D.otf": [path.join(PUBLIC_DIR, "fonts", "AirstrikeBold3D.otf")],
  "/fonts/Barber-Complete.woff": [path.join(PUBLIC_DIR, "fonts", "Barber-Complete.woff")],
  "/fonts/BuiltTitling-Bold.otf": [path.join(PUBLIC_DIR, "fonts", "BuiltTitling-Bold.otf")],
  "/fonts/BuiltTitling-BoldItalic.otf": [path.join(PUBLIC_DIR, "fonts", "BuiltTitling-BoldItalic.otf")]
};

const timingState = {
  source: "none",
  raw: [],
  classes: {},
  competitors: new Map(),
  roster: new Map(),
  session: "",
  className: "",
  trackName: "",
  trackLength: "",
  weatherLocation: "",
  mode: "",
  clock: "",
  lapsToGo: "",
  raceTime: "",
  timeToGo: "",
  flag: "",
  flagCode: "",
  flagLabel: "",
  lastUpdated: null
};

const clients = new Set();
const programClients = new Set();
const topBannerClients = new Set();
const storyOutputClients = new Set();
let programState = {
  graphic: "blank",
  version: Date.now()
};
let topBannerState = {
  visible: true,
  version: Date.now()
};
let storyOutputState = {
  visible: true,
  version: Date.now()
};
const DEFAULT_STYLE_STATE = {
  schemaVersion: 1,
  styleProfileVersion: STYLE_PROFILE_VERSION,
  accentColor: "#e7332f",
  numberColor: "#f4c542",
  nameColor: "#ffffff",
  metricColor: "#cfd4d9",
  graphicBackgroundColor: "#071016",
  graphicPanelColor: "#242929",
  graphicBorderColor: "#050505",
  graphicBadgeColor: "#050505",
  graphicBadgeTextColor: "#ffffff",
  graphicLightTextColor: "#111111",
  graphicLightSubtextColor: "#252525",
  titleSize: 88,
  rowNameSize: 28,
  leaderNameSize: 34,
  boardScale: 0.92,
  boardOffsetY: 72,
  panelOpacity: 0.92,
  competitorNameFormat: "first_initial_last",
  lineupMetricSource: "class",
  footerSource: "class",
  manualTrackName: "",
  manualSession: "",
  bannerDataOpen: true,
  bannerTopFiveOpen: true,
  bannerHeaderLogoVersion: 0,
  bannerSponsorLogoVersion: 0,
  bannerHeaderLogoCount: 0,
  bannerSponsorLogoCount: 0,
  bannerLogoRotateSeconds: 8,
  bannerMetricFields: ["lastLap", "gap"],
  bannerMetricRotateSeconds: 4,
  bannerTemplate: "reference-premium",
  bannerInfoSource: "class",
  bannerManualInfo: "",
  bannerDurationMode: "lapsToGo",
  bannerRaceMode: "auto",
  bannerModeChipOpen: true,
  bannerAccentLinesOpen: true,
  bannerTextureOpen: true,
  bannerReflectionOpen: true,
  bannerBevelOpen: true,
  bannerLogoPanelColor: "#d7dad7",
  bannerDarkCellColor: "#242929",
  bannerLightCellColor: "#e5e9ea",
  bannerFlyoutColor: "#5a6060",
  bannerAccentLineColor: "#e7332f",
  bannerOpacity: 1,
  bannerFlyoutOpacity: 1,
  bannerGlassOpen: true,
  bannerMomentsOpen: true,
  bannerMotionOpen: true,
  bannerNameFormat: "first_initial_last",
  bannerNameSize: 20,
  bannerMetricSize: 17,
  bannerInfoSize: 22,
  bannerDurationSize: 22,
  top3TitleSource: "class",
  top3Title: "Top 3",
  top3Subtitle: "",
  top3Source: "qualifying",
  top3Layout: "photo",
  top3Drivers: [
    { name: "", number: "" },
    { name: "", number: "" },
    { name: "", number: "" }
  ],
  top3PhotoVersions: [0, 0, 0],
  missingPlaceholder: "dash",
  weatherLocationOverride: "",
  tickerSource: "manual",
  tickerText: "",
  tickerSpeedSeconds: 28,
  raceStoryPosition: "bottom-center",
  raceStoryAutoUpdate: true,
  raceStoryHeldTitle: "",
  raceStoryHeldDetail: "",
  raceStoryHeldTone: "",
  raceStoryHeldType: "",
  raceStoryHeldClass: "",
  raceStoryHeldMode: "",
  raceStoryHeldKey: ""
};
const RACE_STORY_RUNTIME_KEYS = [
  "raceStoryHeldTitle",
  "raceStoryHeldDetail",
  "raceStoryHeldTone",
  "raceStoryHeldType",
  "raceStoryHeldClass",
  "raceStoryHeldMode",
  "raceStoryHeldKey"
];
const RACE_STORY_RUNTIME_KEY_SET = new Set(RACE_STORY_RUNTIME_KEYS);
const DEFAULT_RUNTIME_STATE = Object.fromEntries(RACE_STORY_RUNTIME_KEYS.map(key => [key, ""]));

// One-time move: older builds kept settings/logs/license inside "Logos and Images".
// Relocate them into "Program Resources" so the image folder holds only images.
// Best-effort and idempotent: only moves an item missing from the new location.
function migrateLegacyResources() {
  if (UPLOAD_DIR === RESOURCES_DIR) return;
  const items = [
    "style-state.json", "style-presets.json", "driver-profiles.json", ".last-run",
    "logs", "License", "Support Reports"
  ];
  let moved = 0;
  for (const item of items) {
    const from = path.join(UPLOAD_DIR, item);
    const to = path.join(RESOURCES_DIR, item);
    try {
      if (!fs.existsSync(from) || fs.existsSync(to)) continue;
      fs.mkdirSync(RESOURCES_DIR, { recursive: true });
      fs.renameSync(from, to);
      moved += 1;
    } catch (_err) {
      // Leave the original in place if the move fails; never block startup.
    }
  }
  if (moved) console.log(`Moved ${moved} program file(s) from "Logos and Images" into "Program Resources".`);
}
migrateLegacyResources();
prepareLogDir();
let styleState = loadPersistedStyleState();
let runtimeState = loadPersistedRuntimeState();
let liveRaceMonitorSocket = null;
let demoRaceChild = null;
// Connect flows await network calls between closing the old source and
// installing the new one; rapid connect clicks used to leak orphan sockets
// and intervals that kept feeding stale data nothing could disconnect.
// Each connect takes a generation; only the latest may finish.
let raceMonitorConnectGeneration = 0;
let myRacePassConnectGeneration = 0;
let raceMonitorReconnectTimer = null;
let raceMonitorReconnectDelayMs = RACE_MONITOR_RECONNECT_INITIAL_MS;
let liveRaceMonitorUrl = "";
let raceMonitorLastMessageAt = 0;
let raceMonitorWatchdogTimer = null;
let myRacePassInterval = null;
let myRacePassPollInFlight = false;
let liveRaceMonitorState = {
  connected: false,
  connecting: false,
  raceId: "",
  host: "",
  instance: "",
  error: ""
};
let liveMyRacePassState = {
  connected: false,
  connecting: false,
  url: "",
  error: "",
  lastPoll: ""
};
let myRacePassLineupCache = {
  url: "",
  fetchedAt: 0,
  rows: []
};
let weatherCache = {
  key: "",
  fetchedAt: 0,
  data: null
};
const supportAlertLastSent = new Map();
const licenseManager = createLicenseManager({
  uploadDir: RESOURCES_DIR,
  productConfig: PRODUCT_CONFIG,
  fetchTimeoutMs: FETCH_TIMEOUT_MS
});
const raceIntelligence = createRaceIntelligence({
  profilePath: path.join(RESOURCES_DIR, "driver-profiles.json")
});

const PROGRAM_ROUTES = {
  blank: "/program.html",
  lineup: "/board.html?mode=lineup",
  running: "/board.html?mode=running",
  topbanner: "/top-banner.html",
  sidebanner: "/side-banner.html",
  results: "/board.html?mode=results",
  leaders: "/current-leaders.html",
  top5: "/results.html",
  podium: "/top3-podium.html",
  weather: "/weather.html",
  ticker: "/ticker.html",
  story: "/story-spotlight.html"
};
const TOP_BANNER_PRESERVING_PROGRAM_GRAPHICS = new Set(["blank", "leaders", "story", "weather", "topbanner", "sidebanner"]);
// Broadcast output pages get the trial watermark script injected when served.
const TRIAL_WATERMARK_PAGES = new Set(Object.values(PROGRAM_ROUTES).map(route => route.split("?")[0]));

function cloneStyleState(source = DEFAULT_STYLE_STATE) {
  const clone = {
    ...source,
    bannerMetricFields: Array.isArray(source.bannerMetricFields) ? [...source.bannerMetricFields] : [],
    top3Drivers: Array.isArray(source.top3Drivers)
      ? source.top3Drivers.map(driver => ({ ...driver }))
      : DEFAULT_STYLE_STATE.top3Drivers.map(driver => ({ ...driver })),
    top3PhotoVersions: Array.isArray(source.top3PhotoVersions) ? [...source.top3PhotoVersions] : [0, 0, 0]
  };
  if (!ALLOWED_BANNER_TEMPLATES.includes(clone.bannerTemplate)) {
    clone.bannerTemplate = ALLOWED_BANNER_TEMPLATES[0];
  }
  return clone;
}

function logoFileCandidates(side, index) {
  const cleanSide = normalizeBannerSide(side);
  return [
    path.join(UPLOAD_DIR, `banner-${cleanSide}-${index}.png`),
    path.join(UPLOAD_DIR, `banner-${cleanSide}-${index}.jpg`),
    path.join(UPLOAD_DIR, `banner-${cleanSide}-${index}.webp`),
    path.join(LEGACY_UPLOAD_DIR, `banner-${cleanSide}-${index}.png`),
    path.join(LEGACY_UPLOAD_DIR, `banner-${cleanSide}-${index}.jpg`),
    path.join(LEGACY_UPLOAD_DIR, `banner-${cleanSide}-${index}.webp`)
  ];
}

function bannerLogoExists(side, index) {
  return logoFileCandidates(side, index).some(candidate => fs.existsSync(candidate));
}

function availableBannerLogoCount(side) {
  const cleanSide = normalizeBannerSide(side);
  const indices = new Set();
  for (const dir of [UPLOAD_DIR, LEGACY_UPLOAD_DIR]) {
    let files = [];
    try {
      files = fs.readdirSync(dir);
    } catch (err) {
      if (err.code !== "ENOENT") {
        logEvent("warn", "Could not read banner logo directory", { dir, error: errorMessage(err) });
      }
    }
    files.forEach(file => {
      const match = file.match(new RegExp(`^banner-${cleanSide}-(\\d+)\\.(?:png|jpg|webp)$`, "i"));
      if (match) indices.add(Number(match[1]));
    });
  }
  let count = 0;
  while (indices.has(count)) count += 1;
  return count;
}

function reconcileBannerLogoState(next) {
  next.bannerHeaderLogoCount = availableBannerLogoCount("header");
  next.bannerSponsorLogoCount = availableBannerLogoCount("sponsor");
  if (!next.bannerHeaderLogoCount) next.bannerHeaderLogoVersion = 0;
  if (!next.bannerSponsorLogoCount) next.bannerSponsorLogoVersion = 0;
  return next;
}

function migrateStyleState(source = {}) {
  const sourceObject = source && typeof source === "object" && !Array.isArray(source) ? source : {};
  const savedVersion = String(sourceObject.styleProfileVersion || "");
  const useSavedValues = !savedVersion || STYLE_PROFILE_ALIASES.has(savedVersion);
  const next = cloneStyleState(useSavedValues ? { ...DEFAULT_STYLE_STATE, ...sourceObject } : DEFAULT_STYLE_STATE);
  next.styleProfileVersion = STYLE_PROFILE_VERSION;
  if (next.titleSize === 72) next.titleSize = DEFAULT_STYLE_STATE.titleSize;
  if (next.top3Subtitle === "Drivers Rolling Out") next.top3Subtitle = "";
  RACE_STORY_RUNTIME_KEYS.forEach(key => {
    delete next[key];
  });
  return reconcileBannerLogoState(next);
}

function loadPersistedStyleState() {
  try {
    if (!fs.existsSync(STYLE_STATE_FILE)) return cloneStyleState(DEFAULT_STYLE_STATE);
    const parsed = JSON.parse(fs.readFileSync(STYLE_STATE_FILE, "utf8"));
    return migrateStyleState(parsed);
  } catch (err) {
    logEvent("warn", "Could not load saved style state", { error: errorMessage(err) });
    return cloneStyleState(DEFAULT_STYLE_STATE);
  }
}

// Write through a temp file + rename so a crash mid-write cannot corrupt saved state.
async function writeFileAtomic(filePath, contents) {
  const tempPath = `${filePath}.tmp-${process.pid}`;
  await fs.promises.writeFile(tempPath, contents);
  await fs.promises.rename(tempPath, filePath);
}

async function persistStyleState() {
  try {
    await fs.promises.mkdir(RESOURCES_DIR, { recursive: true });
    const persisted = reconcileBannerLogoState(cloneStyleState(styleState));
    RACE_STORY_RUNTIME_KEYS.forEach(key => {
      delete persisted[key];
    });
    await writeFileAtomic(STYLE_STATE_FILE, JSON.stringify(persisted, null, 2));
  } catch (err) {
    logEvent("warn", "Could not save style state", { error: errorMessage(err) });
  }
}

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

function presetIdFromName(name) {
  const slug = cleanPresetName(name)
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 48);
  return slug || `look-${Date.now()}`;
}

function normalizeStylePreset(raw) {
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
  const name = cleanPresetName(raw.name);
  if (!name) return null;
  const id = String(raw.id || presetIdFromName(name)).replace(/[^a-z0-9-]/gi, "").slice(0, 64) || presetIdFromName(name);
  const style = migrateStyleState(raw.style || {});
  return {
    id,
    name,
    savedAt: raw.savedAt || new Date().toISOString(),
    style
  };
}

function publicStylePreset(preset) {
  return {
    id: preset.id,
    name: preset.name,
    savedAt: preset.savedAt,
    summary: {
      template: preset.style.bannerTemplate,
      accentColor: preset.style.bannerAccentLineColor || preset.style.accentColor,
      logoPanelColor: preset.style.bannerLogoPanelColor,
      driverBoxColor: preset.style.bannerDarkCellColor,
      bottomRowColor: preset.style.bannerLightCellColor
    }
  };
}

async function readStylePresets() {
  try {
    const text = await fs.promises.readFile(STYLE_PRESETS_FILE, "utf8");
    const parsed = JSON.parse(text);
    const items = Array.isArray(parsed) ? parsed : parsed.presets;
    return (Array.isArray(items) ? items : [])
      .map(normalizeStylePreset)
      .filter(Boolean)
      .slice(0, 50);
  } catch (err) {
    if (err.code === "ENOENT") return [];
    logEvent("warn", "Could not load style presets", { error: errorMessage(err) });
    return [];
  }
}

async function writeStylePresets(presets) {
  await fs.promises.mkdir(RESOURCES_DIR, { recursive: true });
  await writeFileAtomic(STYLE_PRESETS_FILE, JSON.stringify(presets, null, 2));
}

async function saveStylePreset(name, style, existingId = "") {
  const cleanName = cleanPresetName(name);
  if (!cleanName) throw new Error("Enter a name for this saved look.");
  const presets = await readStylePresets();
  const id = existingId
    ? String(existingId).replace(/[^a-z0-9-]/gi, "").slice(0, 64)
    : presetIdFromName(cleanName);
  const uniqueId = id || presetIdFromName(cleanName);
  const preset = normalizeStylePreset({
    id: uniqueId,
    name: cleanName,
    savedAt: new Date().toISOString(),
    style: migrateStyleState(style || styleState)
  });
  const next = [preset, ...presets.filter(item => item.id !== uniqueId)].slice(0, 50);
  await writeStylePresets(next);
  return { preset, presets: next };
}

function cleanRuntimeState(source = {}) {
  const clean = { ...DEFAULT_RUNTIME_STATE };
  const sourceObject = source && typeof source === "object" && !Array.isArray(source) ? source : {};
  RACE_STORY_RUNTIME_KEYS.forEach(key => {
    clean[key] = cleanStringSetting(sourceObject[key], key === "raceStoryHeldKey" ? 240 : 700);
  });
  return clean;
}

function loadPersistedRuntimeState() {
  try {
    if (fs.existsSync(RUNTIME_STATE_FILE)) {
      return cleanRuntimeState(JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, "utf8")));
    }
    if (fs.existsSync(STYLE_STATE_FILE)) {
      return cleanRuntimeState(JSON.parse(fs.readFileSync(STYLE_STATE_FILE, "utf8")));
    }
  } catch (err) {
    logEvent("warn", "Could not load runtime state", { error: errorMessage(err) });
  }
  return cleanRuntimeState(DEFAULT_RUNTIME_STATE);
}

async function persistRuntimeState() {
  try {
    await writeFileAtomic(RUNTIME_STATE_FILE, JSON.stringify(runtimeState, null, 2));
  } catch (err) {
    logEvent("warn", "Could not save runtime state", { error: errorMessage(err) });
  }
}

function splitStyleRuntimePayload(payload = {}) {
  const stylePayload = {};
  const runtimePayload = {};
  Object.entries(payload || {}).forEach(([key, value]) => {
    if (RACE_STORY_RUNTIME_KEY_SET.has(key)) {
      runtimePayload[key] = value;
    } else {
      stylePayload[key] = value;
    }
  });
  return {
    stylePayload,
    runtimePayload,
    hasRuntimePayload: Object.keys(runtimePayload).length > 0
  };
}

function styleForClient() {
  return { ...reconcileBannerLogoState(cloneStyleState(styleState)), ...runtimeState };
}

function logEvent(level, message, details = {}) {
  const method = level === "error" ? "error" : level === "warn" ? "warn" : "log";
  const safeDetails = Object.fromEntries(
    Object.entries(details || {}).filter(([, value]) => value !== undefined && value !== "")
  );
  const suffix = Object.keys(safeDetails).length ? ` ${JSON.stringify(safeDetails)}` : "";
  const line = `[${new Date().toISOString()}] ${level.toUpperCase()} ${message}${suffix}`;
  console[method](line);
  appendLogFile(line);
  if (level === "warn" || level === "error") {
    recordSupportEvent(level, message, safeDetails);
  }
}

// Mirror console output to a dated log file so race-night problems can still be
// diagnosed after the console window is gone.
function appendLogFile(line) {
  const file = path.join(LOG_DIR, `liberty-raceworks-${new Date().toISOString().slice(0, 10)}.log`);
  fs.promises.appendFile(file, `${line}\n`).catch(err => {
    if (!logFileWarned) {
      logFileWarned = true;
      console.warn(`Could not write log file ${file}: ${errorMessage(err)}`);
    }
  });
}

function prepareLogDir() {
  try {
    fs.mkdirSync(LOG_DIR, { recursive: true });
  } catch (err) {
    logFileWarned = true;
    console.warn(`Could not create log directory ${LOG_DIR}: ${errorMessage(err)}`);
    return;
  }
  fs.promises.readdir(LOG_DIR).then(files => {
    const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
    files.forEach(file => {
      const match = file.match(/^liberty-raceworks-(\d{4}-\d{2}-\d{2})\.log$/);
      if (match && Date.parse(match[1]) < cutoff) {
        fs.promises.rm(path.join(LOG_DIR, file), { force: true }).catch(() => {});
      }
    });
  }).catch(() => {});
}

function errorMessage(err) {
  return err && err.message ? err.message : String(err || "Unknown error");
}

function cleanReportText(value, maxLength = 500) {
  return String(value == null ? "" : value)
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, maxLength);
}

function cleanClientErrorDetails(details) {
  if (!details || typeof details !== "object" || Array.isArray(details)) return {};
  const clean = {};
  Object.entries(details).slice(0, 12).forEach(([key, value]) => {
    const cleanKey = cleanReportText(key, 50);
    if (!cleanKey) return;
    if (typeof value === "number" || typeof value === "boolean") {
      clean[cleanKey] = value;
    } else {
      clean[cleanKey] = cleanReportText(value, 250);
    }
  });
  return clean;
}

function sendApiError(res, status, err, context, extra = {}) {
  const message = errorMessage(err);
  logEvent(status >= 500 ? "error" : "warn", context, { error: message, ...extra });
  sendJson(res, status, { ok: false, error: message });
}

function cleanStringSetting(value, maxLength = 1000) {
  return String(value == null ? "" : value).slice(0, maxLength);
}

function booleanSetting(value, fallback) {
  if (typeof value === "boolean") return value;
  if (value === "true") return true;
  if (value === "false") return false;
  return fallback;
}

function numberSetting(value, fallback, range = {}) {
  const number = Number(value);
  if (!Number.isFinite(number)) return fallback;
  const min = Number.isFinite(range.min) ? range.min : -Infinity;
  const max = Number.isFinite(range.max) ? range.max : Infinity;
  return Math.min(max, Math.max(min, number));
}

const STYLE_NUMBER_RANGES = {
  titleSize: { min: 24, max: 180 },
  rowNameSize: { min: 10, max: 90 },
  leaderNameSize: { min: 10, max: 90 },
  boardScale: { min: 0.35, max: 1.35 },
  boardOffsetY: { min: -300, max: 500 },
  panelOpacity: { min: 0.05, max: 1 },
  bannerHeaderLogoVersion: { min: 0 },
  bannerSponsorLogoVersion: { min: 0 },
  bannerHeaderLogoCount: { min: 0 },
  bannerSponsorLogoCount: { min: 0 },
  bannerLogoRotateSeconds: { min: 2, max: 60 },
  bannerMetricRotateSeconds: { min: 1, max: 60 },
  bannerOpacity: { min: 0.05, max: 1 },
  bannerFlyoutOpacity: { min: 0.05, max: 1 },
  bannerNameSize: { min: 8, max: 60 },
  bannerMetricSize: { min: 8, max: 60 },
  bannerInfoSize: { min: 8, max: 72 },
  bannerDurationSize: { min: 8, max: 72 },
  tickerSpeedSeconds: { min: 5, max: 120 }
};

const STYLE_ENUMS = {
  competitorNameFormat: ["first_initial_last", "full_caps", "full", "last", "first"],
  lineupMetricSource: ["class", "hometown", "lap", "auto"],
  footerSource: ["class", "track", "manual"],
  bannerTemplate: [...ALLOWED_BANNER_TEMPLATES],
  bannerInfoSource: ["track", "class", "manual"],
  bannerDurationMode: ["auto", "lapsToGo", "raceTime", "timeToGo", "none"],
  bannerRaceMode: ["auto", "qualifying", "racing"],
  bannerNameFormat: ["first_initial_last", "full_caps", "full", "last", "first"],
  top3TitleSource: ["class", "manual"],
  top3Source: ["timing", "qualifying", "race", "manual"],
  top3Layout: ["photo", "name"],
  missingPlaceholder: ["dash", "tbd", "blank"],
  tickerSource: ["manual", "raceStory"],
  raceStoryPosition: ["bottom-center", "bottom-left"]
};

const BANNER_METRIC_FIELDS = new Set(["totalLaps", "bestLap", "gap", "lastLap", "interval", "className", "hometown"]);

function sanitizeStylePayload(payload) {
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    throw new Error("Style update must be an object.");
  }
  const clean = {};
  Object.entries(payload).forEach(([key, value]) => {
    if (!(key in DEFAULT_STYLE_STATE)) {
      logEvent("warn", "Ignored unknown style setting", { key });
      return;
    }
    const fallback = DEFAULT_STYLE_STATE[key];
    if (key === "styleProfileVersion") {
      clean.styleProfileVersion = STYLE_PROFILE_VERSION;
      return;
    }
    if (key === "top3Drivers") {
      clean.top3Drivers = (Array.isArray(value) ? value : []).slice(0, 3).map(driver => ({
        name: cleanStringSetting(driver && driver.name, 120),
        number: cleanStringSetting(driver && driver.number, 32)
      }));
      while (clean.top3Drivers.length < 3) clean.top3Drivers.push({ name: "", number: "" });
      return;
    }
    if (key === "top3PhotoVersions") {
      clean.top3PhotoVersions = (Array.isArray(value) ? value : []).slice(0, 3).map(item => numberSetting(item, 0, { min: 0 }));
      while (clean.top3PhotoVersions.length < 3) clean.top3PhotoVersions.push(0);
      return;
    }
    if (key === "bannerMetricFields") {
      clean.bannerMetricFields = [...new Set((Array.isArray(value) ? value : [])
        .map(item => String(item))
        .filter(item => BANNER_METRIC_FIELDS.has(item)))];
      return;
    }
    if (STYLE_ENUMS[key]) {
      const text = String(value || "");
      clean[key] = STYLE_ENUMS[key].includes(text) ? text : fallback;
      return;
    }
    if (typeof fallback === "boolean") {
      clean[key] = booleanSetting(value, fallback);
      return;
    }
    if (typeof fallback === "number") {
      clean[key] = numberSetting(value, fallback, STYLE_NUMBER_RANGES[key] || {});
      return;
    }
    clean[key] = cleanStringSetting(value);
  });
  clean.styleProfileVersion = STYLE_PROFILE_VERSION;
  return clean;
}

function sendJson(res, status, data) {
  const body = JSON.stringify(data);
  res.writeHead(status, {
    "content-type": "application/json",
    "content-length": Buffer.byteLength(body)
  });
  res.end(body);
}

// ---- Commercial distribution: support reporting, license page, lockdown ----
function supportEventPayload(level, message, details = {}) {
  return {
    app: APP_NAME,
    level,
    message,
    details,
    serverTime: new Date().toISOString(),
    startedAt: SERVER_STARTED_AT.toISOString()
  };
}

function recordSupportEvent(level, message, details = {}) {
  const payload = supportEventPayload(level, message, details);
  fs.promises.mkdir(SUPPORT_DIR, { recursive: true })
    .then(() => fs.promises.appendFile(SUPPORT_LOG_FILE, `${JSON.stringify(payload)}\n`))
    .catch(err => {
      console.warn(`[${new Date().toISOString()}] WARN Could not write support log ${err.message}`);
    });
  maybeSendSupportAlert(payload);
}

function maybeSendSupportAlert(payload) {
  if (!SUPPORT_WEBHOOK_URL || !SUPPORT_ALERT_LEVELS.has(payload.level)) return;
  const key = `${payload.level}:${payload.message}`;
  const now = Date.now();
  const lastSent = supportAlertLastSent.get(key) || 0;
  if (now - lastSent < SUPPORT_ALERT_COOLDOWN_MS) return;
  supportAlertLastSent.set(key, now);
  sendSupportAlert(payload).catch(err => {
    console.warn(`[${new Date().toISOString()}] WARN Support alert failed ${err.message}`);
  });
}

function sendSupportAlert(payload) {
  return new Promise((resolve, reject) => {
    if (!SUPPORT_WEBHOOK_URL) {
      reject(new Error("Set SUPPORT_WEBHOOK_URL before sending support alerts."));
      return;
    }
    let target;
    try {
      target = new URL(SUPPORT_WEBHOOK_URL);
    } catch (_err) {
      reject(new Error("SUPPORT_WEBHOOK_URL is not a valid URL."));
      return;
    }
    const isHttps = target.protocol === "https:";
    const isHttp = target.protocol === "http:";
    if (!isHttps && !isHttp) {
      reject(new Error("SUPPORT_WEBHOOK_URL must start with http:// or https://."));
      return;
    }
    const body = JSON.stringify(payload);
    const request = (isHttps ? https : http).request(target, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "content-length": Buffer.byteLength(body)
      },
      timeout: 5000
    }, response => {
      response.resume();
      response.on("end", () => {
        if (response.statusCode < 200 || response.statusCode >= 300) {
          reject(new Error(`Support webhook returned HTTP ${response.statusCode}`));
          return;
        }
        resolve();
      });
    });
    request.on("timeout", () => request.destroy(new Error("Support webhook timed out.")));
    request.on("error", reject);
    request.end(body);
  });
}

async function readSupportLog(limit = 200) {
  try {
    const text = await fs.promises.readFile(SUPPORT_LOG_FILE, "utf8");
    return text
      .split(/\r?\n/)
      .filter(Boolean)
      .slice(-limit)
      .map(line => {
        try {
          return JSON.parse(line);
        } catch (_err) {
          return { level: "warn", message: line };
        }
      });
  } catch (err) {
    if (err.code === "ENOENT") return [];
    throw err;
  }
}

function escapeHtml(value) {
  return String(value || "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}

function licensePageHtml() {
  const state = licenseState();
  const message = licenseMessage(state);
  return `<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${escapeHtml(APP_NAME)} Activation</title>
  <style>
    :root {
      color-scheme: dark;
      font-family: Arial, Helvetica, sans-serif;
      background: #05080b;
      color: #f7f8f9;
    }
    body {
      margin: 0;
      min-height: 100vh;
      display: grid;
      place-items: center;
      background:
        linear-gradient(135deg, rgba(218, 31, 38, 0.18), transparent 36%),
        radial-gradient(circle at 72% 18%, rgba(25, 95, 155, 0.16), transparent 28%),
        #05080b;
    }
    main {
      width: min(520px, calc(100vw - 40px));
      border: 1px solid rgba(255, 255, 255, 0.18);
      background: rgba(12, 16, 21, 0.94);
      box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
      border-radius: 8px;
      padding: 30px;
    }
    img {
      display: block;
      max-width: 220px;
      max-height: 92px;
      object-fit: contain;
      margin-bottom: 24px;
    }
    h1 {
      margin: 0 0 12px;
      font-size: 28px;
      line-height: 1.1;
    }
    p {
      margin: 0 0 18px;
      color: #cfd6de;
      line-height: 1.45;
    }
    label {
      display: block;
      margin-bottom: 8px;
      color: #dfe5eb;
      font-weight: 700;
      font-size: 14px;
    }
    input {
      box-sizing: border-box;
      width: 100%;
      min-height: 48px;
      padding: 12px 14px;
      border-radius: 6px;
      border: 1px solid rgba(255, 255, 255, 0.18);
      background: #090d12;
      color: #ffffff;
      font-size: 16px;
    }
    .actions {
      display: flex;
      gap: 12px;
      margin-top: 16px;
      flex-wrap: wrap;
    }
    button, a.button {
      border: 0;
      border-radius: 6px;
      padding: 12px 18px;
      min-height: 46px;
      background: #d71920;
      color: #ffffff;
      font-weight: 800;
      cursor: pointer;
      text-decoration: none;
      display: inline-flex;
      align-items: center;
    }
    button.secondary, a.button.secondary {
      background: #242c35;
    }
    small {
      display: block;
      margin-top: 18px;
      color: #8f9aa5;
      line-height: 1.4;
    }
    #status {
      min-height: 20px;
      margin-top: 16px;
      color: #ffffff;
      font-weight: 700;
    }
  </style>
</head>
<body>
  <main>
    <img src="/assets/liberty-raceworks-logo.png" alt="${escapeHtml(APP_NAME)}">
    <h1>${escapeHtml(APP_NAME)} Activation</h1>
    <p>${escapeHtml(message)}</p>
    <label for="productKey">Product key</label>
    <input id="productKey" autocomplete="off" spellcheck="false" placeholder="Enter product key">
    <div class="actions">
      <button id="activateButton" type="button">Activate</button>
      <button class="secondary" id="trialButton" type="button">Start ${Number(PRODUCT_CONFIG.trialDays) || 7} Day Trial</button>
      ${state.active ? '<a class="button secondary" href="/control.html">Open Program</a>' : ""}
    </div>
    <div id="status"></div>
    <small>Machine ID: ${escapeHtml(state.machineId || "")}</small>
  </main>
  <script>
    const statusEl = document.getElementById("status");
    const keyEl = document.getElementById("productKey");
    async function post(path, body) {
      statusEl.textContent = "Working...";
      const response = await fetch(path, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(body || {})
      });
      const data = await response.json().catch(() => ({}));
      if (!response.ok || data.ok === false) throw new Error(data.error || "Activation request failed.");
      if (data.license && data.license.active) {
        statusEl.textContent = "License active. Opening the program...";
        window.location.href = "/control.html";
        return;
      }
      statusEl.textContent = data.license && data.license.message ? data.license.message : "Saved, but this license is not active yet.";
    }
    document.getElementById("activateButton").addEventListener("click", () => {
      post("/api/license/activate", { productKey: keyEl.value }).catch(error => {
        statusEl.textContent = error.message;
      });
    });
    document.getElementById("trialButton").addEventListener("click", () => {
      post("/api/license/trial").catch(error => {
        statusEl.textContent = error.message;
      });
    });
  </script>
</body>
</html>`;
}

function sendLicenseLockedJson(res) {
  const state = licenseState();
  sendJson(res, 402, {
    ok: false,
    error: licenseMessage(state),
    license: state,
    activationUrl: "/license.html"
  });
}

function sendLicenseLockedPage(res) {
  const body = licensePageHtml();
  res.writeHead(402, {
    "content-type": "text/html; charset=utf-8",
    "content-length": Buffer.byteLength(body),
    "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
    "pragma": "no-cache",
    "expires": "0"
  });
  res.end(body);
}

function isLocalRequest(req) {
  const address = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : "";
  return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
}

function hasRemoteControlAccess(req) {
  if (process.env.ALLOW_REMOTE_CONTROL === "1") return true;
  const token = String(process.env.CONTROL_TOKEN || "").trim();
  if (!token) return false;
  return req.headers["x-control-token"] === token;
}

function shouldBlockRemoteMutation(req, url) {
  return req.method !== "GET"
    && url.pathname.startsWith("/api/")
    && !isLocalRequest(req)
    && !hasRemoteControlAccess(req);
}
// ---- end commercial distribution support/license helpers -------------------

function views() {
  syncTop3PhotoVersions();
  const baseView = buildViews(timingState);
  return {
    ...raceIntelligence.enhance(baseView),
    style: styleForClient(),
    // Output states ride along on the main event stream so overlay pages can
    // follow program/banner changes without opening extra SSE connections
    // (browsers cap connections per origin at 6).
    output: {
      program: programState,
      topBanner: topBannerState,
      storyOutput: storyOutputState
    }
  };
}

function healthSnapshot() {
  const currentViews = views();
  const meta = currentViews.meta || {};
  const drivers = currentViews.competitors || currentViews.drivers || [];
  const liveState = timingState.live || {};
  return {
    ok: true,
    serverTime: new Date().toISOString(),
    startedAt: SERVER_STARTED_AT.toISOString(),
    uptimeSeconds: Math.round(process.uptime()),
    host: HOST,
    port: PORT,
    timing: {
      source: meta.source || timingState.source || "none",
      connected: Boolean(liveState.connected),
      connecting: Boolean(liveState.connecting),
      provider: liveState.provider || meta.source || timingState.source || "none",
      lastUpdated: meta.lastUpdated || timingState.lastUpdated,
      drivers: drivers.length,
      session: meta.session || timingState.session || "",
      className: meta.className || timingState.className || "",
      mode: meta.mode || timingState.mode || "",
      flag: meta.flagLabel || meta.flag || timingState.flagLabel || timingState.flag || ""
    },
    output: {
      program: programState.graphic,
      programVersion: programState.version,
      topBannerVisible: topBannerState.visible,
      topBannerVersion: topBannerState.version
    },
    style: {
      profileVersion: styleState.styleProfileVersion,
      bannerTemplate: styleState.bannerTemplate
    },
    product: {
      appName: APP_NAME,
      productId: PRODUCT_CONFIG.productId,
      tier: PRODUCT_CONFIG.tier,
      allowedGraphics: [...ALLOWED_PROGRAM_GRAPHICS],
      allowedBannerTemplates: [...ALLOWED_BANNER_TEMPLATES]
    },
    license: licenseManager.publicState(),
    connections: {
      browserClients: clients.size,
      programClients: programClients.size,
      topBannerClients: topBannerClients.size,
      storyOutputClients: storyOutputClients.size
    }
  };
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    // Decode as a stream so multi-byte characters split across chunks stay intact.
    req.setEncoding("utf8");
    let body = "";
    let settled = false;
    const timer = setTimeout(() => {
      finish(new Error("Request body timed out."));
      req.destroy();
    }, REQUEST_BODY_TIMEOUT_MS);

    function finish(err, value) {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      if (err) reject(err);
      else resolve(value);
    }

    req.on("data", chunk => {
      body += chunk;
      if (body.length > REQUEST_BODY_LIMIT_BYTES) {
        finish(new Error("Request body too large."));
        req.destroy();
      }
    });
    req.on("end", () => finish(null, body));
    req.on("error", finish);
  });
}

function cleanMetaText(value) {
  return String(value == null ? "" : value).trim();
}

function firstText(...values) {
  return values.map(cleanMetaText).find(Boolean) || "";
}

function raceDiscoveryLocation(race = {}, data = {}) {
  const trackName = firstText(
    race.TrackName,
    race.Track,
    race.TrackDisplayName,
    race.Facility,
    race.Venue,
    race.Name,
    data.TrackName
  );
  const city = firstText(race.City, race.TrackCity, race.VenueCity, data.City);
  const region = firstText(race.State, race.Region, race.Province, race.TrackState, race.VenueState, data.State);
  const country = firstText(race.Country, race.TrackCountry, race.VenueCountry, data.Country);
  const location = [city, region, country].filter(Boolean).join(", ")
    || firstText(race.Location, race.TrackLocation, race.VenueLocation, race.Address, data.Location)
    || trackName;
  return { trackName, location };
}

function applyRaceDiscoveryMetadata(race, data) {
  const metadata = raceDiscoveryLocation(race, data);
  if (metadata.trackName && !timingState.trackName) timingState.trackName = metadata.trackName;
  if (metadata.location && !timingState.weatherLocation) timingState.weatherLocation = metadata.location;
}

function weatherLocationSource() {
  const manual = cleanMetaText(styleState.weatherLocationOverride);
  if (manual) return { location: manual, source: "manual" };
  const timingLocation = cleanMetaText(timingState.weatherLocation);
  if (timingLocation) return { location: timingLocation, source: "timing" };
  const trackName = cleanMetaText(timingState.trackName);
  if (trackName) return { location: trackName, source: "track" };
  return { location: "", source: "" };
}

function compassDirection(degrees) {
  const value = Number(degrees);
  if (!Number.isFinite(value)) return "";
  const directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
  return directions[Math.round(((value % 360) / 22.5)) % 16];
}

function weatherDescription(code) {
  const value = Number(code);
  if ([0].includes(value)) return "Clear";
  if ([1, 2].includes(value)) return "Partly Cloudy";
  if ([3].includes(value)) return "Cloudy";
  if ([45, 48].includes(value)) return "Fog";
  if ([51, 53, 55, 56, 57].includes(value)) return "Drizzle";
  if ([61, 63, 65, 66, 67, 80, 81, 82].includes(value)) return "Rain";
  if ([71, 73, 75, 77, 85, 86].includes(value)) return "Snow";
  if ([95, 96, 99].includes(value)) return "Thunderstorms";
  return "Weather";
}

function parseCoordinateLocation(location) {
  const match = String(location || "").trim().match(/^(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)$/);
  if (!match) return null;
  const latitude = Number(match[1]);
  const longitude = Number(match[2]);
  if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
  return {
    latitude,
    longitude,
    name: `${latitude.toFixed(3)}, ${longitude.toFixed(3)}`,
    admin1: "",
    country: ""
  };
}

const US_STATE_NAMES = {
  AL: "Alabama",
  AK: "Alaska",
  AZ: "Arizona",
  AR: "Arkansas",
  CA: "California",
  CO: "Colorado",
  CT: "Connecticut",
  DE: "Delaware",
  FL: "Florida",
  GA: "Georgia",
  HI: "Hawaii",
  ID: "Idaho",
  IL: "Illinois",
  IN: "Indiana",
  IA: "Iowa",
  KS: "Kansas",
  KY: "Kentucky",
  LA: "Louisiana",
  ME: "Maine",
  MD: "Maryland",
  MA: "Massachusetts",
  MI: "Michigan",
  MN: "Minnesota",
  MS: "Mississippi",
  MO: "Missouri",
  MT: "Montana",
  NE: "Nebraska",
  NV: "Nevada",
  NH: "New Hampshire",
  NJ: "New Jersey",
  NM: "New Mexico",
  NY: "New York",
  NC: "North Carolina",
  ND: "North Dakota",
  OH: "Ohio",
  OK: "Oklahoma",
  OR: "Oregon",
  PA: "Pennsylvania",
  RI: "Rhode Island",
  SC: "South Carolina",
  SD: "South Dakota",
  TN: "Tennessee",
  TX: "Texas",
  UT: "Utah",
  VT: "Vermont",
  VA: "Virginia",
  WA: "Washington",
  WV: "West Virginia",
  WI: "Wisconsin",
  WY: "Wyoming"
};

function expandStateToken(value) {
  const key = cleanMetaText(value).toUpperCase().replace(/\./g, "");
  return US_STATE_NAMES[key] || cleanMetaText(value);
}

function isUsZipCode(value) {
  return /^\d{5}(?:-\d{4})?$/.test(cleanMetaText(value));
}

function weatherSearchTerms(location) {
  const text = cleanMetaText(location);
  if (isUsZipCode(text)) {
    const zip = text.slice(0, 5);
    return [zip, `${zip}, United States`];
  }
  const parts = text.split(",").map(part => part.trim()).filter(Boolean);
  const city = parts[0] || text;
  const region = expandStateToken(parts[1]);
  return [...new Set([
    text,
    city && region ? `${city}, ${region}, United States` : "",
    city && region ? `${city} ${region}` : "",
    city ? `${city}, United States` : "",
    city,
    parts.slice(0, 2).join(" ")
  ].filter(Boolean))];
}

function weatherPlaceScore(place, location) {
  const haystack = [
    place.name,
    place.admin1,
    place.admin2,
    place.admin3,
    place.country,
    place.country_code
  ].map(cleanMetaText).join(" ").toLowerCase();
  const tokens = cleanMetaText(location)
    .toLowerCase()
    .split(/[,\s]+/)
    .flatMap(token => {
      const expanded = expandStateToken(token);
      return expanded && expanded.toLowerCase() !== token ? [token, ...expanded.toLowerCase().split(/\s+/)] : [token];
    })
    .filter(token => token.length > 1);
  return tokens.reduce((score, token) => score + (haystack.includes(token) ? 1 : 0), 0);
}

async function geocodeWeatherLocation(location) {
  const coordinates = parseCoordinateLocation(location);
  if (coordinates) return coordinates;
  let result = null;
  let resultScore = -1;
  for (const term of weatherSearchTerms(location)) {
    const geocodeUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(term)}&count=10&language=en&format=json`;
    const data = await fetchJson(geocodeUrl, "Weather geocoding");
    (Array.isArray(data.results) ? data.results : []).forEach(place => {
      const score = weatherPlaceScore(place, location);
      if (!result || score > resultScore) {
        result = place;
        resultScore = score;
      }
    });
    if (result && resultScore > 1) break;
  }
  if (!result) throw new Error(`Could not find weather location for "${location}". Enter a city, state, or ZIP in Weather location override.`);
  return result;
}

async function getWeatherData(force = false) {
  const { location, source } = weatherLocationSource();
  if (!location) throw new Error("No weather location found. Enter a city, state, or ZIP in Weather location override.");
  const key = `${source}:${location}`.toLowerCase();
  if (!force && weatherCache.key === key && weatherCache.data && Date.now() - weatherCache.fetchedAt < 300000) {
    return weatherCache.data;
  }
  // Remember failures briefly so a bad location doesn't hammer the weather APIs
  // from every overlay refresh.
  if (!force && weatherCache.key === key && weatherCache.error && Date.now() - weatherCache.fetchedAt < 60000) {
    throw new Error(weatherCache.error);
  }

  let place;
  try {
    place = await geocodeWeatherLocation(location);
  } catch (err) {
    weatherCache = { key, fetchedAt: Date.now(), data: null, error: err.message };
    throw err;
  }
  const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(place.latitude)}&longitude=${encodeURIComponent(place.longitude)}&current=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m,wind_gusts_10m,wind_direction_10m&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=auto`;
  let data;
  try {
    data = await fetchJson(weatherUrl, "Weather");
  } catch (err) {
    weatherCache = { key, fetchedAt: Date.now(), data: null, error: err.message };
    throw err;
  }
  const current = data.current || {};
  const units = data.current_units || {};
  const resolvedLocation = [place.name, place.admin1, place.country].filter(Boolean).join(", ");
  const payload = {
    ok: true,
    source,
    requestedLocation: location,
    resolvedLocation,
    updatedAt: new Date().toISOString(),
    current: {
      temperature: current.temperature_2m,
      feelsLike: current.apparent_temperature,
      humidity: current.relative_humidity_2m,
      precipitation: current.precipitation,
      weatherCode: current.weather_code,
      condition: weatherDescription(current.weather_code),
      windSpeed: current.wind_speed_10m,
      windGust: current.wind_gusts_10m,
      windDirection: current.wind_direction_10m,
      windCompass: compassDirection(current.wind_direction_10m),
      time: current.time,
      units: {
        temperature: units.temperature_2m || "°F",
        humidity: units.relative_humidity_2m || "%",
        precipitation: units.precipitation || "in",
        wind: units.wind_speed_10m || "mph"
      }
    }
  };
  weatherCache = { key, fetchedAt: Date.now(), data: payload };
  return payload;
}

function localNetworkBases() {
  const interfaces = os.networkInterfaces();
  const addresses = [];
  Object.entries(interfaces).forEach(([name, entries]) => {
    (entries || []).forEach(entry => {
      if (entry.family !== "IPv4" || entry.internal) return;
      addresses.push({
        name,
        address: entry.address,
        baseUrl: `http://${entry.address}:${PORT}`
      });
    });
  });
  return addresses;
}

function normalizeBannerSide(side) {
  return side === "right" || side === "sponsor" ? "sponsor" : "header";
}

async function saveBannerLogos(side, dataUrls) {
  const cleanSide = normalizeBannerSide(side);
  const urls = Array.isArray(dataUrls) ? dataUrls : [dataUrls];
  const images = urls.filter(Boolean);
  if (!images.length) throw new Error("Choose at least one PNG, JPG, or WebP image.");

  await fs.promises.mkdir(UPLOAD_DIR, { recursive: true });

  const existing = await fs.promises.readdir(UPLOAD_DIR).catch(() => []);
  await Promise.all(
    existing
      .filter(file => file.startsWith(`banner-${cleanSide}-`))
      .map(file => fs.promises.rm(path.join(UPLOAD_DIR, file), { force: true }))
  );

  await Promise.all(images.map(async (dataUrl, index) => {
    const match = String(dataUrl || "").match(/^data:image\/(png|jpeg|jpg|webp);base64,(.+)$/i);
    if (!match) throw new Error("Choose PNG, JPG, or WebP images.");
    const ext = match[1].toLowerCase() === "jpeg" ? "jpg" : match[1].toLowerCase();
    const buffer = Buffer.from(match[2], "base64");
    if (!buffer.length || buffer.length > 8_000_000) throw new Error("Each logo image must be under 8 MB.");
    await fs.promises.writeFile(path.join(UPLOAD_DIR, `banner-${cleanSide}-${index}.${ext}`), buffer);
  }));

  const versionKey = cleanSide === "sponsor" ? "bannerSponsorLogoVersion" : "bannerHeaderLogoVersion";
  const countKey = cleanSide === "sponsor" ? "bannerSponsorLogoCount" : "bannerHeaderLogoCount";
  styleState = { ...styleState, [versionKey]: Date.now(), [countKey]: images.length };
  await persistStyleState();
  broadcast();
  return { count: images.length, style: styleState };
}

async function saveTop3Photo(position, dataUrl) {
  const cleanPosition = Number(position);
  if (!Number.isInteger(cleanPosition) || cleanPosition < 1 || cleanPosition > 3) {
    throw new Error("Choose driver position 1, 2, or 3.");
  }

  const match = String(dataUrl || "").match(/^data:image\/(png|jpeg|jpg|webp);base64,(.+)$/i);
  if (!match) throw new Error("Choose a PNG, JPG, or WebP driver photo.");

  await fs.promises.mkdir(UPLOAD_DIR, { recursive: true });
  const existing = await fs.promises.readdir(UPLOAD_DIR).catch(() => []);
  await Promise.all(
    existing
      .filter(file => file.startsWith(`top3-driver-${cleanPosition}.`))
      .map(file => fs.promises.rm(path.join(UPLOAD_DIR, file), { force: true }))
  );

  const ext = match[1].toLowerCase() === "jpeg" ? "jpg" : match[1].toLowerCase();
  const buffer = Buffer.from(match[2], "base64");
  if (!buffer.length || buffer.length > 10_000_000) throw new Error("Driver photo must be under 10 MB.");
  await fs.promises.writeFile(path.join(UPLOAD_DIR, `top3-driver-${cleanPosition}.${ext}`), buffer);

  const versions = Array.isArray(styleState.top3PhotoVersions)
    ? [...styleState.top3PhotoVersions]
    : [0, 0, 0];
  versions[cleanPosition - 1] = Date.now();
  styleState = { ...styleState, top3PhotoVersions: versions };
  await persistStyleState();
  broadcast();
  return { position: cleanPosition, style: styleState };
}

function top3PhotoPaths(position) {
  return [
    path.join(UPLOAD_DIR, `top3-driver-${position}.png`),
    path.join(UPLOAD_DIR, `top3-driver-${position}.jpg`),
    path.join(UPLOAD_DIR, `top3-driver-${position}.webp`),
    path.join(LEGACY_UPLOAD_DIR, `top3-driver-${position}.png`),
    path.join(LEGACY_UPLOAD_DIR, `top3-driver-${position}.jpg`),
    path.join(LEGACY_UPLOAD_DIR, `top3-driver-${position}.webp`)
  ];
}

function top3PhotoExists(position) {
  return top3PhotoPaths(position).some(filePath => fs.existsSync(filePath));
}

function syncTop3PhotoVersions() {
  const versions = Array.isArray(styleState.top3PhotoVersions)
    ? [...styleState.top3PhotoVersions]
    : [0, 0, 0];
  let changed = false;
  for (let index = 0; index < 3; index += 1) {
    if (versions[index] && !top3PhotoExists(index + 1)) {
      versions[index] = 0;
      changed = true;
    }
  }
  if (changed) {
    styleState = { ...styleState, top3PhotoVersions: versions };
  }
  return styleState;
}

async function clearTop3Photo(position) {
  const cleanPosition = Number(position);
  if (!Number.isInteger(cleanPosition) || cleanPosition < 1 || cleanPosition > 3) {
    throw new Error("Choose driver position 1, 2, or 3.");
  }

  await Promise.all([UPLOAD_DIR, LEGACY_UPLOAD_DIR].map(async directory => {
    const existing = await fs.promises.readdir(directory).catch(() => []);
    await Promise.all(
      existing
        .filter(file => file.startsWith(`top3-driver-${cleanPosition}.`))
        .map(file => fs.promises.rm(path.join(directory, file), { force: true }))
    );
  }));

  const versions = Array.isArray(styleState.top3PhotoVersions)
    ? [...styleState.top3PhotoVersions]
    : [0, 0, 0];
  versions[cleanPosition - 1] = 0;
  styleState = { ...styleState, top3PhotoVersions: versions };
  await persistStyleState();
  broadcast();
  return { position: cleanPosition, style: styleState };
}

// Top 3 photos are per-session, but the launchers auto-restart after a crash.
// A marker file distinguishes a mid-show restart (keep the photos) from a fresh
// show day (start clean).
function wasRecentlyRunning() {
  try {
    return Date.now() - fs.statSync(RECENT_RUN_MARKER_FILE).mtimeMs < RECENT_RUN_MAX_AGE_MS;
  } catch (_err) {
    return false;
  }
}

function touchRecentRunMarker() {
  fs.promises.mkdir(RESOURCES_DIR, { recursive: true })
    .then(() => fs.promises.writeFile(RECENT_RUN_MARKER_FILE, new Date().toISOString()))
    .catch(err => logEvent("warn", "Could not update recent-run marker", { error: errorMessage(err) }));
}

function clearTop3SessionPhotosOnStartup() {
  [UPLOAD_DIR, LEGACY_UPLOAD_DIR].forEach(directory => {
    let existing = [];
    try {
      existing = fs.readdirSync(directory);
    } catch (_err) {
      return;
    }
    existing
      .filter(file => /^top3-driver-[1-3]\.(?:png|jpe?g|webp)$/i.test(file))
      .forEach(file => {
        try {
          fs.rmSync(path.join(directory, file), { force: true });
        } catch (_err) {}
      });
  });
  styleState = { ...styleState, top3PhotoVersions: [0, 0, 0] };
  persistStyleState();
}

function graphicUrl(baseUrl, graphic, version = Date.now(), options = {}) {
  const route = PROGRAM_ROUTES[graphic] || PROGRAM_ROUTES.blank;
  const origin = String(baseUrl || "").replace(/\/+$/, "");
  if (options.direct === true && (graphic === "topbanner" || graphic === "sidebanner")) {
    return `${origin}${route}`;
  }
  return `${origin}/program.html`;
}

async function sendToVmixBrowser({ vmixUrl, input, graphic, overlayBase, direct = false }) {
  let apiBase = String(vmixUrl || "").trim().replace(/\/+$/, "");
  const inputName = String(input || "").trim();
  let overlayOrigin = String(overlayBase || "").trim().replace(/\/+$/, "");
  if (apiBase && !/^https?:\/\//i.test(apiBase)) apiBase = `http://${apiBase}`;
  if (overlayOrigin && !/^https?:\/\//i.test(overlayOrigin)) overlayOrigin = `http://${overlayOrigin}`;
  if (!apiBase) throw new Error("Enter the vMix API address.");
  if (!inputName) throw new Error("Enter the vMix browser input name or number.");
  if (!overlayOrigin) throw new Error("Enter the overlay base URL.");
  if (!PROGRAM_ROUTES[graphic]) throw new Error("Unknown graphic.");

  const target = graphicUrl(overlayOrigin, graphic, programState.version || Date.now(), { direct });
  const vmixApi = `${apiBase}/api/?Function=BrowserNavigate&Input=${encodeURIComponent(inputName)}&Value=${encodeURIComponent(target)}`;
  await fetchHttpText(vmixApi);
  logEvent("info", "Sent browser URL to vMix", { input: inputName, graphic, target });
  return target;
}

function parseRaceId(input) {
  const value = String(input || "").trim();
  if (/^\d+$/.test(value)) return value;
  const match = value.match(/\/Live\/Race\/(\d+)/i) || value.match(/[?&]raceid=(\d+)/i);
  if (!match) throw new Error("Enter a Race Monitor race URL or race ID.");
  return match[1];
}

function setLiveRaceMonitorState(patch) {
  liveRaceMonitorState = { ...liveRaceMonitorState, ...patch };
  timingState.live = { provider: "racemonitor", ...liveRaceMonitorState };
  broadcast();
}

function setLiveMyRacePassState(patch) {
  liveMyRacePassState = { ...liveMyRacePassState, ...patch };
  timingState.live = { provider: "myracepass", ...liveMyRacePassState };
  broadcast();
}

function clearRaceMonitorReconnect() {
  if (raceMonitorReconnectTimer) {
    clearTimeout(raceMonitorReconnectTimer);
    raceMonitorReconnectTimer = null;
  }
}

function resetRaceMonitorReconnectDelay() {
  raceMonitorReconnectDelayMs = RACE_MONITOR_RECONNECT_INITIAL_MS;
}

function scheduleRaceMonitorReconnect(reason) {
  if (!liveRaceMonitorUrl || raceMonitorReconnectTimer) return;
  const delayMs = raceMonitorReconnectDelayMs;
  raceMonitorReconnectDelayMs = Math.min(RACE_MONITOR_RECONNECT_MAX_MS, raceMonitorReconnectDelayMs * 2);
  logEvent("warn", "Race Monitor reconnect scheduled", { reason, retryInMs: delayMs });
  setLiveRaceMonitorState({
    connected: false,
    connecting: true,
    error: reason || "Race Monitor reconnecting"
  });
  raceMonitorReconnectTimer = setTimeout(() => {
    raceMonitorReconnectTimer = null;
    connectRaceMonitor(liveRaceMonitorUrl, { isReconnect: true }).catch(err => {
      setLiveRaceMonitorState({
        connected: false,
        connecting: false,
        error: err.message
      });
      scheduleRaceMonitorReconnect(err.message);
    });
  }, delayMs);
}

// A silently dead socket (venue Wi-Fi drop, NAT timeout) never fires onclose, so
// force a reconnect when no data has arrived for RACE_MONITOR_STALE_SOCKET_MS.
function startRaceMonitorWatchdog() {
  if (raceMonitorWatchdogTimer) return;
  raceMonitorWatchdogTimer = setInterval(() => {
    if (!liveRaceMonitorSocket || !liveRaceMonitorState.connected) return;
    if (!raceMonitorLastMessageAt || Date.now() - raceMonitorLastMessageAt < RACE_MONITOR_STALE_SOCKET_MS) return;
    logEvent("warn", "Race Monitor data stalled, forcing reconnect", {
      silentForMs: Date.now() - raceMonitorLastMessageAt
    });
    const socket = liveRaceMonitorSocket;
    liveRaceMonitorSocket = null;
    socket.onclose = null;
    try {
      socket.close();
    } catch (_err) {}
    scheduleRaceMonitorReconnect("Race Monitor data stalled");
  }, RACE_MONITOR_WATCHDOG_INTERVAL_MS);
  if (raceMonitorWatchdogTimer.unref) raceMonitorWatchdogTimer.unref();
}

function stopRaceMonitorWatchdog() {
  if (raceMonitorWatchdogTimer) {
    clearInterval(raceMonitorWatchdogTimer);
    raceMonitorWatchdogTimer = null;
  }
}

function resetTimingState() {
  timingState.source = "none";
  timingState.raw = [];
  timingState.classes = {};
  timingState.competitors = new Map();
  timingState.roster = new Map();
  timingState.session = "";
  timingState.className = "";
  timingState.trackName = "";
  timingState.trackLength = "";
  timingState.weatherLocation = "";
  timingState.mode = "";
  timingState.clock = "";
  timingState.lapsToGo = "";
  timingState.raceTime = "";
  timingState.timeToGo = "";
  timingState.flag = "";
  timingState.flagCode = "";
  timingState.flagLabel = "";
  timingState.lastUpdated = new Date().toISOString();
}

function disconnectRaceMonitor() {
  clearRaceMonitorReconnect();
  resetRaceMonitorReconnectDelay();
  stopRaceMonitorWatchdog();
  liveRaceMonitorUrl = "";
  if (liveRaceMonitorSocket) {
    liveRaceMonitorSocket.onclose = null;
    liveRaceMonitorSocket.close();
    liveRaceMonitorSocket = null;
  }
  logEvent("info", "Race Monitor disconnected");
  setLiveRaceMonitorState({ connected: false, connecting: false });
}

function disconnectMyRacePass() {
  if (myRacePassInterval) {
    clearInterval(myRacePassInterval);
    myRacePassInterval = null;
  }
  logEvent("info", "MyRacePass disconnected");
  setLiveMyRacePassState({ connected: false, connecting: false });
}

async function connectRaceMonitor(raceUrl, { isReconnect = false } = {}) {
  if (typeof WebSocketRuntime !== "function") {
    throw new Error("This Node runtime does not include WebSocket support.");
  }

  const raceId = parseRaceId(raceUrl);
  const generation = ++raceMonitorConnectGeneration;
  logEvent("info", "Connecting Race Monitor", { raceId, isReconnect });
  disconnectMyRacePass();
  clearRaceMonitorReconnect();
  if (liveRaceMonitorSocket) {
    liveRaceMonitorSocket.onclose = null;
    liveRaceMonitorSocket.close();
    liveRaceMonitorSocket = null;
  }
  liveRaceMonitorUrl = raceUrl;
  // Keep the current field on screen through automatic reconnects; the feed
  // re-sends full state after joining, so only a fresh connect starts clean.
  if (!isReconnect) resetTimingState();
  setLiveRaceMonitorState({ connecting: true, error: "", raceId, host: "", instance: "" });

  const discoveryUrl = `https://api.race-monitor.com/Info/WebRaceList?accountID=&seriesID=&raceID=${encodeURIComponent(raceId)}&styleID=&t=${Date.now()}`;
  const data = await fetchJson(discoveryUrl);
  if (generation !== raceMonitorConnectGeneration) {
    logEvent("info", "Race Monitor connect superseded by a newer connect", { raceId });
    return;
  }
  const races = data.CurrentRaces || [];
  const race = races.find(item => String(item.ID) === String(raceId)) || races[0];

  if (!race || !race.ReceivingData) {
    throw new Error("Race Monitor is not currently receiving live data for this race.");
  }
  applyRaceDiscoveryMetadata(race, data);
  if (!data.LiveTimingHost || !data.LiveTimingToken || !race.Instance) {
    throw new Error("Race Monitor did not return a live timing host, instance, and token.");
  }

  const socket = new WebSocketRuntime(`wss://${data.LiveTimingHost}/instance/${race.Instance}/${data.LiveTimingToken}`);
  liveRaceMonitorSocket = socket;
  setLiveRaceMonitorState({ host: data.LiveTimingHost, instance: String(race.Instance) });

  socket.onopen = () => {
    if (liveRaceMonitorSocket !== socket) {
      try { socket.close(); } catch (_err) {}
      return;
    }
    logEvent("info", "Race Monitor connected", { raceId, host: data.LiveTimingHost, instance: String(race.Instance) });
    resetRaceMonitorReconnectDelay();
    raceMonitorLastMessageAt = Date.now();
    startRaceMonitorWatchdog();
    setLiveRaceMonitorState({ connected: true, connecting: false, error: "" });
  };

  socket.onmessage = event => {
    // An untracked socket must never feed state - close it on sight.
    if (liveRaceMonitorSocket !== socket) {
      try { socket.close(); } catch (_err) {}
      return;
    }
    raceMonitorLastMessageAt = Date.now();
    const message = String(event.data || "");
    if (message.startsWith("$JOIN")) return;
    applyMessage(message);
  };

  socket.onerror = () => {
    if (liveRaceMonitorSocket !== socket) return;
    logEvent("warn", "Race Monitor socket error", { raceId });
    scheduleRaceMonitorReconnect("Race Monitor socket error");
  };

  socket.onclose = () => {
    if (liveRaceMonitorSocket !== socket) return;
    liveRaceMonitorSocket = null;
    logEvent("warn", "Race Monitor socket closed", { raceId });
    scheduleRaceMonitorReconnect("Race Monitor disconnected");
  };
}

function myRacePassSiblingUrl(tickerUrl, sibling) {
  return String(tickerUrl || "").replace(/\/ticker\//i, `/${sibling}/`);
}

function normalizeRacePassMatchText(value) {
  return cleanMetaText(value)
    .toLowerCase()
    .replace(/^(?:\d+\.\s*)+/, "")
    .replace(/&/g, "and")
    .replace(/[^a-z0-9]+/g, "");
}

function racePassLineupScore(row, meta) {
  const rowClass = normalizeRacePassMatchText(row.Class || row.ClassName);
  const rowRace = normalizeRacePassMatchText(row.Race || row.RaceName);
  const metaClass = normalizeRacePassMatchText(meta.className);
  const metaRace = normalizeRacePassMatchText(meta.raceName || meta.session);
  let score = 0;

  if (rowRace && metaRace) {
    if (rowRace === metaRace) score += 80;
    else if (rowRace.includes(metaRace) || metaRace.includes(rowRace)) score += 45;
  }

  if (rowClass && metaClass) {
    if (rowClass === metaClass) score += 50;
    else if (rowClass.includes(metaClass) || metaClass.includes(rowClass)) score += 25;
  }

  return score;
}

function matchingRacePassLineupRow(rows, meta) {
  if (!Array.isArray(rows) || !rows.length) return null;
  const ranked = rows
    .map(row => ({ row, score: racePassLineupScore(row, meta) }))
    .filter(item => item.score >= 45)
    .sort((a, b) => b.score - a.score);
  return ranked[0] ? ranked[0].row : null;
}

function hasRacePassIdentity(driver) {
  return Boolean(driver && (driver.number || driver.firstName || driver.lastName || driver.fallbackName));
}

async function fetchMyRacePassLineups(tickerUrl) {
  const lineupUrl = myRacePassSiblingUrl(tickerUrl, "lineups");
  if (!/^https:\/\/srv\.myracepass\.com\/v3\/broadcasts\/lineups\//i.test(lineupUrl)) return [];
  const now = Date.now();
  if (myRacePassLineupCache.url === lineupUrl && now - myRacePassLineupCache.fetchedAt < 10000) {
    return myRacePassLineupCache.rows;
  }

  try {
    const csv = await fetchText(lineupUrl);
    const rows = parseTickerCsv(csv);
    myRacePassLineupCache = { url: lineupUrl, fetchedAt: now, rows };
    return rows;
  } catch (err) {
    logEvent("warn", "MyRacePass lineup fetch failed", { error: errorMessage(err) });
    return myRacePassLineupCache.url === lineupUrl ? myRacePassLineupCache.rows : [];
  }
}

function applyMyRacePassRows(rows, lineupRows = []) {
  const { meta, competitors } = normalizeTickerRows(rows);
  let nextMeta = meta;
  let nextCompetitors = competitors;

  if (!nextCompetitors.some(hasRacePassIdentity)) {
    const lineupRow = matchingRacePassLineupRow(lineupRows, nextMeta);
    if (lineupRow) {
      const lineup = normalizeTickerRows([lineupRow]);
      nextMeta = {
        ...lineup.meta,
        ...nextMeta,
        className: nextMeta.className || lineup.meta.className,
        raceName: nextMeta.raceName || lineup.meta.raceName,
        lapsToGo: nextMeta.lapsToGo || cleanMetaText(lineupRow.Laps) || lineup.meta.lapsToGo
      };
      nextCompetitors = lineup.competitors;
    }
  }

  const nextSession = [nextMeta.className, nextMeta.raceName].filter(Boolean).join(" - ");
  const sameSession = timingState.source === "myracepass" && nextSession && nextSession === timingState.session;

  timingState.source = "myracepass";
  timingState.trackName = nextMeta.trackName || "";
  timingState.weatherLocation = nextMeta.weatherLocation || nextMeta.trackName || "";
  timingState.className = nextMeta.className || "";
  timingState.session = nextSession;
  timingState.mode = nextMeta.mode || "";
  timingState.flag = nextMeta.flag || "";
  timingState.lapsToGo = nextMeta.lapsToGo || "";
  timingState.timeToGo = nextMeta.lapsToGo ? `${nextMeta.lapsToGo} laps` : "";
  timingState.classes = {};
  if (nextMeta.className) timingState.classes["1"] = nextMeta.className;

  if (nextCompetitors.length || !sameSession) {
    timingState.competitors = new Map();
    nextCompetitors.forEach(driver => {
      timingState.competitors.set(driver.id, {
        ...driver,
        updatedAt: new Date().toISOString()
      });
    });
  }

  timingState.lastUpdated = new Date().toISOString();
}

async function pollMyRacePass(url) {
  const csv = await fetchText(url);
  const rows = parseTickerCsv(csv);
  const lineupRows = await fetchMyRacePassLineups(url);
  applyMyRacePassRows(rows, lineupRows);
  setLiveMyRacePassState({
    connected: true,
    connecting: false,
    url,
    error: "",
    lastPoll: new Date().toISOString()
  });
  broadcast();
}

async function connectMyRacePass(url) {
  const tickerUrl = String(url || "").trim();
  if (!/^https:\/\/srv\.myracepass\.com\/v3\/broadcasts\/ticker\//i.test(tickerUrl)) {
    throw new Error("Enter a MyRacePass ticker URL.");
  }

  const generation = ++myRacePassConnectGeneration;
  logEvent("info", "Connecting MyRacePass", { url: tickerUrl });
  disconnectRaceMonitor();
  disconnectMyRacePass();
  resetTimingState();
  setLiveMyRacePassState({ connecting: true, connected: false, url: tickerUrl, error: "" });
  await pollMyRacePass(tickerUrl);
  if (generation !== myRacePassConnectGeneration) {
    logEvent("info", "MyRacePass connect superseded by a newer connect", { url: tickerUrl });
    return;
  }
  logEvent("info", "MyRacePass connected", { url: tickerUrl });
  myRacePassPollInFlight = false;
  myRacePassInterval = setInterval(() => {
    // Skip this tick if the previous poll is still running so slow responses
    // cannot stack up concurrent requests.
    if (myRacePassPollInFlight) return;
    myRacePassPollInFlight = true;
    pollMyRacePass(tickerUrl)
      .catch(err => {
        logEvent("warn", "MyRacePass poll failed", { error: errorMessage(err) });
        setLiveMyRacePassState({ error: err.message, connecting: false });
      })
      .finally(() => {
        myRacePassPollInFlight = false;
      });
  }, 1000);
}

let broadcastTimer = null;
let broadcastPending = false;

// Live timing can deliver dozens of commands in one websocket message. Building and
// serializing the full view for every command floods event clients, so coalesce
// bursts into at most one broadcast per BROADCAST_COALESCE_MS window.
function broadcast() {
  if (broadcastTimer) {
    broadcastPending = true;
    return;
  }
  flushBroadcast();
  broadcastTimer = setTimeout(() => {
    broadcastTimer = null;
    if (broadcastPending) {
      broadcastPending = false;
      broadcast();
    }
  }, BROADCAST_COALESCE_MS);
  if (broadcastTimer.unref) broadcastTimer.unref();
}

function flushBroadcast() {
  const data = `data: ${JSON.stringify(views())}\n\n`;
  writeEventClients(clients, data, "state");
}

function broadcastProgram() {
  const data = `data: ${JSON.stringify(programState)}\n\n`;
  writeEventClients(programClients, data, "program");
  broadcast();
}

function broadcastTopBanner() {
  const data = `data: ${JSON.stringify(topBannerState)}\n\n`;
  writeEventClients(topBannerClients, data, "top-banner");
  broadcast();
}

function broadcastStoryOutput() {
  const data = `data: ${JSON.stringify(storyOutputState)}\n\n`;
  writeEventClients(storyOutputClients, data, "story-output");
  broadcast();
}

function writeEventClients(clientSet, data, label) {
  for (const client of [...clientSet]) {
    if (client.destroyed || client.writableEnded) {
      clientSet.delete(client);
      continue;
    }
    try {
      client.write(data);
    } catch (err) {
      clientSet.delete(client);
      logEvent("warn", "Dropped disconnected browser event client", { label, error: errorMessage(err) });
    }
  }
}

// Periodic SSE comments keep idle connections alive and flush out dead sockets.
const sseHeartbeatTimer = setInterval(() => {
  writeEventClients(clients, ": ping\n\n", "state");
  writeEventClients(programClients, ": ping\n\n", "program");
  writeEventClients(topBannerClients, ": ping\n\n", "top-banner");
  writeEventClients(storyOutputClients, ": ping\n\n", "story-output");
}, SSE_HEARTBEAT_MS);
if (sseHeartbeatTimer.unref) sseHeartbeatTimer.unref();

function applyLine(line) {
  const command = parseRaceMonitorLine(line);
  if (!command) return false;
  timingState.source = "racemonitor";
  timingState.raw.push(line);
  if (timingState.raw.length > 500) timingState.raw.shift();
  applyRaceMonitorCommand(timingState, command);
  timingState.lastUpdated = new Date().toISOString();
  broadcast();
  return true;
}

function applyMessage(message) {
  return splitRaceMonitorCommands(message).filter(applyLine).length;
}

function readFirstAvailable(paths, callback) {
  const pending = [...paths];

  function tryNext() {
    const next = pending.shift();
    if (!next) {
      callback(new Error("File not found"));
      return;
    }

    fs.readFile(next, (err, data) => {
      if (err) {
        tryNext();
        return;
      }
      callback(null, data, next);
    });
  }

  tryNext();
}

function serveStatic(req, res) {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (url.pathname === "/license.html") {
    const body = licensePageHtml();
    res.writeHead(200, {
      "content-type": "text/html; charset=utf-8",
      "content-length": Buffer.byteLength(body),
      "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
      "pragma": "no-cache",
      "expires": "0"
    });
    res.end(body);
    return;
  }

  const bannerLogoMatch = url.pathname.match(/^\/banner-logo\/(left|right|header|sponsor)(?:\/(\d+))?$/);
  if (bannerLogoMatch) {
    const side = normalizeBannerSide(bannerLogoMatch[1]);
    const index = Number(bannerLogoMatch[2] || 0);
    const fallback = side === "header"
      ? path.join(PUBLIC_DIR, "assets", "liberty-raceworks-logo.png")
      : path.join(PUBLIC_DIR, "assets", "liberty-raceworks-icon.png");
    readFirstAvailable([
      path.join(UPLOAD_DIR, `banner-${side}-${index}.png`),
      path.join(UPLOAD_DIR, `banner-${side}-${index}.jpg`),
      path.join(UPLOAD_DIR, `banner-${side}-${index}.webp`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${side}-${index}.png`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${side}-${index}.jpg`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${side}-${index}.webp`),
      path.join(UPLOAD_DIR, `banner-${bannerLogoMatch[1]}-${index}.png`),
      path.join(UPLOAD_DIR, `banner-${bannerLogoMatch[1]}-${index}.jpg`),
      path.join(UPLOAD_DIR, `banner-${bannerLogoMatch[1]}-${index}.webp`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${bannerLogoMatch[1]}-${index}.png`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${bannerLogoMatch[1]}-${index}.jpg`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${bannerLogoMatch[1]}-${index}.webp`),
      path.join(UPLOAD_DIR, `banner-${bannerLogoMatch[1]}.png`),
      path.join(UPLOAD_DIR, `banner-${bannerLogoMatch[1]}.jpg`),
      path.join(UPLOAD_DIR, `banner-${bannerLogoMatch[1]}.webp`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${bannerLogoMatch[1]}.png`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${bannerLogoMatch[1]}.jpg`),
      path.join(LEGACY_UPLOAD_DIR, `banner-${bannerLogoMatch[1]}.webp`),
      fallback
    ], (err, data, filePath) => {
      if (err) {
        res.writeHead(404);
        res.end("Logo not found");
        return;
      }
      const contentType = filePath.endsWith(".webp") ? "image/webp" : filePath.endsWith(".jpg") ? "image/jpeg" : "image/png";
      res.writeHead(200, {
        "content-type": contentType,
        "cache-control": "no-cache"
      });
      res.end(data);
    });
    return;
  }

  const top3PhotoMatch = url.pathname.match(/^\/top3-photo\/([1-3])$/);
  if (top3PhotoMatch) {
    const position = top3PhotoMatch[1];
    readFirstAvailable([
      path.join(UPLOAD_DIR, `top3-driver-${position}.png`),
      path.join(UPLOAD_DIR, `top3-driver-${position}.jpg`),
      path.join(UPLOAD_DIR, `top3-driver-${position}.webp`),
      path.join(LEGACY_UPLOAD_DIR, `top3-driver-${position}.png`),
      path.join(LEGACY_UPLOAD_DIR, `top3-driver-${position}.jpg`),
      path.join(LEGACY_UPLOAD_DIR, `top3-driver-${position}.webp`)
    ], (err, data, filePath) => {
      if (err) {
        res.writeHead(404);
        res.end("Driver photo not found");
        return;
      }
      const contentType = filePath.endsWith(".webp") ? "image/webp" : filePath.endsWith(".jpg") ? "image/jpeg" : "image/png";
      res.writeHead(200, {
        "content-type": contentType,
        "cache-control": "no-cache"
      });
      res.end(data);
    });
    return;
  }

  if (FONT_FILES[url.pathname]) {
    readFirstAvailable(FONT_FILES[url.pathname], (err, data) => {
      if (err) {
        res.writeHead(404);
        res.end("Font not found");
        return;
      }
      const contentType = url.pathname.endsWith(".woff") ? "font/woff" : url.pathname.endsWith(".otf") ? "font/otf" : "font/ttf";
      res.writeHead(200, {
        "content-type": contentType,
        "cache-control": "public, max-age=3600"
      });
      res.end(data);
    });
    return;
  }

  if (url.pathname === "/sports-action-tv-logo.png") {
    readFirstAvailable([SAT_LOGO_IMAGE, DEFAULT_REFERENCE_IMAGE], (err, data) => {
      if (err) {
        res.writeHead(404);
        res.end("Logo not found");
        return;
      }
      res.writeHead(200, { "content-type": "image/png" });
      res.end(data);
    });
    return;
  }

  if (url.pathname === "/top-5-reference.png") {
    readFirstAvailable([DEFAULT_REFERENCE_IMAGE], (err, data) => {
      if (err) {
        res.writeHead(404);
        res.end("Top 5 reference not found");
        return;
      }
      res.writeHead(200, { "content-type": "image/png" });
      res.end(data);
    });
    return;
  }

  let requested = url.pathname === "/" ? "/control.html" : url.pathname;
  try {
    requested = decodeURIComponent(requested);
  } catch (_err) {
    res.writeHead(400);
    res.end("Bad request");
    return;
  }
  if (!pageAllowed(requested)) {
    res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
    res.end(`${APP_NAME} ${EDITION_LABEL} does not include this graphic.`);
    return;
  }
  const filePath = path.normalize(path.join(PUBLIC_DIR, requested));
  const relativePath = path.relative(PUBLIC_DIR, filePath);
  if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
    res.writeHead(403);
    res.end("Forbidden");
    return;
  }

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end("Not found");
      return;
    }

    const ext = path.extname(filePath).toLowerCase();
    const types = {
      ".html": "text/html; charset=utf-8",
      ".css": "text/css; charset=utf-8",
      ".js": "text/javascript; charset=utf-8",
      ".json": "application/json; charset=utf-8"
    };
    const noStoreAsset = [".html", ".js", ".css"].includes(ext);
    let body = data;
    if (ext === ".html" && TRIAL_WATERMARK_PAGES.has(requested)) {
      body = Buffer.from(
        data.toString("utf8").replace("</body>", '<script src="/trial-watermark.js" defer></script></body>')
      );
    }
    res.writeHead(200, {
      "content-type": types[ext] || "application/octet-stream",
      "cache-control": noStoreAsset ? "no-store, no-cache, must-revalidate, proxy-revalidate" : "public, max-age=3600",
      ...(noStoreAsset ? { "pragma": "no-cache", "expires": "0" } : {})
    });
    res.end(body);
  });
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (shouldBlockRemoteMutation(req, url)) {
    sendJson(res, 403, {
      ok: false,
      error: "Remote control is disabled. Open the control page on this computer, or set ALLOW_REMOTE_CONTROL=1 for trusted networks."
    });
    return;
  }

  if (!licenseAllowsProgram() && !licenseRequestOpen(req, url)) {
    if (url.pathname.startsWith("/api/") || req.method !== "GET") {
      sendLicenseLockedJson(res);
    } else {
      sendLicenseLockedPage(res);
    }
    return;
  }

  if (url.pathname === "/events") {
    res.writeHead(200, {
      "content-type": "text/event-stream",
      "cache-control": "no-cache",
      connection: "keep-alive"
    });
    clients.add(res);
    res.write(`data: ${JSON.stringify(views())}\n\n`);
    req.on("close", () => clients.delete(res));
    req.on("error", () => clients.delete(res));
    res.on("error", () => clients.delete(res));
    return;
  }

  if (url.pathname === "/program-events") {
    res.writeHead(200, {
      "content-type": "text/event-stream",
      "cache-control": "no-cache",
      connection: "keep-alive"
    });
    programClients.add(res);
    res.write(`data: ${JSON.stringify(programState)}\n\n`);
    req.on("close", () => programClients.delete(res));
    req.on("error", () => programClients.delete(res));
    res.on("error", () => programClients.delete(res));
    return;
  }

  if (url.pathname === "/top-banner-events") {
    res.writeHead(200, {
      "content-type": "text/event-stream",
      "cache-control": "no-cache",
      connection: "keep-alive"
    });
    topBannerClients.add(res);
    res.write(`data: ${JSON.stringify(topBannerState)}\n\n`);
    req.on("close", () => topBannerClients.delete(res));
    req.on("error", () => topBannerClients.delete(res));
    res.on("error", () => topBannerClients.delete(res));
    return;
  }

  if (url.pathname === "/story-output-events") {
    res.writeHead(200, {
      "content-type": "text/event-stream",
      "cache-control": "no-cache",
      connection: "keep-alive"
    });
    storyOutputClients.add(res);
    res.write(`data: ${JSON.stringify(storyOutputState)}\n\n`);
    req.on("close", () => storyOutputClients.delete(res));
    req.on("error", () => storyOutputClients.delete(res));
    res.on("error", () => storyOutputClients.delete(res));
    return;
  }

  if (url.pathname === "/api/story-output" && req.method === "GET") {
    sendJson(res, 200, storyOutputState);
    return;
  }

  if (url.pathname === "/api/story-output" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      storyOutputState = { visible: payload.visible !== false, version: Date.now() };
      logEvent("info", "Story output visibility changed", { visible: storyOutputState.visible });
      broadcastStoryOutput();
      sendJson(res, 200, { ok: true, storyOutput: storyOutputState });
    } catch (err) {
      sendApiError(res, 400, err, "Story output update rejected");
    }
    return;
  }

  if (url.pathname === "/api/product" && req.method === "GET") {
    sendJson(res, 200, {
      appName: APP_NAME,
      productId: PRODUCT_CONFIG.productId,
      tier: PRODUCT_CONFIG.tier,
      trialDays: PRODUCT_CONFIG.trialDays,
      allowedGraphics: [...ALLOWED_PROGRAM_GRAPHICS],
      allowedBannerTemplates: [...ALLOWED_BANNER_TEMPLATES],
      license: licenseManager.publicState()
    });
    return;
  }

  if (url.pathname === "/api/license" && req.method === "GET") {
    sendJson(res, 200, licenseManager.publicState());
    return;
  }

  if (url.pathname === "/api/license/trial" && req.method === "POST") {
    try {
      sendJson(res, 200, { ok: true, license: await licenseManager.startTrial() });
    } catch (err) {
      sendApiError(res, 400, err, "Trial start failed");
    }
    return;
  }

  if (url.pathname === "/api/license/activate" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body || "{}");
      sendJson(res, 200, { ok: true, license: await licenseManager.activate(payload.productKey) });
    } catch (err) {
      sendApiError(res, 400, err, "Activation failed");
    }
    return;
  }

  if (url.pathname === "/api/license/refresh" && req.method === "POST") {
    try {
      sendJson(res, 200, { ok: true, license: await licenseManager.refresh() });
    } catch (err) {
      sendApiError(res, 400, err, "License refresh failed");
    }
    return;
  }

  if (url.pathname === "/api/license/deactivate" && req.method === "POST") {
    try {
      sendJson(res, 200, { ok: true, license: await licenseManager.deactivate() });
    } catch (err) {
      sendApiError(res, 400, err, "Deactivation failed");
    }
    return;
  }

  if (url.pathname === "/api/support-log" && req.method === "GET") {
    if (!isLocalRequest(req) && !hasRemoteControlAccess(req)) {
      sendJson(res, 403, { ok: false, error: "Support logs are local-only unless remote control access is enabled." });
      return;
    }
    try {
      const limit = Math.max(1, Math.min(500, Number(url.searchParams.get("limit") || 200)));
      sendJson(res, 200, {
        ok: true,
        alertingEnabled: Boolean(SUPPORT_WEBHOOK_URL),
        alertLevels: [...SUPPORT_ALERT_LEVELS],
        logFile: SUPPORT_LOG_FILE,
        events: await readSupportLog(limit)
      });
    } catch (err) {
      sendApiError(res, 500, err, "Support log read failed");
    }
    return;
  }

  if (url.pathname === "/api/support-test" && req.method === "POST") {
    try {
      const payload = supportEventPayload("error", "Support alert test", { source: "manual-test" });
      await sendSupportAlert(payload);
      sendJson(res, 200, { ok: true });
    } catch (err) {
      sendApiError(res, 400, err, "Support alert test failed");
    }
    return;
  }

  if (url.pathname === "/api/health" && req.method === "GET") {
    sendJson(res, 200, healthSnapshot());
    return;
  }

  if (url.pathname === "/api/client-error" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body || "{}");
      logEvent("error", "Browser page error", {
        page: cleanReportText(payload.page, 180),
        message: cleanReportText(payload.message, 500),
        ...cleanClientErrorDetails(payload.details)
      });
      sendJson(res, 200, { ok: true });
    } catch (err) {
      sendApiError(res, 400, err, "Client error report rejected");
    }
    return;
  }

  if (url.pathname === "/api/state" && req.method === "GET") {
    sendJson(res, 200, views());
    return;
  }

  if (url.pathname === "/api/driver-profiles" && req.method === "GET") {
    sendJson(res, 200, raceIntelligence.profiles());
    return;
  }

  if (url.pathname === "/api/weather" && req.method === "GET") {
    try {
      const force = url.searchParams.get("force") === "1";
      const data = await getWeatherData(force);
      sendJson(res, 200, data);
    } catch (err) {
      logEvent("warn", "Weather request failed", { error: errorMessage(err), location: weatherLocationSource().location });
      sendJson(res, 200, {
        ok: false,
        error: err.message,
        source: weatherLocationSource().source,
        requestedLocation: weatherLocationSource().location
      });
    }
    return;
  }

  if (url.pathname === "/api/network" && req.method === "GET") {
    const bases = localNetworkBases();
    sendJson(res, 200, {
      host: HOST,
      port: PORT,
      localBaseUrl: `http://127.0.0.1:${PORT}`,
      lanBaseUrl: bases[0] ? bases[0].baseUrl : "",
      lanBases: bases
    });
    return;
  }

  if (url.pathname === "/api/style" && req.method === "GET") {
    syncTop3PhotoVersions();
    sendJson(res, 200, styleForClient());
    return;
  }

  if (url.pathname === "/api/style" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      const { stylePayload, runtimePayload, hasRuntimePayload } = splitStyleRuntimePayload(payload);
      styleState = migrateStyleState({ ...styleState, ...sanitizeStylePayload(stylePayload) });
      if (hasRuntimePayload) {
        runtimeState = cleanRuntimeState({ ...runtimeState, ...runtimePayload });
        await persistRuntimeState();
      }
      await persistStyleState();
      broadcast();
      sendJson(res, 200, { ok: true, style: styleForClient() });
    } catch (err) {
      sendApiError(res, 400, err, "Style update rejected");
    }
    return;
  }

  if (url.pathname === "/api/style-presets" && req.method === "GET") {
    const presets = await readStylePresets();
    sendJson(res, 200, { ok: true, presets: presets.map(publicStylePreset) });
    return;
  }

  if (url.pathname === "/api/style-presets" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body || "{}");
      const saved = await saveStylePreset(payload.name, payload.style || styleState, payload.id);
      sendJson(res, 200, {
        ok: true,
        preset: publicStylePreset(saved.preset),
        presets: saved.presets.map(publicStylePreset)
      });
    } catch (err) {
      sendApiError(res, 400, err, "Style preset save failed");
    }
    return;
  }

  if (url.pathname === "/api/style-presets/apply" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body || "{}");
      const presets = await readStylePresets();
      const preset = presets.find(item => item.id === String(payload.id || ""));
      if (!preset) throw new Error("Saved look not found.");
      styleState = migrateStyleState(preset.style);
      await persistStyleState();
      broadcast();
      sendJson(res, 200, {
        ok: true,
        style: styleForClient(),
        preset: publicStylePreset(preset),
        presets: presets.map(publicStylePreset)
      });
    } catch (err) {
      sendApiError(res, 400, err, "Style preset apply failed");
    }
    return;
  }

  if (url.pathname === "/api/style-presets/delete" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body || "{}");
      const id = String(payload.id || "");
      const presets = await readStylePresets();
      const next = presets.filter(item => item.id !== id);
      if (next.length === presets.length) throw new Error("Saved look not found.");
      await writeStylePresets(next);
      sendJson(res, 200, { ok: true, presets: next.map(publicStylePreset) });
    } catch (err) {
      sendApiError(res, 400, err, "Style preset delete failed");
    }
    return;
  }

  if (url.pathname === "/api/banner-logo" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      const result = await saveBannerLogos(payload.side, payload.dataUrls || payload.dataUrl);
      sendJson(res, 200, { ok: true, ...result });
    } catch (err) {
      sendJson(res, 400, { ok: false, error: err.message });
    }
    return;
  }

  if (url.pathname === "/api/top3-photo" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      const result = payload.clear
        ? await clearTop3Photo(payload.position)
        : await saveTop3Photo(payload.position, payload.dataUrl);
      sendJson(res, 200, { ok: true, ...result });
    } catch (err) {
      sendJson(res, 400, { ok: false, error: err.message });
    }
    return;
  }

  if (url.pathname === "/api/program" && req.method === "GET") {
    sendJson(res, 200, programState);
    return;
  }

  if (url.pathname === "/api/program" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      const allowed = new Set(["blank", "lineup", "running", "topbanner", "sidebanner", "results", "leaders", "top5", "podium", "weather", "ticker", "story"]);
      if (!allowed.has(payload.graphic)) throw new Error("Unknown graphic");
      if (!graphicAllowed(payload.graphic)) {
        throw new Error(`${EDITION_LABEL} edition does not include this graphic.`);
      }
      programState = { graphic: payload.graphic, version: Date.now() };
      if (!TOP_BANNER_PRESERVING_PROGRAM_GRAPHICS.has(payload.graphic) && topBannerState.visible !== false) {
        topBannerState = { visible: false, version: Date.now() };
        logEvent("info", "Banner visibility changed", { visible: false, source: "program-request", graphic: payload.graphic });
        broadcastTopBanner();
      }
      logEvent("info", "Program output selected", { graphic: payload.graphic });
      broadcastProgram();
      sendJson(res, 200, { ok: true, program: programState });
    } catch (err) {
      sendApiError(res, 400, err, "Program output update rejected");
    }
    return;
  }

  if (url.pathname === "/api/top-banner-output" && req.method === "GET") {
    sendJson(res, 200, topBannerState);
    return;
  }

  if (url.pathname === "/api/top-banner-output" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      topBannerState = { visible: payload.visible !== false, version: Date.now() };
      logEvent("info", "Banner visibility changed", { visible: topBannerState.visible });
      broadcastTopBanner();
      sendJson(res, 200, { ok: true, topBanner: topBannerState });
    } catch (err) {
      sendApiError(res, 400, err, "Banner output update rejected");
    }
    return;
  }

  if (url.pathname === "/api/vmix/browser" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      const target = await sendToVmixBrowser(payload);
      sendJson(res, 200, { ok: true, target });
    } catch (err) {
      sendApiError(res, 400, err, "vMix browser update failed");
    }
    return;
  }

  if (url.pathname === "/api/race-monitor/connect" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      resetRaceMonitorReconnectDelay();
      await connectRaceMonitor(payload.raceUrl);
      sendJson(res, 200, { ok: true, live: liveRaceMonitorState });
    } catch (err) {
      setLiveRaceMonitorState({ connected: false, connecting: false, error: err.message });
      sendApiError(res, 400, err, "Race Monitor connection failed");
    }
    return;
  }

  if (url.pathname === "/api/race-monitor/disconnect" && req.method === "POST") {
    disconnectRaceMonitor();
    sendJson(res, 200, { ok: true, live: liveRaceMonitorState });
    return;
  }

  if (url.pathname === "/api/myracepass/connect" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      await connectMyRacePass(payload.url);
      sendJson(res, 200, { ok: true, live: liveMyRacePassState });
    } catch (err) {
      setLiveMyRacePassState({ connected: false, connecting: false, error: err.message });
      sendApiError(res, 400, err, "MyRacePass connection failed");
    }
    return;
  }

  if (url.pathname === "/api/myracepass/disconnect" && req.method === "POST") {
    disconnectMyRacePass();
    sendJson(res, 200, { ok: true, live: liveMyRacePassState });
    return;
  }

  if (url.pathname === "/api/reset" && req.method === "POST") {
    resetTimingState();
    logEvent("info", "Live timing state reset");
    broadcast();
    sendJson(res, 200, { ok: true });
    return;
  }

  if (url.pathname === "/api/demo-race" && req.method === "POST") {
    try {
      if (demoRaceChild) {
        sendJson(res, 200, { ok: true, alreadyRunning: true });
        return;
      }
      const demoScript = path.join(__dirname, "scripts", "motion-demo.mjs");
      demoRaceChild = spawn(process.execPath, [demoScript], {
        env: { ...process.env, DEMO_BASE: `http://127.0.0.1:${PORT}` },
        stdio: "ignore"
      });
      demoRaceChild.on("exit", () => {
        demoRaceChild = null;
        logEvent("info", "Demo race finished");
      });
      demoRaceChild.on("error", err => {
        demoRaceChild = null;
        logEvent("warn", "Demo race failed", { error: errorMessage(err) });
      });
      logEvent("info", "Demo race started from control center");
      sendJson(res, 200, { ok: true });
    } catch (err) {
      demoRaceChild = null;
      sendApiError(res, 500, err, "Demo race failed to start");
    }
    return;
  }

  if (url.pathname === "/api/new-event" && req.method === "POST") {
    try {
      clearTop3SessionPhotosOnStartup();
      resetTimingState();
      runtimeState = cleanRuntimeState(DEFAULT_RUNTIME_STATE);
      await persistRuntimeState();
      touchRecentRunMarker();
      logEvent("info", "New event started: session photos cleared, timing and held story reset");
      broadcast();
      sendJson(res, 200, { ok: true });
    } catch (err) {
      sendApiError(res, 500, err, "New event reset failed");
    }
    return;
  }

  if (url.pathname === "/api/message" && req.method === "POST") {
    try {
      const body = await readBody(req);
      const payload = JSON.parse(body);
      const lines = String(payload.message || "")
        .split(/\r?\n/)
        .map(line => line.trim())
        .filter(Boolean);
      const accepted = lines.reduce((count, line) => count + applyMessage(line), 0);
      sendJson(res, 200, { ok: true, accepted, state: buildViews(timingState) });
    } catch (err) {
      sendJson(res, 400, { ok: false, error: err.message });
    }
    return;
  }

  serveStatic(req, res);
});

server.on("error", err => {
  // Exit 0 for configuration problems so the launchers stop with the message on
  // screen instead of restarting into the same failure every 3 seconds.
  if (err.code === "EADDRINUSE") {
    console.error(`${APP_NAME} could not start because port ${PORT} is already in use.`);
    console.error(`Close the other ${APP_NAME}/server window, or set a different PORT before starting.`);
    process.exit(0);
  }
  if (err.code === "EACCES" || err.code === "EPERM") {
    console.error(`${APP_NAME} could not start on ${HOST}:${PORT} because the operating system blocked access.`);
    console.error("Try running the launcher normally, or choose a different port.");
    process.exit(0);
  }
  logEvent("error", "Server failed to start", { error: errorMessage(err), code: err.code });
  process.exit(1);
});

process.on("unhandledRejection", err => {
  logEvent("error", "Unhandled async server error", { error: errorMessage(err) });
});

// The process is about to die when this fires; async log writes get lost.
// Write the stack synchronously so a crash in the field is diagnosable.
function writeCrashLogSync(kind, err) {
  try {
    const stamp = new Date().toISOString();
    const stack = err && err.stack ? err.stack : String(err);
    const line = `[${stamp}] FATAL ${kind}: ${stack}\n`;
    fs.appendFileSync(path.join(LOG_DIR, "crash.log"), line);
    fs.appendFileSync(path.join(LOG_DIR, `liberty-raceworks-${stamp.slice(0, 10)}.log`), line);
  } catch (_writeErr) {
    // Nothing else we can do this late.
  }
}

process.on("uncaughtExceptionMonitor", err => {
  writeCrashLogSync("uncaught exception", err);
  logEvent("error", "Uncaught server error", { error: errorMessage(err) });
});

if (wasRecentlyRunning()) {
  logEvent("info", "Recent run detected; keeping Top 3 session photos through restart");
} else {
  clearTop3SessionPhotosOnStartup();
}
touchRecentRunMarker();
const recentRunMarkerTimer = setInterval(touchRecentRunMarker, 5 * 60 * 1000);
if (recentRunMarkerTimer.unref) recentRunMarkerTimer.unref();

// Periodic license check-in: renews the signed token's offline grace and, for an
// online machine, applies a revocation/cancellation within one interval. A failed
// check-in (offline) is ignored by licenseManager.refresh, so the show goes on.
const LICENSE_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
function runLicenseCheckIn() {
  Promise.resolve(licenseManager.refresh())
    .then(state => {
      if (!licenseAllowsProgram(state)) {
        logEvent("warn", "License check-in returned not-allowed; program locked", { status: state.status });
      }
    })
    .catch(err => logEvent("warn", "License check-in error", { error: errorMessage(err) }));
}
// Give the server a moment to settle, then check in on a schedule.
const licenseCheckInStartTimer = setTimeout(runLicenseCheckIn, 15000);
if (licenseCheckInStartTimer.unref) licenseCheckInStartTimer.unref();
const licenseCheckInTimer = setInterval(runLicenseCheckIn, LICENSE_REFRESH_INTERVAL_MS);
if (licenseCheckInTimer.unref) licenseCheckInTimer.unref();

server.listen(PORT, HOST, () => {
  logEvent("info", "Liberty Raceworks Pro started", { host: HOST, port: PORT });
  console.log(`${APP_NAME} running at http://127.0.0.1:${PORT}`);
  localNetworkBases().forEach(item => {
    console.log(`vMix browser URL on ${item.name}: ${item.baseUrl}/program.html`);
  });
});
