function parseCsv(line) {
  const values = [];
  let value = "";
  let quoted = false;

  for (let i = 0; i < line.length; i += 1) {
    const char = line[i];
    const next = line[i + 1];

    if (char === '"' && quoted && next === '"') {
      value += '"';
      i += 1;
    } else if (char === '"') {
      quoted = !quoted;
    } else if (char === "," && !quoted) {
      values.push(value);
      value = "";
    } else {
      value += char;
    }
  }
  values.push(value);
  return values;
}

function parseRaceMonitorLine(rawLine) {
  const line = String(rawLine || "").trim();
  if (!line.startsWith("$")) return null;
  const parts = parseCsv(line);
  return {
    type: parts[0].slice(1),
    values: parts.slice(1),
    raw: line
  };
}

function splitRaceMonitorCommands(rawMessage) {
  return String(rawMessage || "")
    .replace(/\r?\n/g, " ")
    .split(/ (?=\$[A-Z])/)
    .map(line => line.trim())
    .filter(line => line.startsWith("$"));
}

const QUALIFYING_TEXT_PATTERN = /\b(?:qual(?:ifying|ifier|ifiers|s)?|practice|hot\s*laps?|hotlaps|time\s*trials?|best\s*lap)\b/i;

function isQualifyingText(value) {
  const text = String(value || "").toLowerCase();
  return QUALIFYING_TEXT_PATTERN.test(text) || /(?:^|[\s/._-])q(?:$|[\s/._-])/.test(text);
}

function getCompetitor(state, id) {
  if (!state.competitors.has(id)) {
    const roster = state.roster && state.roster.get(id) ? state.roster.get(id) : {};
    state.competitors.set(id, {
      id,
      number: roster.number || "",
      transponder: roster.transponder || "",
      firstName: roster.firstName || "",
      lastName: roster.lastName || "",
      hometown: roster.hometown || "",
      classNumber: roster.classNumber || "",
      className: "",
      position: null,
      racePosition: null,
      qualifyingPosition: null,
      laps: null,
      bestLap: "",
      lastLap: "",
      totalTime: "",
      gap: "",
      interval: "",
      status: "",
      updatedAt: ""
    });
  }
  return state.competitors.get(id);
}

function mergeIdentity(driver, identity = {}) {
  ["number", "transponder", "hometown", "classNumber", "status"].forEach(key => {
    const next = identity[key];
    if (isMissingRaceMonitorValue(next)) return;
    driver[key] = key === "hometown" ? cleanHometown(next) : next;
  });

  const existingName = cleanDriverName([driver.firstName, driver.lastName].filter(Boolean).join(" "));
  const nextName = cleanDriverName([identity.firstName, identity.lastName].filter(Boolean).join(" "));
  const existingScore = existingName.replace(/\s+/g, "").length + (driver.firstName && driver.lastName ? 20 : 0);
  const nextScore = nextName.replace(/\s+/g, "").length + (identity.firstName && identity.lastName ? 20 : 0);
  if (nextName && nextScore >= existingScore) {
    if (!isMissingRaceMonitorValue(identity.firstName)) driver.firstName = cleanDriverName(identity.firstName);
    if (!isMissingRaceMonitorValue(identity.lastName)) driver.lastName = cleanDriverName(identity.lastName);
  }
}

function positiveNumber(value) {
  const number = Number(value);
  return Number.isFinite(number) && number > 0 ? number : null;
}

function nextSequence(state) {
  const next = Number(state.sequence || 0) + 1;
  state.sequence = next;
  return next;
}

function applyRaceMonitorCommand(state, command) {
  const v = command.values;

  if (command.type === "B") {
    const nextSession = v[1] || "";
    if (nextSession && state.session && nextSession !== state.session) {
      state.competitors = new Map();
      if (state.roster) state.roster = new Map();
      state.classes = {};
      state.sequence = 0;
    }
    state.session = nextSession;
  }

  if (command.type === "A") {
    const [id, number, transponder, firstName, lastName, hometown, classNumber] = v;
    const identity = {
      number,
      transponder,
      firstName: cleanDriverName(firstName),
      lastName: cleanDriverName(lastName),
      hometown: cleanHometown(hometown),
      classNumber
    };
    if (!state.roster) state.roster = new Map();
    state.roster.set(id, { ...state.roster.get(id), ...identity });
    mergeIdentity(getCompetitor(state, id), identity);
  }

  if (command.type === "COMP") {
    const [id, number, classNumber, firstName, lastName, hometown, status] = v;
    const identity = {
      number,
      classNumber,
      firstName: cleanDriverName(firstName),
      lastName: cleanDriverName(lastName),
      hometown: cleanHometown(hometown),
      status: (status || "").trim()
    };
    if (!state.roster) state.roster = new Map();
    state.roster.set(id, { ...state.roster.get(id), ...identity });
    mergeIdentity(getCompetitor(state, id), identity);
  }

  if (command.type === "C") {
    state.classes[v[0]] = cleanClassName(v[1]);
  }

  if (command.type === "E") {
    if (v[0] === "TRACKNAME") state.trackName = v[1] || "";
    if (v[0] === "TRACKLENGTH") state.trackLength = v[1] || "";
  }

  if (command.type === "F") {
    const flagLabel = (v[4] || "").trim();
    state.lapsToGo = cleanLapsToGo(v[0]);
    state.timeToGo = cleanCountdownTime(v[1]);
    state.clock = v[2] || "";
    state.raceTime = v[3] || "";
    state.flag = flagLabel;
    state.flagCode = "";
    state.flagLabel = flagLabel;
  }

  if (command.type === "G") {
    const [position, id, laps, totalTime] = v;
    if (isMissingRaceMonitorValue(id)) return;
    const nextPosition = positiveNumber(position);
    Object.assign(getCompetitor(state, id), {
      position: nextPosition || getCompetitor(state, id).position,
      racePosition: nextPosition || getCompetitor(state, id).racePosition,
      raceOrderIndex: nextSequence(state),
      laps: Number(laps),
      totalTime,
      updatedAt: new Date().toISOString()
    });
  }

  if (command.type === "H") {
    const [position, id, laps, bestLap] = v;
    if (isMissingRaceMonitorValue(id)) return;
    const driver = getCompetitor(state, id);
    const nextPosition = positiveNumber(position);
    Object.assign(driver, {
      position: Number.isFinite(driver.racePosition) ? driver.racePosition : nextPosition || driver.position,
      qualifyingPosition: nextPosition || driver.qualifyingPosition,
      qualifyingOrderIndex: nextSequence(state),
      laps: Number.isFinite(driver.racePosition) ? driver.laps : Number(laps),
      bestLap,
      updatedAt: new Date().toISOString()
    });
  }

  if (command.type === "J") {
    const [id, lastLap, totalTime] = v;
    if (isMissingRaceMonitorValue(id)) return;
    Object.assign(getCompetitor(state, id), {
      lastLap,
      totalTime,
      updatedAt: new Date().toISOString()
    });
  }

  if (command.type === "I") {
    state.competitors = new Map();
    state.classes = {};
    state.sequence = 0;
  }

  if (command.type === "RMS") {
    state.mode = v[0] || "";
  }
}

function driverName(driver) {
  if (driver.fallbackName) return cleanDriverName(driver.fallbackName);
  const name = [driver.firstName, driver.lastName].filter(Boolean).join(" ").trim();
  return cleanDriverName(name) || (driver.number ? `Car ${driver.number}` : "");
}

function driverNumber(driver) {
  return preferredDriverNumber(driver);
}

function cleanDriverName(name) {
  if (isMissingRaceMonitorValue(name)) return "";
  return titleCaseAllCapsName(String(name || "")
    .replace(/\s*:\d{4,}\s*$/, "")
    .replace(/\s+/g, " ")
    .trim());
}

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

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

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

function cleanHometown(value) {
  if (isMissingRaceMonitorValue(value)) return "";
  const text = String(value || "").replace(/\s+/g, " ").trim();
  return isLikelyVehicleDescription(text) || isLikelyCarColor(text) ? "" : text;
}

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

function cleanDriverNumber(number) {
  return isMissingRaceMonitorValue(number) ? "" : String(number || "").trim();
}

function preferredDriverNumber(driver) {
  const number = cleanDriverNumber(driver && driver.number);
  const id = cleanDriverNumber(driver && driver.id);
  const transponder = cleanDriverNumber(driver && driver.transponder);
  const hasName = Boolean(cleanDriverName(driver && driver.fallbackName)
    || cleanDriverName([driver && driver.firstName, driver && driver.lastName].filter(Boolean).join(" ")));

  if (!hasName && id && id !== transponder) {
    if (!number) return id;
    if (id !== number && id.length > number.length && id.endsWith(number)) return id;
  }

  return number;
}

function cleanClassName(name, source = "") {
  const text = isMissingRaceMonitorValue(name) ? "" : String(name || "").trim();
  if (source !== "myracepass" && /^p\d+$/i.test(text)) return "";
  return text.replace(/^(?:replay|live)\s*-\s*/i, "").trim();
}

function cleanLapsToGo(value) {
  const text = isMissingRaceMonitorValue(value) ? "" : String(value || "").trim();
  const laps = Number(text);
  if (!Number.isFinite(laps) || laps >= 9999) return "";
  return text;
}

function cleanCountdownTime(value) {
  const text = isMissingRaceMonitorValue(value) ? "" : String(value || "").trim();
  return /^0{1,2}:0{2}:0{2}(?:\.0+)?$/.test(text) ? "" : text;
}

function lapTimeMs(value) {
  const text = String(value || "").trim();
  if (!text || /^0{1,2}:0{2}:0{2}(?:\.0+)?$/.test(text)) return null;
  if (!/^\d+(?::\d{1,2}){0,2}(?:\.\d+)?$/.test(text)) return null;
  const parts = text.split(":");
  if (parts.some(part => part === "")) return null;
  const seconds = Number(parts.pop());
  const minutes = parts.length ? Number(parts.pop()) : 0;
  const hours = parts.length ? Number(parts.pop()) : 0;
  if (![hours, minutes, seconds].every(Number.isFinite)) return null;
  return Math.round(((hours * 60 * 60) + (minutes * 60) + seconds) * 1000);
}

function formatDuration(ms) {
  if (!Number.isFinite(ms)) return "";
  const sign = ms < 0 ? "-" : "";
  const total = Math.abs(ms);
  const hours = Math.floor(total / 3600000);
  const minutes = Math.floor((total % 3600000) / 60000);
  const seconds = Math.floor((total % 60000) / 1000);
  const millis = total % 1000;
  const padded = value => String(value).padStart(2, "0");
  let text = `${padded(hours)}:${padded(minutes)}:${padded(seconds)}.${String(millis).padStart(3, "0")}`;
  while (text.startsWith("00:")) text = text.slice(3);
  if (text.startsWith("0") && !text.startsWith("0.")) text = text.slice(1);
  return `${sign}${text}`;
}

function lapDifference(value) {
  if (!Number.isFinite(value) || value <= 0) return "";
  return value === 1 ? "-1 LAP" : `-${value} LAPS`;
}

function updatedTimeMs(value) {
  const time = Date.parse(value || "");
  return Number.isFinite(time) ? time : null;
}

function hasDriverTimingRow(driver) {
  return Boolean(driver && (
    Number.isFinite(driver.position)
    || Number.isFinite(driver.racePosition)
    || Number.isFinite(driver.qualifyingPosition)
    || Number.isFinite(Number(driver.laps))
    || Number.isFinite(lapTimeMs(driver.bestLap))
    || Number.isFinite(lapTimeMs(driver.lastLap))
    || Number.isFinite(lapTimeMs(driver.totalTime))
  ));
}

function activeCompetitorValues(state) {
  const drivers = [...state.competitors.values()];
  if (state.source !== "racemonitor") return drivers;

  const latest = drivers.reduce((max, driver) => {
    const updated = updatedTimeMs(driver.updatedAt);
    return Number.isFinite(updated) ? Math.max(max, updated) : max;
  }, 0);
  if (!latest) return drivers;

  const cutoff = latest - 120000;
  const freshTimingRows = drivers.filter(driver => {
    const updated = updatedTimeMs(driver.updatedAt);
    return Number.isFinite(updated) && updated >= cutoff && hasDriverTimingRow(driver);
  });
  if (freshTimingRows.length < 2) return drivers;

  return drivers.filter(driver => {
    const updated = updatedTimeMs(driver.updatedAt);
    return Number.isFinite(updated) && updated >= cutoff && hasDriverTimingRow(driver);
  });
}

function isMissingRaceMonitorValue(value) {
  const text = String(value == null ? "" : value).trim();
  if (!text) return true;
  const normalized = text.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
  return [
    "missing data",
    "no data",
    "n/a",
    "na",
    "null",
    "undefined",
    "none",
    "-",
    "--"
  ].includes(normalized) || /\bmissing data\b/i.test(text);
}

function competitorsArray(state) {
  return activeCompetitorValues(state)
    .map(driver => {
      const displayNumber = preferredDriverNumber(driver);
      const name = cleanDriverName(driver.fallbackName) || cleanDriverName([driver.firstName, driver.lastName].filter(Boolean).join(" "));
      const missingData = Boolean(driver.missingData || /^missing-/i.test(String(driver.id || "")) || (driver.position && !displayNumber && !name));
      return {
        ...driver,
        displayNumber,
        name: missingData ? "" : name || (displayNumber ? `Car ${displayNumber}` : ""),
        missingData,
        className: cleanClassName(state.classes[driver.classNumber] || driver.className, state.source)
      };
    })
    .filter(driver => driver.displayNumber || driver.name)
    .sort((a, b) => {
      const ap = Number.isFinite(a.position) ? a.position : 9999;
      const bp = Number.isFinite(b.position) ? b.position : 9999;
      if (ap !== bp) return ap - bp;
      return String(a.displayNumber).localeCompare(String(b.displayNumber), undefined, { numeric: true });
    });
}

function positionedOrder(competitors, mode) {
  const positionKey = mode === "race" ? "racePosition" : "qualifyingPosition";
  const orderKey = mode === "race" ? "raceOrderIndex" : "qualifyingOrderIndex";
  const rows = competitors
    .filter(driver => mode === "qualifying"
      ? Number.isFinite(driver[positionKey]) || Number.isFinite(lapTimeMs(driver.bestLap))
      : Number.isFinite(driver[positionKey]) || Number.isFinite(driver.position))
    .map(driver => ({ ...driver }))
    .sort((a, b) => {
      if (mode === "qualifying") {
        const at = lapTimeMs(a.bestLap);
        const bt = lapTimeMs(b.bestLap);
        if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return at - bt;
        if (Number.isFinite(at) !== Number.isFinite(bt)) return Number.isFinite(at) ? -1 : 1;
      }
      const ap = Number.isFinite(a[positionKey]) ? a[positionKey] : (Number.isFinite(a.position) ? a.position : 9999);
      const bp = Number.isFinite(b[positionKey]) ? b[positionKey] : (Number.isFinite(b.position) ? b.position : 9999);
      if (ap !== bp) return ap - bp;
      const ao = Number(a[orderKey]);
      const bo = Number(b[orderKey]);
      if (Number.isFinite(ao) && Number.isFinite(bo) && ao !== bo) return ao - bo;
      return String(a.displayNumber).localeCompare(String(b.displayNumber), undefined, { numeric: true });
    })
    .map((driver, index) => ({
      ...driver,
      position: mode === "qualifying"
        ? index + 1
        : (Number.isFinite(driver[positionKey]) ? driver[positionKey] : driver.position)
    }));

  const leader = rows[0] || null;
  return rows.map((driver, index) => {
    const ahead = index > 0 ? rows[index - 1] : null;
    let gap = "";
    let diff = "";

    if (mode === "qualifying" && driver.position !== 1) {
      const driverBest = lapTimeMs(driver.bestLap);
      const aheadBest = lapTimeMs(ahead && ahead.bestLap);
      const leaderBest = lapTimeMs(leader && leader.bestLap);
      if (Number.isFinite(driverBest) && Number.isFinite(aheadBest)) gap = formatDuration(driverBest - aheadBest);
      if (Number.isFinite(driverBest) && Number.isFinite(leaderBest)) diff = formatDuration(driverBest - leaderBest);
    }

    if (mode === "race" && driver.position !== 1) {
      const driverLaps = Number(driver.laps);
      const aheadLaps = Number(ahead && ahead.laps);
      const leaderLaps = Number(leader && leader.laps);
      const driverTotal = lapTimeMs(driver.totalTime);
      const aheadTotal = lapTimeMs(ahead && ahead.totalTime);
      const leaderTotal = lapTimeMs(leader && leader.totalTime);
      if (Number.isFinite(driverLaps) && Number.isFinite(aheadLaps)) {
        gap = lapDifference(aheadLaps - driverLaps) || (Number.isFinite(driverTotal) && Number.isFinite(aheadTotal) ? formatDuration(driverTotal - aheadTotal) : "");
      }
      if (Number.isFinite(driverLaps) && Number.isFinite(leaderLaps)) {
        diff = lapDifference(leaderLaps - driverLaps) || (Number.isFinite(driverTotal) && Number.isFinite(leaderTotal) ? formatDuration(driverTotal - leaderTotal) : "");
      }
    }

    return {
      ...driver,
      interval: gap || driver.interval || "",
      gap: diff || driver.gap || ""
    };
  });
}

function buildViews(state) {
  const competitors = competitorsArray(state);
  const raceOrder = positionedOrder(competitors, "race");
  const qualifyingOrder = positionedOrder(competitors, "qualifying");
  const positionedCompetitors = competitors.filter(driver => Number.isFinite(driver.position));
  const lapsToGo = Number(state.lapsToGo);
  const sessionText = `${state.session || ""}`.toLowerCase();
  const modeText = `${state.mode || ""} ${sessionText}`;
  const activeMode = isQualifyingText(modeText)
      ? "qualifying"
      : Number.isFinite(lapsToGo) && lapsToGo > 0
        ? "race"
        : /\b(race|feature|heat|main|dash|b-main|a-main|competitors)\b/i.test(sessionText)
          && !isQualifyingText(sessionText)
          ? "race"
          : "race";
  const activeOrder = activeMode === "qualifying"
    ? (qualifyingOrder.length ? qualifyingOrder : (raceOrder.length ? raceOrder : positionedCompetitors))
    : (raceOrder.length ? raceOrder : (qualifyingOrder.length ? qualifyingOrder : positionedCompetitors));
  return {
    meta: {
      source: state.source,
      session: state.session,
      className: Object.values(state.classes).find(Boolean) || cleanClassName(state.session, state.source),
      trackName: state.trackName,
      trackLength: state.trackLength,
      weatherLocation: state.weatherLocation || state.trackName || "",
      mode: state.mode,
      clock: state.clock,
      lapsToGo: state.lapsToGo,
      raceTime: state.raceTime,
      timeToGo: state.timeToGo,
      flag: state.flag,
      flagCode: state.flagCode,
      flagLabel: state.flagLabel,
      lastUpdated: state.lastUpdated,
      live: state.live || null
    },
    classes: state.classes,
    competitors,
    lineup: activeOrder.length ? activeOrder : competitors,
    runningOrder: activeOrder,
    qualifyingOrder,
    raceOrder,
    results: activeOrder,
    leader: activeOrder.find(driver => driver.position === 1) || activeOrder[0] || competitors[0] || null
  };
}

module.exports = {
  splitRaceMonitorCommands,
  parseRaceMonitorLine,
  applyRaceMonitorCommand,
  buildViews
};
