const state = {
  hands: [],
  tournaments: [],
  heroes: new Set(),
  handIds: new Set(),
  tournamentIds: new Set(),
  selectedHero: "all",
  selectedTournamentId: "",
  selectedReplayHandId: "",
  selectedReplayActionIndex: 0,
  replayStackUnit: "bb",
  handNotes: {},
  activeTab: "dashboard",
  graphTimeFilter: "all",
  graphDateFrom: "",
  graphDateTo: "",
  statsTimeFilter: "all",
  statsDateFrom: "",
  statsDateTo: "",
  statsSection: "overview",
  srpScenario: "defender-oop",
  srpStreet: "flop",
  threeBetPotScenario: "defender-oop",
  threeBetPotStreet: "flop",
  blindWarSide: "bb",
  autoImportFolder: "",
  autoImportFolders: [],
  autoImportEnabled: false,
  hudEnabled: true,
  hudFloatingScale: 1,
  hudStats: ["hands", "vpip", "pfr", "threeBet", "foldToThreeBet", "cbetFlop", "foldToCbetFlop"],
  hudSecondRowEnabled: false,
  hudStatsSecondRow: ["fourBet", "squeeze", "cbetFlopIP", "cbetFlopOOP", "betVsMissedFlop", "skipCbetFoldFlop", "skipCbetRaiseFlop"],
};

const STORAGE_KEY = "mtt-edge-lab-v1";
const PARSER_VERSION = 35;
const EQUITY_VERSION = 4;
const HAND_HISTORY_TEXT_EXTENSIONS = [".txt", ".log", ".xml", ".hh", ".hhs"];
const COLLECTED_POT_SUFFIX_PATTERN = "(?:\\s+from(?:\\s+.+?)?\\s+pot)?";
const STATS_SCHEMA_VERSION = 9;
const equityCache = new Map();
const EQUITY_SUITS = ["c", "d", "h", "s"];
const EQUITY_SUIT_PERMUTATIONS = buildSuitPermutations(EQUITY_SUITS);
const EQUITY_EXACT_COMBO_LIMIT = 250000;
const EQUITY_HU_PREFLOP_SAMPLES = 32000;
const EQUITY_MULTI_PREFLOP_SAMPLES = 24000;
const EQUITY_HU_PARTIAL_SAMPLES = 26000;
const EQUITY_MULTI_PARTIAL_SAMPLES = 20000;
const replayCache = new Map();
const statPopupRegistry = new Map();
const statHandComboCache = new Map();
const statDecisionCache = new Map();
const statActionTimelineCache = new Map();
const statBetSizeCache = new Map();
const srpSpotCache = new Map();
let srpAnalysisJob = null;
let srpSelectedPath = "all";
const threeBetPotSpotCache = new Map();
let threeBetPotAnalysisJob = null;
let threeBetPotSelectedPath = "all";
const blindWarSpotCache = new Map();
let blindWarAnalysisJob = null;
const playerHudCache = { signature: "", stats: new Map() };
const opponentHudStats = new Map();
const opponentHudIndexedHandIds = new Set();
let opponentHudIndexCursor = -1;
let opponentHudIndexRunning = false;
let hudOverlayScaleOverride = null;
let currentReplayContext = null;
let replayActionListKey = "";
const statPopupUi = {
  key: "",
  tab: "range",
  filterType: "",
  filterValue: "",
  sourceHands: [],
  filteredHands: [],
  selectedHandId: "",
  actionIndex: 0,
  stackUnit: "bb",
};
let autoImportTimer = null;
let autoImportScanPromise = null;
let autoSaveTimer = null;
let autoSavePending = false;
let hudSyncTimer = null;
let backgroundStatsMigrationRunning = false;
let backgroundHandMigrationRunning = false;
let backgroundEvCalculationRunning = false;
const backgroundEvQueue = [];
const backgroundEvQueuedIds = new Set();
const autoImportSeen = new Map();
const sessionHandIds = new Set();
const liveHudTableSnapshots = new Map();

const HUD_STAT_OPTIONS = [
  { key: "hands", label: "H", name: "Hands", value: (stats) => hudCount(stats.hands) },
  { key: "vpip", label: "VP", name: "VPIP", value: (stats) => hudPct(stats.vpip, stats.hands) },
  { key: "pfr", label: "PF", name: "PFR", value: (stats) => hudPct(stats.pfr, stats.hands) },
  { key: "threeBet", label: "3B", name: "3bet", value: (stats) => hudPct(stats.threeBet, stats.threeBetOpp) },
  { key: "foldToThreeBet", label: "F3", name: "Fold to 3bet", value: (stats) => hudPct(stats.foldToThreeBet, stats.foldToThreeBetOpp) },
  { key: "fourBet", label: "4B", name: "4bet", value: (stats) => hudPct(stats.fourBet, stats.fourBetOpp) },
  { key: "squeeze", label: "SQ", name: "Squeeze", value: (stats) => hudPct(stats.squeeze, stats.squeezeOpp) },
  { key: "cbetFlop", label: "CB", name: "C-bet flop", value: (stats) => hudPct(stats.cbetFlop, stats.cbetFlopOpp) },
  { key: "cbetFlopIP", label: "CBI", name: "C-bet flop IP", value: (stats) => hudPct(stats.cbetFlopIP, stats.cbetFlopIPOpp) },
  { key: "cbetFlopOOP", label: "CBO", name: "C-bet flop OOP", value: (stats) => hudPct(stats.cbetFlopOOP, stats.cbetFlopOOPOpp) },
  { key: "cbetTurn", label: "CBT", name: "C-bet turn", value: (stats) => hudPct(stats.cbetTurn, stats.cbetTurnOpp) },
  { key: "cbetRiver", label: "CBR", name: "C-bet river", value: (stats) => hudPct(stats.cbetRiver, stats.cbetRiverOpp) },
  { key: "foldToCbetFlop", label: "FC", name: "Fold to c-bet flop", value: (stats) => hudPct(stats.foldToCbetFlop, stats.foldToCbetFlopOpp) },
  { key: "foldToCbetTurn", label: "FCT", name: "Fold to c-bet turn", value: (stats) => hudPct(stats.foldToCbetTurn, stats.foldToCbetTurnOpp) },
  { key: "foldToCbetRiver", label: "FCR", name: "Fold to c-bet river", value: (stats) => hudPct(stats.foldToCbetRiver, stats.foldToCbetRiverOpp) },
  { key: "betVsMissedFlop", label: "BVM", name: "Bet vs missed flop", value: (stats) => hudPct(stats.betVsMissedFlop, stats.betVsMissedFlopOpp) },
  { key: "betVsMissedTurn", label: "BVMT", name: "Bet vs missed turn", value: (stats) => hudPct(stats.betVsMissedTurn, stats.betVsMissedTurnOpp) },
  { key: "betVsMissedRiver", label: "BVMR", name: "Bet vs missed river", value: (stats) => hudPct(stats.betVsMissedRiver, stats.betVsMissedRiverOpp) },
  { key: "xrOopFlop", label: "XR", name: "XR OOP flop", value: (stats) => hudPct(stats.xrOopFlop, stats.xrOopFlopOpp) },
  { key: "xrOopTurn", label: "XRT", name: "XR OOP turn", value: (stats) => hudPct(stats.xrOopTurn, stats.xrOopTurnOpp) },
  { key: "xrOopRiver", label: "XRR", name: "XR OOP river", value: (stats) => hudPct(stats.xrOopRiver, stats.xrOopRiverOpp) },
  { key: "donkFlop", label: "DKF", name: "Donk flop", value: (stats) => hudPct(stats.donkFlop, stats.donkFlopOpp) },
  { key: "donkTurn", label: "DKT", name: "Donk turn", value: (stats) => hudPct(stats.donkTurn, stats.donkTurnOpp) },
  { key: "donkRiver", label: "DKR", name: "Donk river", value: (stats) => hudPct(stats.donkRiver, stats.donkRiverOpp) },
  { key: "foldVsBetFlop", label: "FBF", name: "Fold vs bet flop", value: (stats) => hudPct(stats.foldVsBetFlop, stats.foldVsBetFlopOpp) },
  { key: "foldVsBetTurn", label: "FBT", name: "Fold vs bet turn", value: (stats) => hudPct(stats.foldVsBetTurn, stats.foldVsBetTurnOpp) },
  { key: "foldVsBetRiver", label: "FBR", name: "Fold vs bet river", value: (stats) => hudPct(stats.foldVsBetRiver, stats.foldVsBetRiverOpp) },
  { key: "raiseIpFlop", label: "RIP", name: "Raise IP flop", value: (stats) => hudPct(stats.raiseIpFlop, stats.raiseIpFlopOpp) },
  { key: "raiseIpTurn", label: "RIT", name: "Raise IP turn", value: (stats) => hudPct(stats.raiseIpTurn, stats.raiseIpTurnOpp) },
  { key: "raiseIpRiver", label: "RIR", name: "Raise IP river", value: (stats) => hudPct(stats.raiseIpRiver, stats.raiseIpRiverOpp) },
  { key: "riverBet", label: "RB", name: "River bet", value: (stats) => hudPct(stats.riverBet, stats.riverBetOpp) },
  { key: "wwsf", label: "WWSF", name: "Won when saw flop", value: (stats) => hudPct(stats.wonWhenSawFlop, stats.sawFlop) },
  { key: "wtsd", label: "WTSD", name: "Went to showdown", value: (stats) => hudPct(stats.showedDown, stats.sawFlop) },
  { key: "wonShowdown", label: "W$SD", name: "Won at showdown", value: (stats) => hudPct(stats.wonShowdown, stats.showedDown) },
  { key: "skipCbetFoldFlop", label: "XF", name: "Skip CB e XF", value: (stats) => hudPct(stats.skipCbetFoldFlop, stats.skipCbetFlopOpp) },
  { key: "skipCbetRaiseFlop", label: "XR2", name: "Skip CB e XR", value: (stats) => hudPct(stats.skipCbetRaiseFlop, stats.skipCbetFlopOpp) },
];

const els = {
  bootScreen: document.querySelector("#bootScreen"),
  bootStatus: document.querySelector("#bootStatus"),
  fileInput: document.querySelector("#fileInput"),
  dropzone: document.querySelector("#dropzone"),
  heroSelect: document.querySelector("#heroSelect"),
  sidebarFilters: document.querySelector("#sidebarFilters"),
  positionFilter: document.querySelector("#positionFilter"),
  stackFilter: document.querySelector("#stackFilter"),
  blindLevelFilter: document.querySelector("#blindLevelFilter"),
  searchInput: document.querySelector("#searchInput"),
  dashboardTab: document.querySelector("#dashboardTab"),
  graphTab: document.querySelector("#graphTab"),
  replayerTab: document.querySelector("#replayerTab"),
  hudTab: document.querySelector("#hudTab"),
  statsTab: document.querySelector("#statsTab"),
  dashboardView: document.querySelector("#dashboardView"),
  graphView: document.querySelector("#graphView"),
  replayerView: document.querySelector("#replayerView"),
  hudView: document.querySelector("#hudView"),
  statsView: document.querySelector("#statsView"),
  hudToggle: document.querySelector("#hudToggle"),
  openFloatingHudButton: document.querySelector("#openFloatingHudButton"),
  hudSecondRowToggle: document.querySelector("#hudSecondRowToggle"),
  hudStatus: document.querySelector("#hudStatus"),
  hudConfigGrid: document.querySelector("#hudConfigGrid"),
  hudSecondConfigGrid: document.querySelector("#hudSecondConfigGrid"),
  hudPreview: document.querySelector("#hudPreview"),
  hudFloatingOverlay: document.querySelector("#hudFloatingOverlay"),
  hudFloatingPanel: document.querySelector("#hudFloatingPanel"),
  hudFloatingTitle: document.querySelector("#hudFloatingTitle"),
  hudFloatingContent: document.querySelector("#hudFloatingContent"),
  hudPlayerSelect: document.querySelector("#hudPlayerSelect"),
  metricsGrid: document.querySelector("#metricsGrid"),
  statsGrid: document.querySelector("#statsGrid"),
  statsCaption: document.querySelector("#statsCaption"),
  statsTimeFilter: document.querySelector("#statsTimeFilter"),
  statsDateRange: document.querySelector("#statsDateRange"),
  statsDateFrom: document.querySelector("#statsDateFrom"),
  statsDateTo: document.querySelector("#statsDateTo"),
  preflopStats: document.querySelector("#preflopStats"),
  postflopStats: document.querySelector("#postflopStats"),
  streetMatrix: document.querySelector("#streetMatrix"),
  threeBetByPosition: document.querySelector("#threeBetByPosition"),
  rfiByPosition: document.querySelector("#rfiByPosition"),
  ccByPosition: document.querySelector("#ccByPosition"),
  positionReportTable: document.querySelector("#positionReportTable"),
  srpStatsSection: document.querySelector("#statsSrpSection"),
  srpStatsContent: document.querySelector("#srpStatsContent"),
  srpSampleCaption: document.querySelector("#srpSampleCaption"),
  srpPathBar: document.querySelector("#srpPathBar"),
  srpPathSelect: document.querySelector("#srpPathSelect"),
  srpLoading: document.querySelector("#srpLoading"),
  threeBetPotStatsContent: document.querySelector("#threeBetPotStatsContent"),
  threeBetPotSampleCaption: document.querySelector("#threeBetPotSampleCaption"),
  threeBetPotPathBar: document.querySelector("#threeBetPotPathBar"),
  threeBetPotPathSelect: document.querySelector("#threeBetPotPathSelect"),
  threeBetPotLoading: document.querySelector("#threeBetPotLoading"),
  blindWarStatsContent: document.querySelector("#blindWarStatsContent"),
  blindWarSampleCaption: document.querySelector("#blindWarSampleCaption"),
  blindWarLoading: document.querySelector("#blindWarLoading"),
  stackBars: document.querySelector("#stackBars"),
  positionTable: document.querySelector("#positionTable"),
  handsTable: document.querySelector("#handsTable"),
  handCount: document.querySelector("#handCount"),
  chartCaption: document.querySelector("#chartCaption"),
  saveButton: document.querySelector("#saveButton"),
  resetButton: document.querySelector("#resetButton"),
  importStatus: document.querySelector("#importStatus"),
  chooseAutoImportFolder: document.querySelector("#chooseAutoImportFolder"),
  detectAutoImportFolders: document.querySelector("#detectAutoImportFolders"),
  toggleAutoImport: document.querySelector("#toggleAutoImport"),
  autoImportStatus: document.querySelector("#autoImportStatus"),
  autoImportFolderList: document.querySelector("#autoImportFolderList"),
  tournamentCount: document.querySelector("#tournamentCount"),
  tournamentsTable: document.querySelector("#tournamentsTable"),
  resultsGraph: document.querySelector("#resultsGraph"),
  graphSummary: document.querySelector("#graphSummary"),
  graphTimeFilter: document.querySelector("#graphTimeFilter"),
  graphDateRange: document.querySelector("#graphDateRange"),
  graphDateFrom: document.querySelector("#graphDateFrom"),
  graphDateTo: document.querySelector("#graphDateTo"),
  tournamentSearch: document.querySelector("#tournamentSearch"),
  replayDateFrom: document.querySelector("#replayDateFrom"),
  replayDateTo: document.querySelector("#replayDateTo"),
  tournamentTypeFilter: document.querySelector("#tournamentTypeFilter"),
  tournamentList: document.querySelector("#tournamentList"),
  replayHandFilter: document.querySelector("#replayHandFilter"),
  replayHandList: document.querySelector("#replayHandList"),
  replayTable: document.querySelector("#replayTable"),
  replayActionList: document.querySelector("#replayActionList"),
  replayMetrics: document.querySelector("#replayMetrics"),
  replayNotes: document.querySelector("#replayNotes"),
  replayHandTitle: document.querySelector("#replayHandTitle"),
  replayHandMeta: document.querySelector("#replayHandMeta"),
  prevReplayHand: document.querySelector("#prevReplayHand"),
  nextReplayHand: document.querySelector("#nextReplayHand"),
  prevReplayAction: document.querySelector("#prevReplayAction"),
  nextReplayAction: document.querySelector("#nextReplayAction"),
  openReplayWindowButton: document.querySelector("#openReplayWindowButton"),
  replayStepLabel: document.querySelector("#replayStepLabel"),
  replayStackUnit: document.querySelector("#replayStackUnit"),
  statPopup: document.querySelector("#statPopup"),
  statPopupTitle: document.querySelector("#statPopupTitle"),
  statPopupMeta: document.querySelector("#statPopupMeta"),
  statPopupWorkspace: document.querySelector("#statPopupWorkspace"),
  statPopupSummary: document.querySelector("#statPopupSummary"),
  statPopupTabs: document.querySelector("#statPopupTabs"),
  statPopupContent: document.querySelector("#statPopupContent"),
  statPopupHandsPanel: document.querySelector("#statPopupHandsPanel"),
  statPopupHandsTitle: document.querySelector("#statPopupHandsTitle"),
  statPopupHandsMeta: document.querySelector("#statPopupHandsMeta"),
  statPopupList: document.querySelector("#statPopupList"),
  statPopupReplayPanel: document.querySelector("#statPopupReplayPanel"),
  statPopupReplayTitle: document.querySelector("#statPopupReplayTitle"),
  statPopupReplayMeta: document.querySelector("#statPopupReplayMeta"),
  statPopupReplayStep: document.querySelector("#statPopupReplayStep"),
  statPopupReplayUnit: document.querySelector("#statPopupReplayUnit"),
  statPopupReplayTable: document.querySelector("#statPopupReplayTable"),
  statPopupClose: document.querySelector("#statPopupClose"),
};

function pct(value, total) {
  if (!total) return "0%";
  return `${Math.round((value / total) * 100)}%`;
}

function toNumber(value) {
  if (value === null || value === undefined) return null;
  const source = String(value).trim();
  if (!/\d/.test(source)) return null;
  const negative = /^\s*-/.test(source);
  let cleaned = source.replace(/[^\d,.]/g, "");
  const commaIndex = cleaned.lastIndexOf(",");
  const dotIndex = cleaned.lastIndexOf(".");
  if (commaIndex >= 0 && dotIndex >= 0) {
    if (commaIndex > dotIndex) cleaned = cleaned.replace(/\./g, "").replace(",", ".");
    else cleaned = cleaned.replace(/,/g, "");
  } else if (commaIndex >= 0) {
    const groups = cleaned.split(",");
    const thousands = groups.length > 2 || (groups.length === 2 && groups[1].length === 3);
    cleaned = thousands ? groups.join("") : groups.join(".");
  } else if (dotIndex >= 0) {
    const groups = cleaned.split(".");
    const thousands = groups.length > 2 || (groups.length === 2 && groups[1].length === 3);
    if (thousands) cleaned = groups.join("");
  }
  const number = Number(`${negative ? "-" : ""}${cleaned}`);
  return Number.isFinite(number) ? number : null;
}

function fmt(n) {
  if (n === null || n === undefined || Number.isNaN(n)) return "-";
  return new Intl.NumberFormat("pt-BR").format(n);
}

function setBootStatus(message) {
  if (els.bootStatus) els.bootStatus.textContent = message;
}

function hideBootScreen() {
  if (!els.bootScreen) return;
  els.bootScreen.classList.add("is-hidden");
  window.setTimeout(() => {
    if (els.bootScreen) els.bootScreen.remove();
  }, 240);
}

const IPOKER_SOURCE = String.raw`Champion(?:\s+Poker)?|iPoker|Bet365|Betfair(?:\s+Poker)?|William\s+Hill|Paddy\s+Power|NetBet|Boyle(?:\s+Sports|sports)|Coral(?:\s+Poker)?|Grosvenor(?:\s+Poker)?|Betfred|Redstar(?:\s+Poker)?|Titan(?:\s+Poker)?|Winner(?:\s+Poker)?|Mansion(?:\s+Poker)?`;
const HAND_START_SOURCE = String.raw`(?:(?:PokerStars|PokerKing|${IPOKER_SOURCE}|Chico|TigerGaming|Tiger\s+Gaming|BetOnline|Bet\s+Online|SportsBetting|Sports\s+Betting|H2N(?:\s+XML)?|Poker)\s+)?(?:Game\s+)?Hand\s+#|\*{5}\s+Hand History For Game`;
const HAND_START_RE = new RegExp(`(^|\\n)${HAND_START_SOURCE}`, "i");
const HAND_BOUNDARY_RE = new RegExp(`^(?:${HAND_START_SOURCE})`, "gim");
const AMOUNT_SOURCE = String.raw`[$€£]?\s*([\d,.]+)`;

function splitHands(text) {
  const normalized = text.replace(/\r/g, "");
  if (!HAND_START_RE.test(normalized)) return [];
  const starts = [...normalized.matchAll(HAND_BOUNDARY_RE)].map((match) => match.index);
  return starts.map((start, index) => normalized.slice(start, starts[index + 1] ?? normalized.length).trim()).filter(Boolean);
}

function parseHeader(firstLine, lines = []) {
  const partyHeader = parsePartyHeader(firstLine, lines);
  if (partyHeader) return partyHeader;
  const wpnHeader = parseWpnHeader(firstLine);
  if (wpnHeader) return wpnHeader;
  const chicoHeader = parseChicoHeader(firstLine);
  if (chicoHeader) return chicoHeader;

  const sitePrefix = String.raw`(?:(PokerStars|PokerKing|${IPOKER_SOURCE}|Chico|TigerGaming|Tiger\s+Gaming|BetOnline|Bet\s+Online|SportsBetting|Sports\s+Betting|H2N(?:\s+XML)?|Poker)\s+)?`;
  const handPrefix = String.raw`${sitePrefix}(?:Game\s+)?Hand\s+#([A-Z]*\d+)`;
  const tournamentGameText = String.raw`(.+?)\s+Hold'?em\s+No\s+Limit`;
  const cashGameText = String.raw`(.*?)\s*Hold'?em\s+No\s+Limit`;
  const blindsText = String.raw`\(\s*${AMOUNT_SOURCE}\s*\/\s*${AMOUNT_SOURCE}(?:\s*\/\s*${AMOUNT_SOURCE})?(?:\s*\([\d,.]+\))?(?:\s*[A-Z]{2,4})?\s*\)`;
  const tournamentRe = new RegExp(`^${handPrefix}:\\s*Tournament\\s+#([A-Z]*\\d+),\\s*${tournamentGameText}\\s*-\\s*(?:Level\\s*)?([^()\\-]+?)\\s*${blindsText}\\s*-\\s*(.+)$`, "i");
  const cashRe = new RegExp(`^${handPrefix}:\\s*${cashGameText}\\s*(?:-\\s*)?${blindsText}\\s*-\\s*(.+)$`, "i");

  let match = firstLine.match(tournamentRe);
  if (match) {
    return {
      site: normalizeSiteName(match[1]),
      handId: match[2],
      tournamentId: match[3],
      buyIn: match[4].trim(),
      level: match[5].trim(),
      smallBlind: toNumber(match[6]),
      bigBlind: toNumber(match[7]),
      ante: toNumber(match[8]),
      date: match[9].trim(),
    };
  }

  match = firstLine.match(cashRe);
  if (!match) return null;
  const site = normalizeSiteName(match[1]);
  const smallBlind = toNumber(match[4]);
  const bigBlind = toNumber(match[5]);
  const date = match[7].trim();
  const stakeLabel = cashStakeLabel(match[3], smallBlind, bigBlind);
  return {
    site,
    handId: match[2],
    tournamentId: cashSessionId(site, stakeLabel, date),
    buyIn: stakeLabel,
    level: "Cash",
    isCashGame: true,
    smallBlind,
    bigBlind,
    ante: toNumber(match[6]),
    date,
  };
}

function parseWpnHeader(firstLine) {
  const match = String(firstLine || "").match(
    /^Game Hand #([A-Z]*\d+)\s*-\s*Tournament #([A-Z]*\d+)\s*-\s*Hold'?em\s*\(No Limit\)\s*-\s*Level\s*([^\s(]+)\s*\(\s*([\d,.]+)\s*\/\s*([\d,.]+)(?:\s*\/\s*([\d,.]+))?\s*\)\s*-\s*(.+)$/i
  );
  if (!match) return null;
  return {
    site: "WPN",
    handId: match[1],
    tournamentId: match[2],
    buyIn: "Tournament",
    level: match[3],
    smallBlind: toNumber(match[4]),
    bigBlind: toNumber(match[5]),
    ante: toNumber(match[6]),
    date: match[7].trim(),
  };
}

function parseChicoHeader(firstLine) {
  const match = String(firstLine || "").match(
    /^Chico Hand #([A-Z]*\d+):\s*Tournament #([A-Z]*\d+),\s*(.*?)\s+Hold'?em\s+No Limit\s*-\s*Level\s*([^()]*)\(\s*([\d,.]+)\s*\/\s*([\d,.]+)(?:\s*-\s*Ante\s+([\d,.]+))?\s*\)\s*-\s*(.+)$/i
  );
  if (!match) return null;
  return {
    site: "Chico",
    handId: match[1],
    tournamentId: match[2],
    buyIn: match[3].trim() || "Tournament",
    level: match[4].trim() || "Tournament",
    smallBlind: toNumber(match[5]),
    bigBlind: toNumber(match[6]),
    ante: toNumber(match[7]),
    date: match[8].trim(),
  };
}

function normalizeSiteName(site) {
  const value = String(site || "").replace(/\s+/g, " ").trim().toLowerCase();
  if (value === "pokerstars") return "PokerStars";
  if (value === "pokerking") return "PokerKing";
  if (value === "champion" || value === "champion poker") return "Champion";
  if (["ipoker", "bet365", "betfair", "betfair poker", "william hill", "paddy power", "netbet", "boyle sports", "boylesports", "coral", "coral poker", "grosvenor", "grosvenor poker", "betfred", "redstar", "redstar poker", "titan", "titan poker", "winner", "winner poker", "mansion", "mansion poker"].includes(value)) return "iPoker";
  if (["chico", "tigergaming", "tiger gaming", "betonline", "bet online", "sportsbetting", "sports betting"].includes(value)) return "Chico";
  if (value === "wpn" || value === "winning poker network") return "WPN";
  if (value === "h2n" || value === "h2n xml") return "H2N XML";
  if (value === "partypoker" || value === "party poker" || value === "party") return "PartyPoker";
  if (value === "poker") return "GGPoker/WPN";
  return "Poker";
}

function isIpokerFamilySite(site) {
  return ["Champion", "iPoker"].includes(site);
}

function isIpokerFamilyHand(firstLine = "") {
  return new RegExp(`^(?:${IPOKER_SOURCE})\\s+Hand\\s+#`, "i").test(String(firstLine || ""));
}

function isIncompleteIpokerSnapshot(hand) {
  if (!hand?.raw) return false;
  const raw = String(hand.raw || "");
  const firstLine = raw.split(/\r?\n/, 1)[0] || "";
  if (!isIpokerFamilySite(hand.site) && !isIpokerFamilyHand(firstLine)) return false;
  if (!/\[\?\? \?\?\]/.test(raw)) return false;
  const lines = raw.replace(/\r/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
  const totalPot = parseTotalPot(lines);
  const hasPositiveCollectedAction = lines.some((line) => {
    if (/\bbalance\b/i.test(line)) return false;
    const amount = toNumber(line.match(/ collected (?:\(?)(?:[$€£]?\s*)?([\d,.]+)/i)?.[1]);
    return Number.isFinite(amount) && amount > 0;
  });
  return (!Number.isFinite(totalPot) || totalPot <= 0) && !hasPositiveCollectedAction;
}

function shouldStoreHand(hand) {
  return Boolean(hand) && !isIncompleteIpokerSnapshot(hand);
}

function shouldRefreshAllInEquityForSite(site) {
  return ["Champion", "iPoker", "Chico", "WPN", "PokerKing", "PartyPoker"].includes(site);
}

function parseReturnedBetLine(line) {
  const normalized = String(line || "").trim();
  const patterns = [
    /^Uncalled bet\s+\(?[$€£]?\s*([\d,.]+)\)?\s+returned to\s+(.+)$/i,
    /^Uncalled bet of\s+[$€£]?\s*([\d,.]+)\s+returned to\s+(.+)$/i,
    /^[$€£]?\s*([\d,.]+)\s+returned to\s+(.+)$/i,
  ];
  for (const pattern of patterns) {
    const match = normalized.match(pattern);
    if (match) return { amount: toNumber(match[1]) || 0, player: match[2].trim() };
  }
  return null;
}

function parsePartyHeader(firstLine, lines) {
  const idMatch = String(firstLine || "").match(/^\*{5}\s+Hand History For Game\s+([A-Z0-9a-z_-]+)\s+\*{5}$/i);
  if (!idMatch) return null;
  const infoLine = lines.find((line) => /\bTexas Holdem Game Table\b/i.test(line)) || "";
  const match = infoLine.match(/^([\d,.]+)\s*\/\s*([\d,.]+)\s+(Tourney\s+)?Texas Holdem Game Table\s+\(NL\)\s+(?:\(MTT Tournament #([A-Z0-9_-]+)\)\s+)?(?:\(Buyin\s+([^)]+)\)\s+)?-\s+(.+)$/i);
  if (!match) return null;
  const smallBlind = toNumber(match[1]);
  const bigBlind = toNumber(match[2]);
  const tournamentId = match[4] || `party-${idMatch[1]}`;
  const buyIn = (match[5] || "Cash").trim();
  const date = match[6].trim();
  const isTournament = Boolean(match[3] || match[4]);
  const stakeLabel = cashStakeLabel("", smallBlind, bigBlind);
  return {
    site: "PartyPoker",
    handId: idMatch[1],
    tournamentId: isTournament ? tournamentId : cashSessionId("PartyPoker", stakeLabel, date),
    buyIn: isTournament ? buyIn : stakeLabel,
    level: isTournament ? "Tourney" : "Cash",
    isCashGame: !isTournament,
    smallBlind,
    bigBlind,
    ante: parsePartyAnte(lines),
    date,
  };
}

function parsePartyAnte(lines) {
  const line = lines.find((item) => /\bposts ante\b/i.test(item));
  const match = line?.match(/\bposts ante\s+\(?[$€£]?\s*([\d,.]+)\)?/i);
  return match ? toNumber(match[1]) : null;
}

function cashStakeLabel(gameText, smallBlind, bigBlind) {
  const label = String(gameText || "").trim().replace(/\s+-\s*$/, "");
  if (label) return label;
  if (Number.isFinite(smallBlind) && Number.isFinite(bigBlind)) return `Cash ${smallBlind}/${bigBlind}`;
  return "Cash";
}

function cashSessionId(site, stakeLabel, date) {
  const day = handDateKey(date) || "sem-data";
  return `cash-${site || "site"}-${slug(stakeLabel)}-${day}`;
}

function slug(value) {
  return String(value || "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "") || "cash";
}

function parseSeats(lines) {
  const seats = [];
  const seatLinesEnd = lines.findIndex((line) => line.startsWith("*** HOLE CARDS ***"));
  const candidateLines = seatLinesEnd >= 0 ? lines.slice(0, seatLinesEnd) : lines;
  for (const line of candidateLines) {
    if (/\b(showed|folded|won|lost|collected|mucked|showed down)\b/i.test(line)) continue;
    const match = line.match(/^Seat\s+(\d+):\s+(.+?)\s+\((?:[$€£]\s*)?([\d,.]+)(?:\s+in chips)?(?:[^)]*)\)(.*)$/i);
    if (match) {
      seats.push({
        seat: toNumber(match[1]),
        name: match[2].trim(),
        chips: toNumber(match[3]),
        bounty: parseBountyAmount(line),
      });
    }
  }
  return seats.sort((a, b) => a.seat - b.seat);
}

function parseBountyAmount(line) {
  const text = String(line || "");
  const patterns = [
    /\bbounty\s*:?\s*[$€£]?\s*([\d,.]+)/i,
    /\bprogressive\s+bounty\s*:?\s*[$€£]?\s*([\d,.]+)/i,
    /\bknockout\s+bounty\s*:?\s*[$€£]?\s*([\d,.]+)/i,
    /\bbounty\s+[$€£]?\s*([\d,.]+)/i,
    /[$€£]\s*([\d,.]+)\s+(?:progressive\s+|knockout\s+)?bounty\b/i,
  ];
  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (match) return money(match[1]);
  }
  return null;
}

function buyInPartsFromText(value) {
  const text = String(value || "");
  const currencyValues = moneyValues(text);
  if (currencyValues.length >= 2) return currencyValues;

  const plusMatch = text.match(/(?:[$€£]?\s*\d+(?:[,.]\d+)?\s*\+\s*)+[$€£]?\s*\d+(?:[,.]\d+)?/);
  if (!plusMatch) return currencyValues;

  const values = plusMatch[0]
    .split("+")
    .map((part) => money(part))
    .filter((part) => part > 0);
  return values.length ? values : currencyValues;
}

function bountyFromBuyIn(value) {
  const parts = buyInPartsFromText(value);
  return parts.length >= 3 && parts[1] > 0 ? parts[1] : null;
}

function isBountyTournamentText(...values) {
  const text = values.filter(Boolean).join(" ");
  return /bounty|mystery|knockout|pko|ko/i.test(text) || Number(bountyFromBuyIn(text)) > 0;
}

function canUseBuyInBountyFallback(hand) {
  return ["PokerStars", "PokerKing", "Champion", "iPoker"].includes(hand?.site);
}

function seatBountyValue(hand, seat) {
  const explicit = Number(seat?.bounty);
  if (Number.isFinite(explicit) && explicit > 0) return explicit;
  if (!canUseBuyInBountyFallback(hand)) return null;
  return bountyFromBuyIn(hand?.buyIn);
}

function hasVisibleBounties(hand, seats = []) {
  if (!isBountyTournamentText(hand?.buyIn, hand?.tournamentName)) return false;
  return seats.some((seat) => Number(seatBountyValue(hand, seat)) > 0);
}

function assignPositions(seats, buttonSeat) {
  const ordered = [...seats].sort((a, b) => a.seat - b.seat);
  const btnIndex = ordered.findIndex((p) => p.seat === buttonSeat);
  const startIndex = btnIndex >= 0 ? btnIndex : 0;
  const clockwise = [];
  for (let i = 0; i < ordered.length; i += 1) {
    clockwise.push(ordered[(startIndex + i) % ordered.length]);
  }

  const byCount = {
    2: ["BTN", "BB"],
    3: ["BTN", "SB", "BB"],
    4: ["BTN", "SB", "BB", "CO"],
    5: ["BTN", "SB", "BB", "UTG", "CO"],
    6: ["BTN", "SB", "BB", "UTG", "HJ", "CO"],
    7: ["BTN", "SB", "BB", "UTG", "UTG+1", "HJ", "CO"],
    8: ["BTN", "SB", "BB", "UTG", "UTG+1", "MP", "HJ", "CO"],
    9: ["BTN", "SB", "BB", "UTG", "UTG+1", "MP", "LJ", "HJ", "CO"],
  };
  const labels = byCount[clockwise.length] || byCount[9];
  return new Map(clockwise.map((player, index) => [player.name, labels[index] || `P${index + 1}`]));
}

function parsePreflop(lines) {
  const start = lines.findIndex((line) => line === "*** HOLE CARDS ***" || /^\*\* Dealing down cards \*\*/i.test(line));
  const endCandidates = ["*** FLOP ***", "*** SUMMARY ***", "** Dealing Flop **", "** Summary **"];
  let end = lines.findIndex((line, index) => index > start && endCandidates.some((marker) => line.startsWith(marker)));
  if (start < 0) return [];
  if (end < 0) end = lines.length;
  return lines.slice(start + 1, end).filter((line) => parseActionLine(line) || isAllInMarkerLine(line));
}

function parseStreetActions(lines, streetName) {
  const marker = `*** ${streetName} ***`;
  const partyMarker = `** Dealing ${streetName[0]}${streetName.slice(1).toLowerCase()} **`;
  const start = lines.findIndex((line) => line.startsWith(marker) || line.startsWith(partyMarker));
  if (start < 0) return [];
  const markers = ["*** FLOP ***", "*** TURN ***", "*** RIVER ***", "*** SHOWDOWN ***", "*** SHOW DOWN ***", "*** SUMMARY ***", "** Dealing Flop **", "** Dealing Turn **", "** Dealing River **", "** Summary **"];
  let end = lines.findIndex((line, index) => index > start && markers.some((streetMarker) => line.startsWith(streetMarker)));
  if (end < 0) end = lines.length;
  return parseActionsFromLines(lines.slice(start + 1, end));
}

function parseActionLine(line) {
  const normalized = String(line || "").trim();
  let match = normalized.match(/^(.+?): (folds|checks|calls|bets|raises)(.*)$/i);
  if (!match) {
    match = normalized.match(/^(.+?)\s+(folds|checks|calls|bets|raises)\b(.*)$/i);
  }
  if (!match) return null;
  const [, player, rawVerb, tail] = match;
  const verb = rawVerb.toLowerCase();
  const amountMatch = tail.match(/[$€£]?\s*([\d,.]+)(?!.*\d)/);
  return {
    player: player.trim(),
    verb,
    amount: amountMatch ? toNumber(amountMatch[1]) : 0,
    allIn: /all-?in/i.test(tail),
    raw: normalized,
  };
}

function isAllInMarkerLine(line) {
  return /^(.+?)\s+is\s+all-?In\.?$/i.test(String(line || "").trim());
}

function parseActionsFromLines(lines) {
  const actions = [];
  for (const line of lines || []) {
    const action = parseActionLine(line);
    if (action) {
      actions.push(action);
      continue;
    }
    const allIn = String(line || "").trim().match(/^(.+?)\s+is\s+all-?In\.?$/i);
    if (allIn) {
      const player = allIn[1].trim();
      const previous = [...actions].reverse().find((item) => item.player === player);
      if (previous) previous.allIn = true;
    }
  }
  return actions;
}

function parseResult(lines, hero) {
  const amountText = "[$€£]?\\s*([\\d,.]+)";
  const collected = lines
    .map((line) => line.match(new RegExp(`^${escapeRegExp(hero)} collected \\(?${amountText}\\)?${COLLECTED_POT_SUFFIX_PATTERN}`, "i")))
    .filter(Boolean)
    .map((match) => toNumber(match[1]))
    .reduce((sum, value) => sum + value, 0);

  const summaryLine = lines.find((line) => line.startsWith("Seat ") && line.includes(hero));
  const partyLine = partyPlayerSummaryLine(lines, hero);
  if (partyLine) {
    const net = partyLine.match(/\bnet\s+([+-]?[\d,.]+)/i);
    if (net) return Math.max(0, toNumber(net[1]) || 0);
    if (/\blost\b|\(folded\)/i.test(partyLine)) return 0;
    const partyCollected = partyLine.match(/\bcollected\s+([\d,.]+)/i);
    if (partyCollected) return toNumber(partyCollected[1]);
  }
  const lost = summaryLine ? / lost |folded/.test(summaryLine) : false;
  return collected > 0 ? collected : lost ? 0 : null;
}

function parseHeroEndingChips(lines, hero) {
  const escaped = escapeRegExp(hero);
  const amountText = "[$€£]?\\s*([\\d,.]+)";
  const summaryLine = lines.find((line) => new RegExp(`^Seat \\d+: ${escaped}(?: \\([^)]*\\))*`).test(line));
  const partyLine = partyPlayerSummaryLine(lines, hero);
  if (partyLine) {
    const balance = partyLine.match(/\bbalance\s+([\d,.]+)/i);
    if (balance) return toNumber(balance[1]);
  }
  if (!summaryLine) return null;
  if (/lost|folded|showed .* lost/.test(summaryLine)) return 0;
  const collectedMatch = summaryLine.match(/(?:won|collected) \((?:[$€£]?\s*)?([\d,.]+)\)/);
  if (collectedMatch) return toNumber(collectedMatch[1]);
  const lineCollected = lines
    .map((line) => line.match(new RegExp(`^${escaped} collected \\(?${amountText}\\)?${COLLECTED_POT_SUFFIX_PATTERN}`, "i")))
    .filter(Boolean)
    .map((match) => toNumber(match[1]))
    .reduce((sum, value) => sum + (value || 0), 0);
  return lineCollected || null;
}

function partyPlayerSummaryLine(lines, player) {
  if (!player) return "";
  const escaped = escapeRegExp(player);
  return lines.find((line) => new RegExp(`^${escaped}\\s+balance\\b`, "i").test(line)) || "";
}

function playerSummaryWonAmount(lines, player) {
  if (!player) return null;
  const escaped = escapeRegExp(player);
  const summary = lines.find((line) => new RegExp(`^Seat \\d+: ${escaped}\\b.*?\\bwon\\s+\\(?[$€£]?\\s*[\\d,.]+`, "i").test(line));
  if (!summary) return null;
  const match = summary.match(/\bwon\s+\(?[$€£]?\s*([\d,.]+)\)?/i);
  return match ? toNumber(match[1]) : null;
}

function calculateHeroMoney(lines, hero) {
  if (!hero) return { netChips: null, investedChips: null, collectedChips: null };
  const partyLine = partyPlayerSummaryLine(lines, hero);
  if (partyLine) {
    const netMatch = partyLine.match(/\bnet\s+([+-]?[\d,.]+)/i);
    const betMatch = partyLine.match(/\bbet\s+([\d,.]+)/i);
    const collectedMatch = partyLine.match(/\bcollected\s+([\d,.]+)/i);
    const lostMatch = partyLine.match(/\blost\s+([\d,.]+)/i);
    const net = netMatch ? toNumber(netMatch[1]) : collectedMatch ? (toNumber(collectedMatch[1]) || 0) - (toNumber(betMatch?.[1]) || 0) : lostMatch ? -(toNumber(lostMatch[1]) || 0) : null;
    const invested = toNumber(betMatch?.[1]) ?? toNumber(lostMatch?.[1]);
    const collected = toNumber(collectedMatch?.[1]) || 0;
    return {
      netChips: Number.isFinite(net) ? net : null,
      investedChips: Number.isFinite(invested) ? invested : null,
      collectedChips: collected,
    };
  }
  const escaped = escapeRegExp(hero);
  const heroPrefix = new RegExp(`^${escaped}(?::\\s*|\\s+)`);
  const heroCollected = new RegExp(`^${escaped} collected \\(?[$€£]?\\s*([\\d,.]+)\\)?${COLLECTED_POT_SUFFIX_PATTERN}`, "i");
  let net = 0;
  let invested = 0;
  let collected = 0;
  let sawHeroMoneyLine = false;
  let streetContribution = 0;

  for (const line of lines) {
    if (/^\*\*\* (FLOP|TURN|RIVER) \*\*\*/.test(line)) {
      streetContribution = 0;
      continue;
    }

    const collectedMatch = line.match(heroCollected);
    if (collectedMatch) {
      const amount = toNumber(collectedMatch[1]) || 0;
      net += amount;
      collected += amount;
      sawHeroMoneyLine = true;
      continue;
    }

    const returnedMatch = parseReturnedBetLine(line);
    if (returnedMatch && returnedMatch.player === hero) {
      const returned = returnedMatch.amount || 0;
      net += returned;
      invested -= returned;
      streetContribution = Math.max(0, streetContribution - returned);
      sawHeroMoneyLine = true;
      continue;
    }

    if (!heroPrefix.test(line)) continue;

    const postMatch = line.match(/posts (?:the )?(ante|small blind|big blind|button blind|small & big blinds|dead blind) [$€£]?\s*([\d,.]+)/i);
    if (postMatch) {
      const posted = toNumber(postMatch[2]) || 0;
      net -= posted;
      invested += posted;
      if (!/ante/i.test(postMatch[1])) streetContribution += posted;
      sawHeroMoneyLine = true;
      continue;
    }

    const betMatch = line.match(/\bbets [$€£]?\s*([\d,.]+)/i);
    if (betMatch) {
      const amount = toNumber(betMatch[1]) || 0;
      net -= amount;
      invested += amount;
      streetContribution += amount;
      sawHeroMoneyLine = true;
      continue;
    }

    const callMatch = line.match(/\bcalls [$€£]?\s*([\d,.]+)/i);
    if (callMatch) {
      const amount = toNumber(callMatch[1]) || 0;
      net -= amount;
      invested += amount;
      streetContribution += amount;
      sawHeroMoneyLine = true;
      continue;
    }

    const raiseMatch = line.match(/\braises [$€£]?\s*[\d,.]+ to [$€£]?\s*([\d,.]+)/i);
    if (raiseMatch) {
      const totalStreetBet = toNumber(raiseMatch[1]) || 0;
      const paid = Math.max(0, totalStreetBet - streetContribution);
      net -= paid;
      invested += paid;
      streetContribution += paid;
      sawHeroMoneyLine = true;
    }
  }

  if (collected === 0) {
    const summaryWon = playerSummaryWonAmount(lines, hero);
    if (Number.isFinite(summaryWon) && summaryWon > 0) {
      net += summaryWon;
      collected += summaryWon;
      sawHeroMoneyLine = true;
    }
  }

  return sawHeroMoneyLine
    ? { netChips: net, investedChips: invested, collectedChips: collected }
    : { netChips: null, investedChips: null, collectedChips: null };
}

function calculatePlayerContributions(lines) {
  const contributions = new Map();
  const streetContributions = new Map();

  const addContribution = (player, amount, countOnStreet = true) => {
    if (!player || !Number.isFinite(amount) || amount === 0) return;
    contributions.set(player, (contributions.get(player) || 0) + amount);
    if (countOnStreet) {
      streetContributions.set(player, Math.max(0, (streetContributions.get(player) || 0) + amount));
    }
  };

  for (const line of lines) {
    if (/^\*\*\* (FLOP|TURN|RIVER) \*\*\*/.test(line) || /^\*\* Dealing (Flop|Turn|River) \*\*/i.test(line)) {
      streetContributions.clear();
      continue;
    }

    const returned = parseReturnedBetLine(line);
    if (returned) {
      addContribution(returned.player, -(returned.amount || 0));
      continue;
    }

    const post = line.match(/^(.+?)(?::\s*|\s+)posts (?:the )?(ante|small blind|big blind|button blind|small & big blinds|dead blind) [$€£]?\s*([\d,.]+)/i);
    if (post) {
      addContribution(post[1], toNumber(post[3]) || 0, !/ante/i.test(post[2]));
      continue;
    }

    const bet = line.match(/^(.+?)(?::\s*|\s+)bets [$€£]?\s*([\d,.]+)/i);
    if (bet) {
      addContribution(bet[1], toNumber(bet[2]) || 0);
      continue;
    }

    const call = line.match(/^(.+?)(?::\s*|\s+)calls [$€£]?\s*([\d,.]+)/i);
    if (call) {
      addContribution(call[1], toNumber(call[2]) || 0);
      continue;
    }

    const raise = line.match(/^(.+?)(?::\s*|\s+)raises [$€£]?\s*[\d,.]+ to [$€£]?\s*([\d,.]+)/i);
    if (raise) {
      const player = raise[1];
      const totalStreetBet = toNumber(raise[2]) || 0;
      const paid = Math.max(0, totalStreetBet - (streetContributions.get(player) || 0));
      addContribution(player, paid);
    }
  }

  return contributions;
}

function normalizeChampionUncalledReturn(sourceLines) {
  if (!isIpokerFamilyHand(sourceLines[0] || "")) return sourceLines;

  const lines = [...sourceLines];
  const contributions = calculatePlayerContributions(lines);
  const ranked = [...contributions.entries()]
    .map(([player, amount]) => [player, Math.max(0, Number(amount) || 0)])
    .filter(([, amount]) => amount > 0)
    .sort((a, b) => b[1] - a[1]);
  const totalPot = parseTotalPot(lines);
  const contributionTotal = ranked.reduce((sum, [, amount]) => sum + amount, 0);

  if (ranked.length > 1 && Number.isFinite(totalPot) && totalPot > 0) {
    const [topPlayer, topAmount] = ranked[0];
    const secondAmount = ranked[1][1];
    const excess = topAmount - secondAmount;
    const potAfterReturn = contributionTotal - excess;
    const alreadyReturned = lines.some((line) => parseReturnedBetLine(line));
    if (!alreadyReturned && excess > 0 && Math.abs(potAfterReturn - totalPot) < 0.01) {
      const collectionIndex = lines.findIndex((line) => new RegExp(`\\scollected\\s+[$€£]?\\s*[\\d,.]+${COLLECTED_POT_SUFFIX_PATTERN}$`, "i").test(line));
      const summaryIndex = lines.findIndex((line) => line === "*** SUMMARY ***");
      const insertIndex = collectionIndex >= 0 ? collectionIndex : summaryIndex >= 0 ? summaryIndex : lines.length;
      lines.splice(insertIndex, 0, `Uncalled bet (${excess}) returned to ${topPlayer}`);
    }
  }

  const normalizedContributions = calculatePlayerContributions(lines);
  const dealtMatch = lines.find((line) => /^Dealt to .+?\s+\[[^\]]+\]/.test(line))?.match(/^Dealt to (.+?)\s+\[/);
  const hero = dealtMatch?.[1] || "";
  const heroContribution = normalizedContributions.get(hero);
  if (!hero || !Number.isFinite(heroContribution)) return lines;

  const escapedHero = escapeRegExp(hero);
  const collected = lines.reduce((sum, line) => {
    const match = line.match(new RegExp(`^${escapedHero}\\s+collected\\s+\\(?[$€£]?\\s*([\\d,.]+)\\)?${COLLECTED_POT_SUFFIX_PATTERN}\\s*$`, "i"));
    return sum + (match ? toNumber(match[1]) || 0 : 0);
  }, 0);
  const startingChips = toNumber(lines
    .find((line) => new RegExp(`^Seat \\d+: ${escapedHero} \\(`, "i").test(line))
    ?.match(/\(([\d,.]+) in chips\)/i)?.[1]);
  const net = collected - heroContribution;
  const balance = Number.isFinite(startingChips) ? Math.max(0, startingChips + net) : 0;
  const summaryIndex = lines.findIndex((line) => new RegExp(`^${escapedHero} balance\\b`, "i").test(line));
  const existingSummary = summaryIndex >= 0 ? partyPlayerSummaryLine(lines, hero) : "";
  const existingNet = existingSummary.match(/\bnet\s+([+-]?[\d,.]+)/i);
  const existingCollected = existingSummary.match(/\bcollected\s+([\d,.]+)/i);
  if (summaryIndex >= 0 && existingNet && existingCollected && (collected <= 0 || (toNumber(existingCollected[1]) || 0) > 0)) {
    return lines;
  }
  if (summaryIndex >= 0) {
    lines[summaryIndex] = `${hero} balance ${balance}, bet ${heroContribution}, collected ${collected}, net ${net >= 0 ? "+" : ""}${net}`;
  }
  return lines;
}

function calculateAllInEvChips(lines, hero, heroCardsText, investedChips, handId) {
  const scenario = buildAllInEvScenario(lines, hero, heroCardsText, investedChips);
  if (!scenario) return null;

  let expectedReturn = 0;
  let heroEligible = false;
  for (let index = 0; index < scenario.pots.length; index += 1) {
    const pot = scenario.pots[index];
    if (!pot.contenders.includes(hero)) continue;
    heroEligible = true;
    const opponents = pot.contenders.filter((player) => player !== hero);
    if (!opponents.length) {
      expectedReturn += pot.amount;
      continue;
    }
    const villainHands = opponents.map((player) => scenario.shownHands.get(player));
    if (villainHands.some((cards) => !cards || cards.length !== 2)) return null;
    const equity = villainHands.length === 1
      ? calculateHoldemEquity(scenario.heroCards, villainHands[0], scenario.equityPoint.board, `${handId}:pot${index}`)
      : calculateHoldemEquityMultiway(scenario.heroCards, villainHands, scenario.equityPoint.board, `${handId}:pot${index}`);
    if (!Number.isFinite(equity)) return null;
    expectedReturn += equity * pot.amount;
  }
  return heroEligible ? expectedReturn - scenario.heroContribution : null;
}

function canCalculateAllInEquity(lines, hero, heroCardsText) {
  return Boolean(buildAllInEvScenario(lines, hero, heroCardsText, null));
}

function buildAllInEvScenario(lines, hero, heroCardsText, investedChips) {
  const heroCards = parseCardList(heroCardsText);
  if (!hero || heroCards.length !== 2) return null;
  const equityPoint = findAllInEquityPoint(lines, hero);
  if (!equityPoint) return null;

  const contributions = calculatePlayerContributions(lines);
  const heroContributionFromActions = contributions.get(hero);
  const heroContribution = Number.isFinite(heroContributionFromActions) ? heroContributionFromActions : investedChips;
  if (!Number.isFinite(heroContribution) || heroContribution <= 0) return null;

  const pointHeroContribution = equityPoint.contributions.get(hero);
  if (Number.isFinite(pointHeroContribution) && heroContribution - pointHeroContribution > 0.01) {
    return null;
  }

  const shownHands = parseShownHands(lines);
  shownHands.set(hero, heroCards);
  const activePlayers = new Set(equityPoint.activePlayers);
  const pots = buildContributionPots(contributions, activePlayers);
  if (!pots.some((pot) => pot.contenders.includes(hero))) return null;

  for (const pot of pots) {
    if (!pot.contenders.includes(hero)) continue;
    for (const player of pot.contenders) {
      const cards = shownHands.get(player);
      if (!cards || cards.length !== 2) return null;
    }
  }

  const contributionTotal = [...contributions.values()].reduce((sum, amount) => sum + Math.max(0, amount), 0);
  const distributablePot = parseDistributablePot(lines);
  const rake = parsePotRake(lines);
  const normalizedPots = normalizeContributionPotsForEv(pots, contributionTotal, distributablePot, rake);
  if (!normalizedPots) return null;

  return {
    heroCards,
    shownHands,
    equityPoint,
    heroContribution,
    pots: normalizedPots,
  };
}

function findAllInEquityPoint(lines, hero) {
  const seats = parseSeats(lines);
  const active = new Set(seats.map((seat) => seat.name));
  const allInPlayers = new Set();
  let board = [];
  let lastActionPlayer = "";
  let candidate = null;

  const capture = (lineIndex) => {
    if (candidate) return;
    candidate = {
      board: [...board],
      lineIndex,
      contributions: calculatePlayerContributions(lines.slice(0, lineIndex + 1)),
    };
  };
  const heroAlreadyMatchedAllIn = (player, lineIndex) => {
    if (!player || player === hero || !active.has(hero)) return false;
    const contributionsAtPoint = calculatePlayerContributions(lines.slice(0, lineIndex + 1));
    const heroPaid = contributionsAtPoint.get(hero) || 0;
    const playerPaid = contributionsAtPoint.get(player) || 0;
    return heroPaid > 0 && playerPaid > 0 && heroPaid + 0.01 >= playerPaid;
  };

  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index];
    if (line.startsWith("*** FLOP ***") || line.startsWith("*** TURN ***") || line.startsWith("*** RIVER ***") || line.startsWith("** Dealing Flop **") || line.startsWith("** Dealing Turn **") || line.startsWith("** Dealing River **")) {
      board = parseBoardFromLine(line);
      continue;
    }

    const action = parseActionLine(line);
    if (action) {
      lastActionPlayer = action.player;
      if (action.verb === "folds") {
        active.delete(action.player);
        continue;
      }
      if (action.allIn) {
        allInPlayers.add(action.player);
        if (action.player === hero) capture(index);
        else if (!candidate && heroAlreadyMatchedAllIn(action.player, index)) capture(index);
      }
      if (!candidate && action.player === hero && ["calls", "raises", "bets"].includes(action.verb) && allInPlayers.size > 0) {
        capture(index);
      }
      continue;
    }

    const marker = line.match(/^(.+?)\s+is\s+all-?In\.?$/i);
    if (marker) {
      const player = marker[1].trim() || lastActionPlayer;
      allInPlayers.add(player);
      if (player === hero) capture(index);
      else if (!candidate && heroAlreadyMatchedAllIn(player, index)) capture(index);
    }
  }

  return candidate ? { ...candidate, activePlayers: [...active] } : null;
}

function buildContributionPots(contributions, activePlayers) {
  const entries = [...contributions.entries()]
    .map(([player, amount]) => [player, Math.max(0, Number(amount) || 0)])
    .filter(([, amount]) => amount > 0);
  const levels = [...new Set(entries.map(([, amount]) => amount))].sort((a, b) => a - b);
  const pots = [];
  let previous = 0;
  for (const level of levels) {
    const contributors = entries.filter(([, amount]) => amount >= level).map(([player]) => player);
    const amount = (level - previous) * contributors.length;
    const contenders = contributors.filter((player) => activePlayers.has(player));
    if (amount > 0 && contenders.length) pots.push({ amount, contenders });
    previous = level;
  }
  return pots;
}

function normalizeContributionPotsForEv(pots, contributionTotal, distributablePot, rake = 0) {
  if (!pots.length) return null;
  const nextPots = pots.map((pot) => ({ ...pot, contenders: [...pot.contenders] }));
  if (!Number.isFinite(distributablePot) || distributablePot <= 0 || !Number.isFinite(contributionTotal) || contributionTotal <= 0) {
    return nextPots;
  }
  const diff = distributablePot - contributionTotal;
  if (Math.abs(diff) <= 0.01) return nextPots;
  if (distributablePot < contributionTotal) {
    const shortfall = contributionTotal - distributablePot;
    if (Number.isFinite(rake) && rake > 0 && Math.abs(shortfall - rake) <= Math.max(0.01, contributionTotal * 0.001)) {
      const scale = distributablePot / contributionTotal;
      return nextPots.map((pot) => ({ ...pot, amount: pot.amount * scale }));
    }
    let excess = contributionTotal - distributablePot;
    for (let index = nextPots.length - 1; index >= 0 && excess > 0.01; index -= 1) {
      const reduction = Math.min(nextPots[index].amount, excess);
      nextPots[index].amount -= reduction;
      excess -= reduction;
    }
    if (excess > 0.01) return null;
    return nextPots.filter((pot) => pot.amount > 0.01);
  }
  nextPots[0].amount += diff;
  return nextPots;
}

function parseShownHands(lines) {
  const shown = new Map();
  for (const line of lines) {
    const direct = line.match(/^(.+?)(?::\s*|\s+)shows \[([^\]]+)\]/);
    if (direct) shown.set(direct[1], parseCardList(direct[2]));
    const summary = line.match(/^Seat \d+: (.+?)(?: \([^)]*\))* showed \[([^\]]+)\]/);
    if (summary) shown.set(summary[1], parseCardList(summary[2]));
    const party = line.match(/^(.+?)\s+balance\b.*?\[\s*([2-9TJQKA][cdhs]\s*,?\s*[2-9TJQKA][cdhs])\s*\]/i);
    if (party) shown.set(party[1].trim(), parseCardList(party[2]));
  }
  return shown;
}

function parseBoardFromLine(line) {
  return [...String(line || "").matchAll(/\[([^\]]+)\]/g)]
    .flatMap((match) => parseCardList(match[1]));
}

function parseTotalPot(lines) {
  const totalLine = lines.find((line) => /^Total pot\b/i.test(line) || /^\s*Pot(?:e)? total\b/i.test(line));
  const total = toNumber(totalLine?.match(/(?:Total pot|Pot(?:e)? total)\s+\(?[$€£]?\s*([\d,.]+)\)?/i)?.[1]);
  if (Number.isFinite(total)) return total;
  const namedPots = lines
    .map((line) => toNumber(line.match(/^(?:Main|Side)\s+Pot:?\s*\(?[$€£]?\s*([\d,.]+)\)?/i)?.[1]))
    .filter(Number.isFinite)
    .reduce((sum, value) => sum + value, 0);
  if (namedPots > 0) return namedPots;
  return lines
    .map((line) => toNumber(line.match(new RegExp(` collected (?:\\(?)(?:[$€£]?\\s*)?([\\d,.]+)(?:\\)?)${COLLECTED_POT_SUFFIX_PATTERN}`, "i"))?.[1]))
    .filter(Number.isFinite)
    .reduce((sum, value) => sum + value, 0);
}

function parseDistributablePot(lines) {
  const summaryIndex = lines.findIndex((line) => line.startsWith("*** SUMMARY ***") || line.startsWith("** Summary **"));
  const actionLines = summaryIndex >= 0 ? lines.slice(0, summaryIndex) : lines;
  const collected = actionLines.reduce((sum, line) => {
    const match = line.match(new RegExp(`^.+?\\s+collected\\s+\\(?[$€£]?\\s*([\\d,.]+)\\)?${COLLECTED_POT_SUFFIX_PATTERN}`, "i"));
    return sum + (match ? toNumber(match[1]) || 0 : 0);
  }, 0);
  if (collected > 0) return collected;
  const total = parseTotalPot(lines);
  const rake = parsePotRake(lines);
  return Number.isFinite(total) ? Math.max(0, total - rake) : null;
}

function parsePotRake(lines) {
  const rakeLine = lines.find((line) => /\bRake\b/i.test(line)) || "";
  return toNumber(rakeLine.match(/\bRake\b\s*:?\s*[$€£]?\s*([\d,.]+)/i)?.[1]) || 0;
}

function parseCardList(text) {
  return String(text || "")
    .trim()
    .replace(/,/g, " ")
    .split(/\s+/)
    .filter((card) => /^[2-9TJQKA][cdhs]$/.test(card));
}

function calculateHoldemEquity(heroCards, villainCards, boardCards, seedText) {
  const used = new Set([...heroCards, ...villainCards, ...boardCards]);
  if (used.size !== heroCards.length + villainCards.length + boardCards.length) return null;
  const cacheKey = `hu|${canonicalEquityCacheKey(heroCards, [villainCards], boardCards)}`;
  if (equityCache.has(cacheKey)) return equityCache.get(cacheKey);
  const deck = fullDeck().filter((card) => !used.has(card));
  const missing = 5 - boardCards.length;
  if (missing < 0) return null;
  let wins = 0;
  let ties = 0;
  let total = 0;
  const scoreBoard = (extraBoard) => {
    const board = [...boardCards, ...extraBoard];
    const heroScore = evaluateBestHoldem([...heroCards, ...board]);
    const villainScore = evaluateBestHoldem([...villainCards, ...board]);
    if (heroScore > villainScore) wins += 1;
    else if (heroScore === villainScore) ties += 1;
    total += 1;
  };

  const run = equityRunPlan(deck.length, missing, 2);
  if (run.mode === "exact") {
    enumerateBoards(deck, missing, scoreBoard);
  } else {
    sampleBoards(deck, missing, cacheKey, run.samples, scoreBoard);
  }
  const equity = total ? (wins + ties / 2) / total : null;
  if (Number.isFinite(equity)) equityCache.set(cacheKey, equity);
  return equity;
}

function calculateHoldemEquityMultiway(heroCards, villainHands, boardCards, seedText) {
  const orderedVillainHands = villainHands
    .map((cards) => [...cards])
    .sort((a, b) => a.join("").localeCompare(b.join("")));
  const allCards = [...heroCards, ...orderedVillainHands.flat(), ...boardCards];
  const used = new Set(allCards);
  if (used.size !== allCards.length) return null;
  const cacheKey = `multi|${canonicalEquityCacheKey(heroCards, orderedVillainHands, boardCards)}`;
  if (equityCache.has(cacheKey)) return equityCache.get(cacheKey);
  const deck = fullDeck().filter((card) => !used.has(card));
  const missing = 5 - boardCards.length;
  if (missing < 0) return null;
  let totalShare = 0;
  let total = 0;
  const scoreBoard = (extraBoard) => {
    const board = [...boardCards, ...extraBoard];
    const scores = [
      evaluateBestHoldem([...heroCards, ...board]),
      ...orderedVillainHands.map((cards) => evaluateBestHoldem([...cards, ...board])),
    ];
    const best = Math.max(...scores);
    if (scores[0] === best) totalShare += 1 / scores.filter((score) => score === best).length;
    total += 1;
  };

  const run = equityRunPlan(deck.length, missing, 1 + orderedVillainHands.length);
  if (run.mode === "exact") {
    enumerateBoards(deck, missing, scoreBoard);
  } else {
    sampleBoards(deck, missing, cacheKey, run.samples, scoreBoard);
  }
  const equity = total ? totalShare / total : null;
  if (Number.isFinite(equity)) equityCache.set(cacheKey, equity);
  return equity;
}

function buildSuitPermutations(suits) {
  const permutations = [];
  const visit = (remaining, chosen) => {
    if (!remaining.length) {
      permutations.push(chosen);
      return;
    }
    for (let index = 0; index < remaining.length; index += 1) {
      visit([...remaining.slice(0, index), ...remaining.slice(index + 1)], [...chosen, remaining[index]]);
    }
  };
  visit([...suits], []);
  return permutations;
}

function canonicalEquityCacheKey(heroCards, villainHands, boardCards) {
  let best = "";
  const normalizeCards = (cards, suitMap) => [...cards]
    .map((card) => `${card[0]}${suitMap.get(card[1]) || card[1]}`)
    .sort((a, b) => "23456789TJQKA".indexOf(a[0]) - "23456789TJQKA".indexOf(b[0]) || a[1].localeCompare(b[1]))
    .join("");

  for (const permutation of EQUITY_SUIT_PERMUTATIONS) {
    const suitMap = new Map(EQUITY_SUITS.map((suit, index) => [suit, permutation[index]]));
    const hero = normalizeCards(heroCards, suitMap);
    const villains = villainHands.map((cards) => normalizeCards(cards, suitMap)).sort().join("|");
    const board = normalizeCards(boardCards, suitMap);
    const candidate = `${hero}|${villains}|${board}`;
    if (!best || candidate < best) best = candidate;
  }
  return best;
}

function fullDeck() {
  const cards = [];
  for (const rank of "23456789TJQKA") {
    for (const suit of "cdhs") cards.push(`${rank}${suit}`);
  }
  return cards;
}

function combinationCount(n, k) {
  if (k < 0 || k > n) return 0;
  const picks = Math.min(k, n - k);
  let result = 1;
  for (let index = 1; index <= picks; index += 1) {
    result = (result * (n - picks + index)) / index;
  }
  return Math.round(result);
}

function equityRunPlan(deckSize, missing, contenderCount) {
  const combos = combinationCount(deckSize, missing);
  if (!combos || combos <= EQUITY_EXACT_COMBO_LIMIT) return { mode: "exact", combos };
  const headsUp = contenderCount <= 2;
  const baseSamples = missing >= 5
    ? (headsUp ? EQUITY_HU_PREFLOP_SAMPLES : EQUITY_MULTI_PREFLOP_SAMPLES)
    : (headsUp ? EQUITY_HU_PARTIAL_SAMPLES : EQUITY_MULTI_PARTIAL_SAMPLES);
  return { mode: "sample", samples: Math.min(combos, baseSamples), combos };
}

function enumerateBoards(deck, missing, callback, start = 0, chosen = []) {
  if (chosen.length === missing) {
    callback(chosen);
    return;
  }
  for (let index = start; index <= deck.length - (missing - chosen.length); index += 1) {
    chosen.push(deck[index]);
    enumerateBoards(deck, missing, callback, index + 1, chosen);
    chosen.pop();
  }
}

function sampleBoards(deck, missing, seedText, samples, callback) {
  let seed = [...String(seedText || "hand")].reduce((value, char) => (value * 31 + char.charCodeAt(0)) >>> 0, 2166136261);
  const nextRand = () => {
    seed = (1664525 * seed + 1013904223) >>> 0;
    return seed / 4294967296;
  };
  const seen = new Set();
  let produced = 0;
  let attempts = 0;
  const maxAttempts = Math.max(samples * 8, samples + 100);
  while (produced < samples && attempts < maxAttempts) {
    attempts += 1;
    const chosen = new Set();
    const board = [];
    for (let cardIndex = 0; cardIndex < missing; cardIndex += 1) {
      let pick = Math.floor(nextRand() * deck.length);
      while (chosen.has(pick)) pick = Math.floor(nextRand() * deck.length);
      chosen.add(pick);
      board.push(deck[pick]);
    }
    const key = [...board].sort().join("");
    if (seen.has(key)) continue;
    seen.add(key);
    callback(board);
    produced += 1;
  }
  while (produced < samples) {
    const chosen = new Set();
    const board = [];
    for (let cardIndex = 0; cardIndex < missing; cardIndex += 1) {
      let pick = Math.floor(nextRand() * deck.length);
      while (chosen.has(pick)) pick = Math.floor(nextRand() * deck.length);
      chosen.add(pick);
      board.push(deck[pick]);
    }
    callback(board);
    produced += 1;
  }
}

function evaluateBestHoldem(cards) {
  if (!Array.isArray(cards) || cards.length < 5) return 0;
  const rankCounts = new Map();
  const suitRanks = new Map();
  for (const card of cards) {
    const rank = "23456789TJQKA".indexOf(card?.[0]) + 2;
    const suit = card?.[1];
    if (rank < 2 || !"cdhs".includes(suit)) return 0;
    rankCounts.set(rank, (rankCounts.get(rank) || 0) + 1);
    if (!suitRanks.has(suit)) suitRanks.set(suit, []);
    suitRanks.get(suit).push(rank);
  }
  const ranks = [...rankCounts.keys()].sort((a, b) => b - a);
  const groups = [...rankCounts.entries()].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
  const straightHigh = bestStraightHigh(ranks);
  const flushRanks = [...suitRanks.values()].find((values) => values.length >= 5)?.sort((a, b) => b - a) || [];
  const straightFlushHigh = flushRanks.length ? bestStraightHigh([...new Set(flushRanks)]) : 0;
  if (straightFlushHigh) return handScore(8, [straightFlushHigh]);

  const quads = groups.filter(([, count]) => count === 4).map(([rank]) => rank);
  if (quads.length) return handScore(7, [quads[0], ...ranks.filter((rank) => rank !== quads[0]).slice(0, 1)]);

  const trips = groups.filter(([, count]) => count >= 3).map(([rank]) => rank).sort((a, b) => b - a);
  const pairRanks = groups.filter(([, count]) => count >= 2).map(([rank]) => rank).sort((a, b) => b - a);
  if (trips.length) {
    const pair = pairRanks.find((rank) => rank !== trips[0]);
    if (pair) return handScore(6, [trips[0], pair]);
  }
  if (flushRanks.length >= 5) return handScore(5, flushRanks.slice(0, 5));
  if (straightHigh) return handScore(4, [straightHigh]);
  if (trips.length) return handScore(3, [trips[0], ...ranks.filter((rank) => rank !== trips[0]).slice(0, 2)]);
  if (pairRanks.length >= 2) {
    const highPair = pairRanks[0];
    const lowPair = pairRanks[1];
    const kicker = ranks.find((rank) => rank !== highPair && rank !== lowPair);
    return handScore(2, [highPair, lowPair, kicker || 0]);
  }
  if (pairRanks.length === 1) {
    const pair = pairRanks[0];
    return handScore(1, [pair, ...ranks.filter((rank) => rank !== pair).slice(0, 3)]);
  }
  return handScore(0, ranks.slice(0, 5));
}

function bestStraightHigh(ranks) {
  const values = new Set(ranks);
  if (values.has(14)) values.add(1);
  for (let high = 14; high >= 5; high -= 1) {
    let complete = true;
    for (let rank = high; rank > high - 5; rank -= 1) {
      if (!values.has(rank)) {
        complete = false;
        break;
      }
    }
    if (complete) return high;
  }
  return 0;
}

function evaluateFiveCards(cards) {
  const ranks = cards.map((card) => "23456789TJQKA".indexOf(card[0]) + 2).sort((a, b) => b - a);
  const suits = cards.map((card) => card[1]);
  const counts = new Map();
  for (const rank of ranks) counts.set(rank, (counts.get(rank) || 0) + 1);
  const groups = [...counts.entries()].sort((a, b) => b[1] - a[1] || b[0] - a[0]);
  const uniqueRanks = [...new Set(ranks)];
  const wheelRanks = uniqueRanks.includes(14) ? [...uniqueRanks, 1] : uniqueRanks;
  let straightHigh = 0;
  for (let index = 0; index <= wheelRanks.length - 5; index += 1) {
    const slice = wheelRanks.slice(index, index + 5);
    if (slice[0] - slice[4] === 4) {
      straightHigh = slice[0];
      break;
    }
  }
  const flush = suits.every((suit) => suit === suits[0]);
  if (straightHigh && flush) return handScore(8, [straightHigh]);
  if (groups[0][1] === 4) return handScore(7, [groups[0][0], groups[1][0]]);
  if (groups[0][1] === 3 && groups[1][1] === 2) return handScore(6, [groups[0][0], groups[1][0]]);
  if (flush) return handScore(5, ranks);
  if (straightHigh) return handScore(4, [straightHigh]);
  if (groups[0][1] === 3) return handScore(3, [groups[0][0], ...groups.slice(1).map(([rank]) => rank)]);
  if (groups[0][1] === 2 && groups[1][1] === 2) return handScore(2, [groups[0][0], groups[1][0], groups[2][0]]);
  if (groups[0][1] === 2) return handScore(1, [groups[0][0], ...groups.slice(1).map(([rank]) => rank)]);
  return handScore(0, ranks);
}

function handScore(category, ranks) {
  return [category, ...ranks, 0, 0, 0, 0, 0].slice(0, 6).reduce((score, value) => score * 15 + value, 0);
}

function escapeRegExp(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function parseHand(chunk, options = {}) {
  const sourceLines = chunk.split("\n").map((line) => line.trim()).filter(Boolean);
  const lines = normalizeChampionUncalledReturn(sourceLines);
  const header = parseHeader(lines[0] || "", lines);
  if (!header) return null;

  const tableLine = lines.find((line) => line.startsWith("Table "));
  const buttonMatch = tableLine?.match(/Seat\s+#?(\d+)\s+is the button/i);
  const buttonSeat = buttonMatch ? Number(buttonMatch[1]) : null;
  const seats = parseSeats(lines);
  const positions = assignPositions(seats, buttonSeat);
  const dealtLine = lines.find((line) => /^Dealt to .+?\s+\[[^\]]+\]/.test(line));
  const dealtMatch = dealtLine?.match(/^Dealt to (.+?)\s+\[([^\]]+)\]/);
  const hero = dealtMatch?.[1] || "";
  const heroCards = dealtMatch?.[2] === "?? ??" ? "" : (dealtMatch?.[2] || "");
  const heroSeat = seats.find((seat) => seat.name === hero);
  const preflop = parseActionsFromLines(parsePreflop(lines));
  const flopActions = parseStreetActions(lines, "FLOP");
  const turnActions = parseStreetActions(lines, "TURN");
  const riverActions = parseStreetActions(lines, "RIVER");
  const heroActions = preflop.filter((action) => action.player === hero);
  const vpip = heroActions.some((action) => ["calls", "bets", "raises"].includes(action.verb));
  const pfr = heroActions.some((action) => action.verb === "raises");
  const allInPre = heroActions.some((action) => action.allIn);
  const firstRaise = preflop.find((action) => action.verb === "raises");
  const raises = preflop.filter((action) => action.verb === "raises");
  const secondRaise = raises[1];
  const thirdRaise = raises[2];
  const firstRaiseIndex = firstRaise ? preflop.indexOf(firstRaise) : -1;
  const secondRaiseIndex = secondRaise ? preflop.indexOf(secondRaise) : -1;
  const openRaise = firstRaise?.player === hero;
  const firstRaisePosition = firstRaise ? positions.get(firstRaise.player) : "";
  const firstHeroActionIndex = preflop.findIndex((action) => action.player === hero && ["folds", "calls", "raises"].includes(action.verb));
  const firstHeroAction = firstHeroActionIndex >= 0 ? preflop[firstHeroActionIndex] : null;
  const heroAfterFirstRaise = firstRaiseIndex >= 0
    ? preflop.find((action, index) => index > firstRaiseIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb))
    : null;
  const heroAfterSecondRaise = secondRaiseIndex >= 0
    ? preflop.find((action, index) => index > secondRaiseIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb))
    : null;
  const rfiOpportunity = Boolean(firstHeroAction) && (firstRaiseIndex < 0 || firstHeroActionIndex <= firstRaiseIndex);
  const ggleaksRfi = ggleaksRfiSpot(hero, seats, positions, preflop, header.bigBlind);
  const threeBetOpportunity = Boolean(firstRaise && firstRaise.player !== hero && heroAfterFirstRaise && (!secondRaise || preflop.indexOf(heroAfterFirstRaise) <= secondRaiseIndex));
  const threeBet = threeBetOpportunity && heroAfterFirstRaise?.verb === "raises";
  const legacyFacedThreeBet = Boolean(firstRaise?.player === hero && secondRaise && secondRaise.player !== hero && heroAfterSecondRaise);
  const foldThreeBetSpot = ggleaksFoldToThreeBetSpot(hero, seats, positions, preflop, header.bigBlind);
  const facedThreeBet = Boolean(foldThreeBetSpot);
  const heroFoldAfterThreeBet = foldThreeBetSpot?.heroAction?.verb === "folds";
  const fourBetOpportunity = legacyFacedThreeBet;
  const fourBet = fourBetOpportunity && heroAfterSecondRaise?.verb === "raises";
  const fourBetNonAllIn = fourBet && !heroAfterSecondRaise?.allIn;
  const callVsAllIn = heroActions.some((action) => action.verb === "calls" && action.allIn);
  const firstHeroPreflopAction = heroActions[0]?.verb || "";
  const coldCallOpportunity = Boolean(threeBetOpportunity);
  const coldCall = coldCallOpportunity && heroAfterFirstRaise?.verb === "calls" && !heroAfterFirstRaise?.allIn;
  const coldCallBbSpot = ggleaksColdCallBbSpot(hero, seats, positions, preflop, header.bigBlind);
  const coldCallBbOpportunity = Boolean(coldCallBbSpot);
  const coldCallBb = coldCallBbSpot?.heroAction?.verb === "calls" && !coldCallBbSpot.heroAction.allIn;
  const callsBeforeHero = firstRaiseIndex >= 0 && firstHeroActionIndex > firstRaiseIndex
    ? preflop.filter((action, index) => index > firstRaiseIndex && index < firstHeroActionIndex && action.verb === "calls" && action.player !== hero)
    : [];
  const raisesBeforeHeroAfterOpen = firstRaiseIndex >= 0 && firstHeroActionIndex > firstRaiseIndex
    ? preflop.filter((action, index) => index > firstRaiseIndex && index < firstHeroActionIndex && action.verb === "raises")
    : [];
  const squeezeOpportunity = Boolean(firstRaise && firstRaise.player !== hero && firstHeroAction && callsBeforeHero.length > 0 && raisesBeforeHeroAfterOpen.length === 0);
  const squeeze = squeezeOpportunity && firstHeroAction?.verb === "raises";
  const bbSteal = bbStealSpot(hero, seats, positions, preflop, header.bigBlind);
  const bbFacedSteal = Boolean(bbSteal);
  const bbFoldVsSteal = bbSteal?.heroAction?.verb === "folds";
  const bbFoldVsCO = bbFoldVsSteal && bbSteal?.villainPosition === "CO";
  const bbFoldVsBTN = bbFoldVsSteal && bbSteal?.villainPosition === "BTN";
  const bbFoldVsSB = bbFoldVsSteal && bbSteal?.villainPosition === "SB";
  const effectiveStack = Math.min(...seats.filter((seat) => seat.name !== hero).map((seat) => seat.chips), heroSeat?.chips || 0);
  const stackBb = header.bigBlind ? Math.round(((heroSeat?.chips || 0) / header.bigBlind) * 10) / 10 : null;
  const effectiveBb = header.bigBlind ? Math.round((effectiveStack / header.bigBlind) * 10) / 10 : null;
  const result = parseResult(lines, hero);
  const heroEndingChips = parseHeroEndingChips(lines, hero);
  const heroMoney = calculateHeroMoney(lines, hero);
  const allInEvChips = options.skipEquity ? null : calculateAllInEvChips(lines, hero, heroCards, heroMoney.investedChips, header.handId);
  const evChips = Number.isFinite(allInEvChips) ? allInEvChips : heroMoney.netChips;
  const equityPending = options.skipEquity === true && canCalculateAllInEquity(lines, hero, heroCards);
  const showedDown = heroReachedShowdown(lines, hero);
  const wonShowdown = showedDown && heroWonShowdown(lines, hero);
  const sawFlop = streetWasDealt(lines, "FLOP") && !playerFoldedOnStreet(hero, preflop);
  const sawTurn = streetWasDealt(lines, "TURN") && sawFlop && !playerFoldedOnStreet(hero, flopActions);
  const sawRiver = streetWasDealt(lines, "RIVER") && sawTurn && !playerFoldedOnStreet(hero, turnActions);
  const cbetFlopOpportunity = hasCbetOpportunity(hero, preflop, flopActions);
  const cbetFlop = madeCbet(hero, preflop, flopActions);
  const cbetTurnSpot = ggleaksCbetTurnSpot(hero, seats, positions, preflop, flopActions, turnActions, lines, header.bigBlind);
  const cbetTurnOpportunity = Boolean(cbetTurnSpot);
  const cbetTurn = Boolean(cbetTurnSpot?.bet);
  const foldToCbetTurnSpot = ggleaksFoldToCbetTurnSpot(hero, seats, preflop, flopActions, turnActions, lines, header.bigBlind);
  const cbetRiverSpot = ggleaksCbetRiverSpot(hero, seats, preflop, flopActions, turnActions, riverActions, lines, header.bigBlind);
  const cbetFlopIpSpot = ggleaksCbetFlopIpSpot(hero, seats, positions, preflop, flopActions, lines, header.bigBlind);
  const cbetFlopOopSpot = ggleaksCbetFlopOopSpot(hero, seats, positions, preflop, flopActions, lines, header.bigBlind);
  const foldToCbetFlopSpot = ggleaksFoldToCbetFlopSpot(hero, seats, preflop, flopActions, lines, header.bigBlind);
  const donkStats = ggleaksDonkStats(
    hero,
    seats,
    positions,
    preflop,
    flopActions,
    turnActions,
    riverActions,
    preflopPotBb(lines, preflop, header.bigBlind),
    header.bigBlind
  );
  const srpSpot = buildSrpSpot(hero, seats, positions, preflop, flopActions, turnActions, riverActions, lines);

  return {
    parserVersion: PARSER_VERSION,
    equityVersion: EQUITY_VERSION,
    ...header,
    players: seats.length,
    hero,
    heroCards,
    heroChips: heroSeat?.chips || null,
    heroEndingChips,
    netChips: heroMoney.netChips,
    investedChips: heroMoney.investedChips,
    collectedChips: heroMoney.collectedChips,
    allInEquityChips: allInEvChips,
    equityPending,
    evChips,
    position: positions.get(hero) || "-",
    stackBb,
    effectiveBb,
    vpip,
    pfr,
    allInPre,
    openRaise,
    rfiOpportunity,
    ggleaksRfiOpportunity: Boolean(ggleaksRfi),
    ggleaksRfi: Boolean(ggleaksRfi?.raise),
    firstRaisePosition,
    threeBetOpportunity,
    threeBet,
    facedThreeBet,
    foldToThreeBet: heroFoldAfterThreeBet,
    fourBetOpportunity,
    fourBet,
    fourBetNonAllIn,
    squeezeOpportunity,
    squeeze,
    coldCallOpportunity,
    coldCall,
    coldCallBbOpportunity,
    coldCallBb,
    bbFacedSteal,
    bbFoldVsSteal,
    bbFoldVsCO,
    bbFoldVsBTN,
    bbFoldVsSB,
    callVsAllIn,
    cbetFlopOpportunity,
    cbetFlop,
    cbetFlopIPOpportunity: Boolean(cbetFlopIpSpot),
    cbetFlopIP: Boolean(cbetFlopIpSpot?.bet),
    cbetFlopOOPOpportunity: Boolean(cbetFlopOopSpot),
    cbetFlopOOP: Boolean(cbetFlopOopSpot?.bet),
    cbetTurnOpportunity,
    cbetTurn,
    cbetRiverOpportunity: Boolean(cbetRiverSpot),
    cbetRiver: Boolean(cbetRiverSpot?.bet),
    riverBetOpportunity: hasGgleaksRiverBetOpportunity(hero, seats, preflop, riverActions, lines, header.bigBlind),
    riverBet: madeGgleaksRiverBet(hero, seats, preflop, riverActions, lines, header.bigBlind),
    facedFlopCbet: Boolean(foldToCbetFlopSpot),
    facedTurnCbet: Boolean(foldToCbetTurnSpot),
    facedRiverCbet: facedBarrel(hero, turnActions, riverActions),
    foldToFlopCbet: foldToCbetFlopSpot?.heroAction?.verb === "folds",
    foldToTurnCbet: foldToCbetTurnSpot?.heroAction?.verb === "folds",
    foldToRiverCbet: foldedFacingBarrel(hero, turnActions, riverActions),
    donkBetFlopOpportunity: donkStats.flopOpportunity,
    donkBetFlop: donkStats.flop,
    donkBetTurnOpportunity: donkStats.turnOpportunity,
    donkBetTurn: donkStats.turn,
    donkBetRiverOpportunity: donkStats.riverOpportunity,
    donkBetRiver: donkStats.river,
    betVsMissedFlopOpportunity: hasBetVsMissedOpportunity(hero, preflop, flopActions),
    betVsMissedFlop: madeBetVsMissed(hero, preflop, flopActions),
    betVsMissedTurnOpportunity: hasBetVsMissedTurnOpportunity(hero, preflop, flopActions, turnActions),
    betVsMissedTurn: madeBetVsMissedTurn(hero, preflop, flopActions, turnActions),
    betVsMissedRiverOpportunity: hasBetVsMissedRiverOpportunity(hero, preflop, flopActions, turnActions, riverActions),
    betVsMissedRiver: madeBetVsMissedRiver(hero, preflop, flopActions, turnActions, riverActions),
    xrOopFlopOpportunity: hasBlindCheckRaiseOpportunity(hero, seats, positions, preflop, flopActions),
    xrOopFlop: madeBlindCheckRaise(hero, seats, positions, preflop, flopActions),
    xrOopTurnOpportunity: hasBlindCheckRaiseOpportunity(hero, seats, positions, preflop, turnActions),
    xrOopTurn: madeBlindCheckRaise(hero, seats, positions, preflop, turnActions),
    xrOopRiverOpportunity: hasBlindCheckRaiseOpportunity(hero, seats, positions, preflop, riverActions),
    xrOopRiver: madeBlindCheckRaise(hero, seats, positions, preflop, riverActions),
    xrPfaFlopOpportunity: hasGgleaksPfaOopXrOpportunity(hero, seats, positions, preflop, flopActions, lines, header.bigBlind),
    xrPfaFlop: madeGgleaksPfaOopXr(hero, seats, positions, preflop, flopActions, lines, header.bigBlind),
    xrPfaTurnOpportunity: hasPfaBarrelCheckRaiseOpportunity(hero, flopActions, turnActions),
    xrPfaTurn: madePfaBarrelCheckRaise(hero, flopActions, turnActions),
    xrPfaRiverOpportunity: hasPfaBarrelCheckRaiseOpportunity(hero, turnActions, riverActions),
    xrPfaRiver: madePfaBarrelCheckRaise(hero, turnActions, riverActions),
    raiseIpFlopOpportunity: hasRaiseIpOpportunity(hero, flopActions),
    raiseIpFlop: madeRaiseIp(hero, flopActions),
    raiseIpTurnOpportunity: hasRaiseIpOpportunity(hero, turnActions),
    raiseIpTurn: madeRaiseIp(hero, turnActions),
    raiseIpRiverOpportunity: hasRaiseIpOpportunity(hero, riverActions),
    raiseIpRiver: madeRaiseIp(hero, riverActions),
    foldVsBetFlopOpportunity: hasFoldVsBetOpportunity(hero, flopActions),
    foldVsBetFlop: madeFoldVsBet(hero, flopActions),
    foldVsBetTurnOpportunity: hasFoldVsBetOpportunity(hero, turnActions),
    foldVsBetTurn: madeFoldVsBet(hero, turnActions),
    foldVsBetRiverOpportunity: hasFoldVsBetOpportunity(hero, riverActions),
    foldVsBetRiver: madeFoldVsBet(hero, riverActions),
    skipCbetFlopOpportunity: hasSkipCbetOpportunity(hero, preflop, flopActions),
    skipCbetFoldFlopOpportunity: hasSkipCbetOpportunity(hero, preflop, flopActions),
    skipCbetFoldFlop: skippedCbetAnd(hero, preflop, flopActions, "folds"),
    skipCbetRaiseFlop: skippedCbetAnd(hero, preflop, flopActions, "raises"),
    sawFlop,
    sawTurn,
    sawRiver,
    showedDown,
    wonShowdown,
    srpSpot,
    result,
    preflopSummary: summarizeHero(heroActions),
    raw: lines === sourceLines ? chunk : lines.join("\n"),
  };
}

function firstAggressor(actions) {
  return actions.find((action) => action.verb === "bets" || action.verb === "raises");
}

function buildSrpSpot(hero, seats, positions, preflopActions, flopActions, turnActions, riverActions, lines) {
  if (!hero || !streetWasDealt(lines, "FLOP")) return null;
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (raises.length !== 1) return null;
  const aggressor = raises[0].player;
  if (!aggressor) return null;
  const folded = new Set(preflopActions.filter((action) => action.verb === "folds").map((action) => action.player));
  const activePlayers = seats.map((seat) => seat.name).filter((name) => !folded.has(name));
  if (activePlayers.length !== 2 || !activePlayers.includes(aggressor) || !activePlayers.includes(hero)) return null;
  const defender = activePlayers.find((name) => name !== aggressor);
  const flopFirst = flopActions.find((action) => activePlayers.includes(action.player) && ["checks", "bets", "folds", "calls", "raises"].includes(action.verb));
  if (!defender || !flopFirst) return null;
  const oop = flopFirst.player;
  const ip = activePlayers.find((name) => name !== oop);
  if (!ip) return null;
  const role = hero === defender
    ? (hero === oop ? "defender-oop" : "defender-ip")
    : (hero === oop ? "aggressor-oop" : "aggressor-ip");
  const scenario = role === "defender-oop" || role === "aggressor-ip" ? "defender-oop" : "aggressor-oop";
  const villain = hero === aggressor ? defender : aggressor;
  return {
    scenario,
    role,
    heroPosition: positions.get(hero) || "-",
    villainPosition: positions.get(villain) || "-",
    streets: {
      flop: describeSrpStreet(hero, villain, flopActions),
      turn: streetWasDealt(lines, "TURN") ? describeSrpStreet(hero, villain, turnActions) : null,
      river: streetWasDealt(lines, "RIVER") ? describeSrpStreet(hero, villain, riverActions) : null,
    },
  };
}

function buildSrpSpotFromRaw(raw) {
  if (!raw) return null;
  const lines = String(raw).replace(/\r/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
  const tableLine = lines.find((line) => line.startsWith("Table "));
  const buttonMatch = tableLine?.match(/Seat\s+#?(\d+)\s+is the button/i);
  const seats = parseSeats(lines);
  const positions = assignPositions(seats, buttonMatch ? Number(buttonMatch[1]) : null);
  const dealtLine = lines.find((line) => /^Dealt to .+?\s+\[[^\]]+\]/.test(line));
  const hero = dealtLine?.match(/^Dealt to (.+?)\s+\[/)?.[1] || "";
  if (!hero) return null;
  return buildSrpSpot(
    hero,
    seats,
    positions,
    parseActionsFromLines(parsePreflop(lines)),
    parseStreetActions(lines, "FLOP"),
    parseStreetActions(lines, "TURN"),
    parseStreetActions(lines, "RIVER"),
    lines
  );
}

function buildThreeBetPotSpotFromRaw(raw) {
  if (!raw) return null;
  const lines = String(raw).replace(/\r/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
  const tableLine = lines.find((line) => line.startsWith("Table "));
  const buttonMatch = tableLine?.match(/Seat\s+#?(\d+)\s+is the button/i);
  const seats = parseSeats(lines);
  const positions = assignPositions(seats, buttonMatch ? Number(buttonMatch[1]) : null);
  const dealtLine = lines.find((line) => /^Dealt to .+?\s+\[[^\]]+\]/.test(line));
  const hero = dealtLine?.match(/^Dealt to (.+?)\s+\[/)?.[1] || "";
  if (!hero || !streetWasDealt(lines, "FLOP")) return null;

  const preflopActions = parseActionsFromLines(parsePreflop(lines));
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (raises.length !== 2) return null;
  const aggressor = raises[1].player;
  if (!aggressor || raises[0].player === aggressor) return null;

  const folded = new Set(preflopActions.filter((action) => action.verb === "folds").map((action) => action.player));
  const activePlayers = seats.map((seat) => seat.name).filter((name) => !folded.has(name));
  if (activePlayers.length !== 2 || !activePlayers.includes(hero) || !activePlayers.includes(aggressor)) return null;
  const defender = activePlayers.find((name) => name !== aggressor);
  const flopActions = parseStreetActions(lines, "FLOP");
  const flopFirst = flopActions.find((action) => activePlayers.includes(action.player) && ["checks", "bets", "folds", "calls", "raises"].includes(action.verb));
  if (!defender || !flopFirst) return null;
  const oop = flopFirst.player;
  const ip = activePlayers.find((name) => name !== oop);
  if (!ip) return null;

  const role = hero === defender
    ? (hero === oop ? "defender-oop" : "defender-ip")
    : (hero === oop ? "aggressor-oop" : "aggressor-ip");
  const scenario = role === "defender-oop" || role === "aggressor-ip" ? "defender-oop" : "aggressor-oop";
  const villain = hero === aggressor ? defender : aggressor;
  return {
    scenario,
    role,
    heroPosition: positions.get(hero) || "-",
    villainPosition: positions.get(villain) || "-",
    streets: {
      flop: describeSrpStreet(hero, villain, flopActions),
      turn: streetWasDealt(lines, "TURN") ? describeSrpStreet(hero, villain, parseStreetActions(lines, "TURN")) : null,
      river: streetWasDealt(lines, "RIVER") ? describeSrpStreet(hero, villain, parseStreetActions(lines, "RIVER")) : null,
    },
  };
}

function buildBlindWarSpotFromRaw(raw) {
  if (!raw) return null;
  const lines = String(raw).replace(/\r/g, "").split("\n").map((line) => line.trim()).filter(Boolean);
  if (!streetWasDealt(lines, "FLOP")) return null;
  const tableLine = lines.find((line) => line.startsWith("Table "));
  const buttonMatch = tableLine?.match(/Seat\s+#?(\d+)\s+is the button/i);
  const seats = parseSeats(lines);
  const positions = assignPositions(seats, buttonMatch ? Number(buttonMatch[1]) : null);
  const dealtLine = lines.find((line) => /^Dealt to .+?\s+\[[^\]]+\]/.test(line));
  const hero = dealtLine?.match(/^Dealt to (.+?)\s+\[/)?.[1] || "";
  const heroPosition = positions.get(hero) || "";
  if (!hero || !["SB", "BB"].includes(heroPosition)) return null;

  const sb = seats.find((seat) => positions.get(seat.name) === "SB")?.name || "";
  const bb = seats.find((seat) => positions.get(seat.name) === "BB")?.name || "";
  if (!sb || !bb) return null;
  const preflop = parseActionsFromLines(parsePreflop(lines));
  const folded = new Set(preflop.filter((action) => action.verb === "folds").map((action) => action.player));
  const activePlayers = seats.map((seat) => seat.name).filter((name) => !folded.has(name));
  if (activePlayers.length !== 2 || !activePlayers.includes(sb) || !activePlayers.includes(bb)) return null;

  const raises = preflop.map((action, index) => ({ action, index })).filter((entry) => entry.action.verb === "raises");
  const sbCalls = preflop.map((action, index) => ({ action, index })).filter((entry) => entry.action.player === sb && entry.action.verb === "calls");
  const bbCalls = preflop.map((action, index) => ({ action, index })).filter((entry) => entry.action.player === bb && entry.action.verb === "calls");
  let context = "";
  if (raises.length === 1 && raises[0].action.player === sb && bbCalls.some((entry) => entry.index > raises[0].index)) {
    context = "sb-open";
  } else if (raises.length === 1 && raises[0].action.player === bb
    && sbCalls.some((entry) => entry.index < raises[0].index)
    && sbCalls.some((entry) => entry.index > raises[0].index)) {
    context = "bb-iso";
  } else if (!raises.length && sbCalls.length) {
    context = "limped";
  }
  if (!context) return null;

  const villain = hero === sb ? bb : sb;
  return {
    side: heroPosition.toLowerCase(),
    context,
    heroPosition,
    villainPosition: positions.get(villain) || "-",
    streets: {
      flop: describeSrpStreet(hero, villain, parseStreetActions(lines, "FLOP")),
      turn: streetWasDealt(lines, "TURN") ? describeSrpStreet(hero, villain, parseStreetActions(lines, "TURN")) : null,
      river: streetWasDealt(lines, "RIVER") ? describeSrpStreet(hero, villain, parseStreetActions(lines, "RIVER")) : null,
    },
  };
}

function describeSrpStreet(player, villain, actions) {
  const relevant = actions.filter((action) => action.player === player || action.player === villain);
  const playerIndex = relevant.findIndex((action) => action.player === player && ["checks", "bets", "folds", "calls", "raises"].includes(action.verb));
  if (playerIndex < 0) return null;
  const first = relevant[playerIndex];
  const previousBet = [...relevant.slice(0, playerIndex)].reverse().find((action) => action.player === villain && ["bets", "raises"].includes(action.verb));
  const line = {
    code: "",
    root: "",
    checked: first.verb === "checks",
    bet: first.verb === "bets",
    checkedTo: !previousBet && first.verb === "checks",
    facedBet: Boolean(previousBet),
    responseToBet: "",
    facedRaise: false,
    responseToRaise: "",
    facedReraise: false,
    responseToReraise: "",
  };

  if (first.verb === "checks") {
    line.root = "X";
    const villainBetIndex = relevant.findIndex((action, index) => index > playerIndex && action.player === villain && action.verb === "bets");
    if (villainBetIndex < 0) {
      line.code = "X";
      return line;
    }
    line.facedBet = true;
    const response = relevant.find((action, index) => index > villainBetIndex && action.player === player && ["folds", "calls", "raises"].includes(action.verb));
    line.responseToBet = response?.verb || "";
    const suffix = response?.verb === "folds" ? "F" : response?.verb === "calls" ? "C" : response?.verb === "raises" ? "R" : "";
    line.code = `X${suffix}`;
    if (response?.verb === "raises") addSrpReraiseResponse(line, relevant, response, player, villain, "X");
    return line;
  }

  if (first.verb === "bets") {
    line.root = "B";
    line.code = "B";
    const villainRaise = relevant.find((action, index) => index > playerIndex && action.player === villain && action.verb === "raises");
    if (!villainRaise) return line;
    line.facedRaise = true;
    const raiseIndex = relevant.indexOf(villainRaise);
    const response = relevant.find((action, index) => index > raiseIndex && action.player === player && ["folds", "calls", "raises"].includes(action.verb));
    line.responseToRaise = response?.verb || "";
    line.code = response?.verb === "folds" ? "BF" : response?.verb === "calls" ? "BC" : response?.verb === "raises" ? "BR" : "B";
    return line;
  }

  if (previousBet && ["folds", "calls", "raises"].includes(first.verb)) {
    line.root = first.verb === "folds" ? "F" : first.verb === "calls" ? "C" : "R";
    line.code = line.root;
    line.responseToBet = first.verb;
    if (first.verb === "raises") addSrpReraiseResponse(line, relevant, first, player, villain, "");
    return line;
  }
  return null;
}

function addSrpReraiseResponse(line, actions, raiseAction, player, villain, prefix) {
  const raiseIndex = actions.indexOf(raiseAction);
  const villainReraise = actions.find((action, index) => index > raiseIndex && action.player === villain && action.verb === "raises");
  if (!villainReraise) return;
  line.facedReraise = true;
  const reraiseIndex = actions.indexOf(villainReraise);
  const response = actions.find((action, index) => index > reraiseIndex && action.player === player && ["folds", "calls", "raises"].includes(action.verb));
  line.responseToReraise = response?.verb || "";
  const suffix = response?.verb === "folds" ? "F" : response?.verb === "calls" ? "C" : response?.verb === "raises" ? "R" : "";
  if (suffix) line.code = `${prefix}R${suffix}`;
}

function lastPreflopAggressor(preflopActions) {
  return [...preflopActions].reverse().find((action) => action.verb === "raises");
}

function preflopRaises(preflopActions) {
  return preflopActions.filter((action) => action.verb === "raises");
}

function isSingleRaisedPot(preflopActions) {
  const raises = preflopRaises(preflopActions);
  return raises.length === 1 && !raises[0].allIn;
}

function firstNonFoldAction(actions) {
  return actions.find((action) => ["calls", "bets", "raises"].includes(action.verb)) || null;
}

function lastPlayerAction(actions, player) {
  return [...actions].reverse().find((action) => action.player === player && ["checks", "folds", "calls", "bets", "raises"].includes(action.verb)) || null;
}

function bbStealSpot(hero, seats, positions, preflopActions, bigBlind) {
  if (!hero || positions.get(hero) !== "BB") return null;
  if (seats.length <= 5 || seats.length >= 10) return null;
  const open = firstNonFoldAction(preflopActions);
  if (!open || open.verb !== "raises" || open.player === hero || open.allIn) return null;
  const villainPosition = positions.get(open.player);
  if (!["CO", "BTN", "SB"].includes(villainPosition)) return null;

  const openIndex = preflopActions.indexOf(open);
  const heroAction = preflopActions.find((action, index) => (
    index > openIndex &&
    action.player === hero &&
    ["folds", "calls", "raises"].includes(action.verb)
  )) || null;
  if (!heroAction) return null;
  const heroActionIndex = preflopActions.indexOf(heroAction);
  const raiseBeforeHero = preflopActions.some((action, index) => (
    index > openIndex &&
    index < heroActionIndex &&
    action.verb === "raises"
  ));
  if (raiseBeforeHero) return null;

  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;
  const heroSeat = seats.find((seat) => seat.name === hero);
  const villainSeat = seats.find((seat) => seat.name === open.player);
  const effectiveStack = effectiveStackBetween(seats, hero, open.player);
  if (!Number.isFinite(effectiveStack) || effectiveStack / bigBlind < 10) return null;
  if (!Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 10) return null;
  if (!Number.isFinite(villainSeat?.chips) || villainSeat.chips / bigBlind < 10) return null;
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;
  if (lastPlayerAction(preflopActions, hero) !== heroAction) return null;

  return { villain: open.player, villainPosition, heroAction };
}

function streetWasDealt(lines, street) {
  const name = String(street || "").toUpperCase();
  const title = `${name[0]}${name.slice(1).toLowerCase()}`;
  return lines.some((line) => line.startsWith(`*** ${name} ***`) || line.startsWith(`** Dealing ${title} **`));
}

function playerFoldedOnStreet(player, actions) {
  if (!player) return false;
  return actions.some((action) => action.player === player && action.verb === "folds");
}

function postflopActiveSeats(seats, preflopActions) {
  const folded = new Set(preflopActions.filter((action) => action.verb === "folds").map((action) => action.player));
  return seats.filter((seat) => !folded.has(seat.name));
}

function heroCanActBeforeBet(hero, actions) {
  const firstBetIndex = actions.findIndex((action) => action.verb === "bets" || action.verb === "raises");
  return actions.some((action, index) => {
    const beforeBet = firstBetIndex < 0 || index <= firstBetIndex;
    return beforeBet && action.player === hero && ["checks", "bets"].includes(action.verb);
  });
}

function heroBetsBeforeAnyOtherBet(hero, actions) {
  const firstBet = firstAggressor(actions);
  return firstBet?.player === hero && firstBet.verb === "bets";
}

function heroActsInPosition(hero, actions) {
  const firstHeroIndex = actions.findIndex((action) => action.player === hero);
  if (firstHeroIndex < 0) return false;
  return actions.some((action, index) => index < firstHeroIndex && action.player !== hero);
}

function postflopPositionRank(position) {
  const ranks = {
    SB: 0,
    BB: 1,
    UTG: 2,
    "UTG+1": 3,
    MP: 4,
    LJ: 5,
    HJ: 6,
    CO: 7,
    BTN: 8,
  };
  return ranks[position] ?? 2;
}

function heroPostflopInPosition(hero, seats, positions, preflopActions) {
  if (!hero) return false;
  const activeSeats = postflopActiveSeats(seats, preflopActions);
  if (!activeSeats.some((seat) => seat.name === hero)) return false;
  const heroRank = postflopPositionRank(positions.get(hero));
  return activeSeats.every((seat) => seat.name === hero || heroRank >= postflopPositionRank(positions.get(seat.name)));
}

function hasCbetOpportunity(hero, preflopActions, streetActions) {
  if (!isSingleRaisedPot(preflopActions)) return false;
  const lastPreflopRaise = lastPreflopAggressor(preflopActions);
  if (lastPreflopRaise?.player !== hero) return false;
  return heroCanActBeforeBet(hero, streetActions);
}

function madeCbet(hero, preflopActions, streetActions) {
  return hasCbetOpportunity(hero, preflopActions, streetActions) && heroBetsBeforeAnyOtherBet(hero, streetActions);
}

function hasBarrelOpportunity(hero, previousStreetActions, streetActions) {
  if (!heroBetsBeforeAnyOtherBet(hero, previousStreetActions)) return false;
  return heroCanActBeforeBet(hero, streetActions);
}

function madeBarrel(hero, previousStreetActions, streetActions) {
  return hasBarrelOpportunity(hero, previousStreetActions, streetActions) && heroBetsBeforeAnyOtherBet(hero, streetActions);
}

function hasCbetTurnOpportunity(hero, preflopActions, flopActions, turnActions) {
  return madeCbet(hero, preflopActions, flopActions) && heroCanActBeforeBet(hero, turnActions);
}

function madeCbetTurn(hero, preflopActions, flopActions, turnActions) {
  return hasCbetTurnOpportunity(hero, preflopActions, flopActions, turnActions) && heroBetsBeforeAnyOtherBet(hero, turnActions);
}

function hasCbetRiverOpportunity(hero, preflopActions, flopActions, turnActions, riverActions) {
  return madeCbetTurn(hero, preflopActions, flopActions, turnActions) && heroCanActBeforeBet(hero, riverActions);
}

function madeCbetRiver(hero, preflopActions, flopActions, turnActions, riverActions) {
  return hasCbetRiverOpportunity(hero, preflopActions, flopActions, turnActions, riverActions) && heroBetsBeforeAnyOtherBet(hero, riverActions);
}

function isContinuationBet(hero, preflopActions, streetActions) {
  const lastPreflopRaise = [...preflopActions].reverse().find((action) => action.verb === "raises");
  if (lastPreflopRaise?.player !== hero) return false;
  return firstAggressor(streetActions)?.player === hero;
}

function isBarrelAfterHeroBet(hero, previousStreetActions, streetActions) {
  if (firstAggressor(previousStreetActions)?.player !== hero) return false;
  return firstAggressor(streetActions)?.player === hero;
}

function facedStreetBet(hero, actions) {
  const aggressor = firstAggressor(actions);
  if (!aggressor || aggressor.player === hero) return false;
  const aggressorIndex = actions.indexOf(aggressor);
  return actions.some((action, index) => index > aggressorIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb));
}

function foldedFacingStreetBet(hero, actions) {
  if (!facedStreetBet(hero, actions)) return false;
  return actions.some((action) => action.player === hero && action.verb === "folds");
}

function responseAfterAggressor(hero, actions, aggressor) {
  if (!aggressor || aggressor.player === hero) return null;
  const aggressorIndex = actions.indexOf(aggressor);
  return actions.find((action, index) => index > aggressorIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)) || null;
}

function facedCbet(hero, preflopActions, streetActions) {
  if (!isSingleRaisedPot(preflopActions)) return false;
  const cbetter = lastPreflopAggressor(preflopActions);
  if (!cbetter || cbetter.player === hero) return false;
  const firstBet = firstAggressor(streetActions);
  if (firstBet?.player !== cbetter.player || firstBet.verb !== "bets") return false;
  return Boolean(responseAfterAggressor(hero, streetActions, firstBet));
}

function foldedFacingCbet(hero, preflopActions, streetActions) {
  if (!facedCbet(hero, preflopActions, streetActions)) return false;
  return responseAfterAggressor(hero, streetActions, firstAggressor(streetActions))?.verb === "folds";
}

function facedBarrel(hero, previousStreetActions, streetActions) {
  const previousBettor = firstAggressor(previousStreetActions);
  if (!previousBettor || previousBettor.player === hero) return false;
  const firstBet = firstAggressor(streetActions);
  if (firstBet?.player !== previousBettor.player || firstBet.verb !== "bets") return false;
  return Boolean(responseAfterAggressor(hero, streetActions, firstBet));
}

function foldedFacingBarrel(hero, previousStreetActions, streetActions) {
  if (!facedBarrel(hero, previousStreetActions, streetActions)) return false;
  return responseAfterAggressor(hero, streetActions, firstAggressor(streetActions))?.verb === "folds";
}

function ggleaksDonkPreflopSpot(player, seats, positions, preflopActions, potBb, bigBlind) {
  if (!player || !["SB", "BB"].includes(positions.get(player))) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open.verb !== "raises" || open.player === player || open.allIn || raises.length !== 1) return null;
  if (["SB", "BB"].includes(positions.get(open.player))) return null;

  const effectiveStack = effectiveStackBetween(seats, player, open.player);
  if (!Number.isFinite(effectiveStack) || effectiveStack / bigBlind < 10) return null;
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;

  const openIndex = preflopActions.indexOf(open);
  const playerCall = preflopActions.find((action, index) => (
    index > openIndex && action.player === player && action.verb === "calls"
  )) || null;
  if (!playerCall || playerCall.allIn || lastPlayerAction(preflopActions, player) !== playerCall) return null;
  if (!actionSizeWithinEffective(playerCall, effectiveStack, 0.2)) return null;
  if (!Number.isFinite(potBb) || potBb > 10) return null;

  const activePlayers = postflopActiveSeats(seats, preflopActions);
  if (!activePlayers.some((seat) => seat.name === player) || !activePlayers.some((seat) => seat.name === open.player)) return null;
  return { villain: open.player };
}

function donkLeadOpportunity(player, villain, streetActions) {
  const playerIndex = firstDecisionIndex(streetActions, player);
  if (playerIndex < 0 || !["checks", "bets"].includes(streetActions[playerIndex]?.verb)) return false;
  const firstBetIndex = streetActions.findIndex((action) => ["bets", "raises"].includes(action.verb));
  if (firstBetIndex >= 0 && firstBetIndex < playerIndex) return false;
  const villainIndex = firstDecisionIndex(streetActions, villain);
  return villainIndex < 0 || playerIndex < villainIndex;
}

function madeDonkLead(player, villain, streetActions) {
  if (!donkLeadOpportunity(player, villain, streetActions)) return false;
  return streetActions[firstDecisionIndex(streetActions, player)]?.verb === "bets";
}

function madeDonkCheckCall(player, villain, streetActions) {
  const checkIndex = streetActions.findIndex((action) => action.player === player && action.verb === "checks");
  if (checkIndex < 0 || firstDecisionIndex(streetActions, player) !== checkIndex) return false;
  const firstBet = firstAggressor(streetActions);
  if (!firstBet || firstBet.player !== villain || firstBet.verb !== "bets") return false;
  const betIndex = streetActions.indexOf(firstBet);
  if (betIndex <= checkIndex) return false;
  const call = streetActions.find((action, index) => index > betIndex && action.player === player && action.verb === "calls") || null;
  return Boolean(call && lastPlayerAction(streetActions, player) === call);
}

function ggleaksDonkStats(player, seats, positions, preflopActions, flopActions, turnActions, riverActions, potBb, bigBlind) {
  const spot = ggleaksDonkPreflopSpot(player, seats, positions, preflopActions, potBb, bigBlind);
  if (!spot) {
    return { flopOpportunity: false, flop: false, turnOpportunity: false, turn: false, riverOpportunity: false, river: false };
  }

  const flopOpportunity = donkLeadOpportunity(player, spot.villain, flopActions);
  const flopCheckCall = madeDonkCheckCall(player, spot.villain, flopActions);
  const turnOpportunity = flopCheckCall && donkLeadOpportunity(player, spot.villain, turnActions);
  const turnCheckCall = flopCheckCall && madeDonkCheckCall(player, spot.villain, turnActions);
  const riverOpportunity = turnCheckCall && donkLeadOpportunity(player, spot.villain, riverActions);
  return {
    flopOpportunity,
    flop: flopOpportunity && madeDonkLead(player, spot.villain, flopActions),
    turnOpportunity,
    turn: turnOpportunity && madeDonkLead(player, spot.villain, turnActions),
    riverOpportunity,
    river: riverOpportunity && madeDonkLead(player, spot.villain, riverActions),
  };
}

function hasRiverBetOpportunity(hero, riverActions) {
  return riverActions.some((action) => action.player === hero && ["checks", "bets", "raises"].includes(action.verb));
}

function madeRiverBet(hero, riverActions) {
  return riverActions.some((action) => action.player === hero && ["bets", "raises"].includes(action.verb));
}

function heroReachedShowdown(lines, hero) {
  const escaped = escapeRegExp(hero);
  const directShow = lines.some((line) => new RegExp(`^${escaped}(?::\\s*|\\s+)shows \\[|Seat \\d+: ${escaped} .*showed \\[`).test(line));
  if (directShow) return true;
  const partyLine = partyPlayerSummaryLine(lines, hero);
  if (partyLine && /\[[^\]]+\]/.test(partyLine) && !/\(folded\)/i.test(partyLine)) return true;
  const showdownIndex = lines.findIndex((line) => line.startsWith("*** SHOWDOWN ***") || line.startsWith("*** SHOW DOWN ***") || line.startsWith("** Summary **"));
  if (showdownIndex < 0) return false;
  const summaryLine = lines.find((line) => new RegExp(`^Seat \\d+: ${escaped}\\b`).test(line));
  if (!summaryLine) return false;
  return /\b(showed|mucked|won|collected|lost)\b/i.test(summaryLine) && !/\bfolded\b/i.test(summaryLine);
}

function heroWonShowdown(lines, hero) {
  if (!heroReachedShowdown(lines, hero)) return false;
  const escaped = escapeRegExp(hero);
  const partyLine = partyPlayerSummaryLine(lines, hero);
  if (partyLine) return /\bnet\s+\+|collected\s+[\d,.]+/i.test(partyLine);
  return lines.some((line) =>
    new RegExp(`^${escaped} collected \\(?[$€£]?\\s*[\\d,.]+\\)?${COLLECTED_POT_SUFFIX_PATTERN}|Seat \\d+: ${escaped} .* (won|collected)`, "i").test(line)
  );
}

function hasBetVsMissedOpportunity(hero, preflopActions, streetActions) {
  if (!isSingleRaisedPot(preflopActions)) return false;
  const pfa = lastPreflopAggressor(preflopActions);
  if (!pfa || pfa.player === hero) return false;
  const pfaCheckIndex = streetActions.findIndex((action) => action.player === pfa.player && action.verb === "checks");
  if (pfaCheckIndex < 0) return false;
  const heroActionIndex = streetActions.findIndex((action, index) => index > pfaCheckIndex && action.player === hero && ["checks", "bets"].includes(action.verb));
  if (heroActionIndex < 0) return false;
  const betBeforeHero = streetActions.some((action, index) => index < heroActionIndex && action.verb === "bets");
  return !betBeforeHero;
}

function madeBetVsMissed(hero, preflopActions, streetActions) {
  if (!hasBetVsMissedOpportunity(hero, preflopActions, streetActions)) return false;
  const pfa = lastPreflopAggressor(preflopActions);
  const pfaCheckIndex = streetActions.findIndex((action) => action.player === pfa.player && action.verb === "checks");
  return streetActions.some((action, index) => index > pfaCheckIndex && action.player === hero && action.verb === "bets");
}

function hasBetVsMissedAfterStreetOpportunity(hero, previousStreetActions, streetActions) {
  const previousAggressor = firstAggressor(previousStreetActions);
  if (!previousAggressor || previousAggressor.player === hero) return false;
  const checkIndex = streetActions.findIndex((action) => action.player === previousAggressor.player && action.verb === "checks");
  if (checkIndex < 0) return false;
  const heroActionIndex = streetActions.findIndex((action, index) => index > checkIndex && action.player === hero && ["checks", "bets"].includes(action.verb));
  if (heroActionIndex < 0) return false;
  const betBeforeHero = streetActions.some((action, index) => index < heroActionIndex && action.verb === "bets");
  return !betBeforeHero;
}

function madeBetVsMissedAfterStreet(hero, previousStreetActions, streetActions) {
  if (!hasBetVsMissedAfterStreetOpportunity(hero, previousStreetActions, streetActions)) return false;
  const previousAggressor = firstAggressor(previousStreetActions);
  const checkIndex = streetActions.findIndex((action) => action.player === previousAggressor.player && action.verb === "checks");
  return streetActions.some((action, index) => index > checkIndex && action.player === hero && action.verb === "bets");
}

function hasBetVsMissedTurnOpportunity(hero, preflopActions, flopActions, turnActions) {
  const pfa = lastPreflopAggressor(preflopActions);
  if (!pfa || pfa.player === hero) return false;
  if (!madeCbet(pfa.player, preflopActions, flopActions)) return false;
  const checkIndex = turnActions.findIndex((action) => action.player === pfa.player && action.verb === "checks");
  if (checkIndex < 0) return false;
  const heroActionIndex = turnActions.findIndex((action, index) => index > checkIndex && action.player === hero && ["checks", "bets"].includes(action.verb));
  if (heroActionIndex < 0) return false;
  return !turnActions.some((action, index) => index < heroActionIndex && action.verb === "bets");
}

function madeBetVsMissedTurn(hero, preflopActions, flopActions, turnActions) {
  if (!hasBetVsMissedTurnOpportunity(hero, preflopActions, flopActions, turnActions)) return false;
  const pfa = lastPreflopAggressor(preflopActions);
  const checkIndex = turnActions.findIndex((action) => action.player === pfa.player && action.verb === "checks");
  return turnActions.some((action, index) => index > checkIndex && action.player === hero && action.verb === "bets");
}

function hasBetVsMissedRiverOpportunity(hero, preflopActions, flopActions, turnActions, riverActions) {
  const pfa = lastPreflopAggressor(preflopActions);
  if (!pfa || pfa.player === hero) return false;
  if (!madeCbetTurn(pfa.player, preflopActions, flopActions, turnActions)) return false;
  const checkIndex = riverActions.findIndex((action) => action.player === pfa.player && action.verb === "checks");
  if (checkIndex < 0) return false;
  const heroActionIndex = riverActions.findIndex((action, index) => index > checkIndex && action.player === hero && ["checks", "bets"].includes(action.verb));
  if (heroActionIndex < 0) return false;
  return !riverActions.some((action, index) => index < heroActionIndex && action.verb === "bets");
}

function madeBetVsMissedRiver(hero, preflopActions, flopActions, turnActions, riverActions) {
  if (!hasBetVsMissedRiverOpportunity(hero, preflopActions, flopActions, turnActions, riverActions)) return false;
  const pfa = lastPreflopAggressor(preflopActions);
  const checkIndex = riverActions.findIndex((action) => action.player === pfa.player && action.verb === "checks");
  return riverActions.some((action, index) => index > checkIndex && action.player === hero && action.verb === "bets");
}

function hasCheckRaiseOpportunity(hero, actions) {
  return Boolean(checkRaiseFacingBet(hero, actions));
}

function madeCheckRaise(hero, actions) {
  return checkRaiseFacingBet(hero, actions)?.response?.verb === "raises";
}

function checkRaiseFacingBet(hero, actions) {
  const heroCheckIndex = actions.findIndex((action) => action.player === hero && action.verb === "checks");
  if (heroCheckIndex < 0) return null;
  const betAfterCheck = actions.find((action, index) => index > heroCheckIndex && action.player !== hero && action.verb === "bets");
  if (!betAfterCheck) return null;
  const betIndex = actions.indexOf(betAfterCheck);
  const response = actions.find((action, index) => index > betIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)) || null;
  return response ? { bet: betAfterCheck, response } : null;
}

function hasBlindCheckRaiseOpportunity(hero, seats, positions, preflopActions, streetActions) {
  if (!["SB", "BB"].includes(positions.get(hero))) return false;
  if (!isSingleRaisedPot(preflopActions)) return false;
  if (postflopActiveSeats(seats, preflopActions).length !== 2) return false;
  return hasCheckRaiseOpportunity(hero, streetActions);
}

function madeBlindCheckRaise(hero, seats, positions, preflopActions, streetActions) {
  return hasBlindCheckRaiseOpportunity(hero, seats, positions, preflopActions, streetActions) && madeCheckRaise(hero, streetActions);
}

function isOpenRaisePositionForGgleaks(position) {
  return Boolean(position) && !["SB", "BB", "BTN"].includes(position);
}

function effectiveStackBetween(seats, playerA, playerB) {
  const a = seats.find((seat) => seat.name === playerA);
  const b = seats.find((seat) => seat.name === playerB);
  const values = [a?.chips, b?.chips].filter((value) => Number.isFinite(value) && value > 0);
  return values.length === 2 ? Math.min(...values) : null;
}

function actionSizeWithinEffective(action, effectiveStack, maxRatio = 0.2) {
  if (!action || !Number.isFinite(action.amount) || action.amount <= 0) return false;
  if (!Number.isFinite(effectiveStack) || effectiveStack <= 0) return false;
  return action.amount / effectiveStack <= maxRatio;
}

function ggleaksColdCallBbSpot(hero, seats, positions, preflopActions, bigBlind) {
  if (!hero || positions.get(hero) !== "BB" || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const open = firstNonFoldAction(preflopActions);
  if (!open || open.verb !== "raises" || open.player === hero || open.allIn) return null;

  const heroSeat = seats.find((seat) => seat.name === hero);
  const villainSeat = seats.find((seat) => seat.name === open.player);
  if (!Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 15) return null;
  if (!Number.isFinite(villainSeat?.chips) || villainSeat.chips / bigBlind < 10) return null;

  const effectiveStack = effectiveStackBetween(seats, hero, open.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;

  const openIndex = preflopActions.indexOf(open);
  const heroActionIndex = preflopActions.findIndex((action, index) =>
    index > openIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)
  );
  if (heroActionIndex < 0) return null;
  const raisedBeforeHero = preflopActions.some((action, index) =>
    index > openIndex && index < heroActionIndex && action.verb === "raises"
  );
  if (raisedBeforeHero) return null;

  return { villain: open.player, open, heroAction: preflopActions[heroActionIndex] };
}

function ggleaksRfiSpot(hero, seats, positions, preflopActions, bigBlind) {
  const position = positions.get(hero);
  if (!hero || !position || position === "BB" || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const heroSeat = seats.find((seat) => seat.name === hero);
  const minimumStackBb = position === "SB" ? 15 : 10;
  if (!Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < minimumStackBb) return null;

  if (position === "SB") {
    const bbPlayer = [...positions.entries()].find(([, playerPosition]) => playerPosition === "BB")?.[0];
    const effectiveStack = bbPlayer ? effectiveStackBetween(seats, hero, bbPlayer) : null;
    if (!Number.isFinite(effectiveStack) || effectiveStack / bigBlind < 15) return null;
  }

  const heroActionIndex = preflopActions.findIndex((action) => action.player === hero && ["folds", "calls", "raises"].includes(action.verb));
  if (heroActionIndex < 0) return null;
  const actionBeforeHero = preflopActions.some((action, index) => index < heroActionIndex && ["calls", "bets", "raises"].includes(action.verb));
  if (actionBeforeHero) return null;

  const heroAction = preflopActions[heroActionIndex];
  return { position, heroAction, raise: heroAction.verb === "raises" && firstNonFoldAction(preflopActions) === heroAction };
}

function ggleaksFoldToThreeBetSpot(hero, seats, positions, preflopActions, bigBlind) {
  if (!hero || positions.get(hero) === "BB" || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const open = firstNonFoldAction(preflopActions);
  if (!open || open.verb !== "raises" || open.player !== hero) return null;
  const openIndex = preflopActions.indexOf(open);
  const threeBetIndex = preflopActions.findIndex((action, index) => index > openIndex && action.verb === "raises" && action.player !== hero);
  if (threeBetIndex < 0) return null;
  const threeBet = preflopActions[threeBetIndex];

  const effectiveStack = effectiveStackBetween(seats, hero, threeBet.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;
  if (!Number.isFinite(threeBet.amount) || !Number.isFinite(open.amount) || open.amount <= 0 || threeBet.amount / open.amount < 2) return null;

  const heroAction = preflopActions.find((action, index) =>
    index > threeBetIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)
  ) || null;
  return heroAction ? { villain: threeBet.player, open, threeBet, heroAction } : null;
}

function ggleaksCbetTurnSpot(hero, seats, positions, preflopActions, flopActions, turnActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player !== hero || open.allIn) return null;
  const heroSeat = seats.find((seat) => seat.name === hero);
  if (!Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 10) return null;

  const activeNames = new Set(postflopActiveSeats(seats, preflopActions).map((seat) => seat.name));
  const callers = preflopActions.filter((action) =>
    action.verb === "calls"
    && action.player !== hero
    && activeNames.has(action.player)
  );
  const caller = callers[callers.length - 1];
  if (!caller || lastPlayerAction(preflopActions, caller.player) !== caller) return null;

  const effectiveStack = effectiveStackBetween(seats, hero, caller.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2) || !actionSizeWithinEffective(caller, effectiveStack, 0.2)) return null;
  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10) return null;

  const flopBet = flopActions.find((action) => action.player === hero && action.verb === "bets") || null;
  if (!flopBet || lastPlayerAction(flopActions, hero) !== flopBet) return null;
  if (!heroCanActBeforeBet(hero, turnActions)) return null;
  return { villain: caller.player, bet: heroBetsBeforeAnyOtherBet(hero, turnActions) };
}

function ggleaksCbetRiverSpot(hero, seats, preflopActions, flopActions, turnActions, riverActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player !== hero || open.allIn) return null;
  const heroSeat = seats.find((seat) => seat.name === hero);
  if (!Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 10) return null;

  const activeNames = new Set(postflopActiveSeats(seats, preflopActions).map((seat) => seat.name));
  const callers = preflopActions.filter((action) => action.verb === "calls" && action.player !== hero && activeNames.has(action.player));
  const caller = callers[callers.length - 1];
  if (!caller || lastPlayerAction(preflopActions, caller.player) !== caller) return null;
  const effectiveStack = effectiveStackBetween(seats, hero, caller.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2) || !actionSizeWithinEffective(caller, effectiveStack, 0.2)) return null;
  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10) return null;

  const flopBet = flopActions.find((action) => action.player === hero && action.verb === "bets") || null;
  const turnBet = turnActions.find((action) => action.player === hero && action.verb === "bets") || null;
  if (!flopBet || lastPlayerAction(flopActions, hero) !== flopBet) return null;
  if (!turnBet || lastPlayerAction(turnActions, hero) !== turnBet) return null;
  if (!heroCanActBeforeBet(hero, riverActions)) return null;
  return { villain: caller.player, bet: heroBetsBeforeAnyOtherBet(hero, riverActions) };
}

function ggleaksCbetFlopIpSpot(hero, seats, positions, preflopActions, flopActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0 || ["SB", "BB"].includes(positions.get(hero))) return null;
  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player !== hero || open.allIn) return null;
  const heroSeat = seats.find((seat) => seat.name === hero);
  if (!Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 10) return null;

  const activeNames = new Set(postflopActiveSeats(seats, preflopActions).map((seat) => seat.name));
  const callers = preflopActions.filter((action) =>
    action.verb === "calls" && action.player !== hero && activeNames.has(action.player) && ["SB", "BB"].includes(positions.get(action.player))
  );
  const caller = callers[callers.length - 1];
  if (!caller || lastPlayerAction(preflopActions, caller.player) !== caller) return null;
  const effectiveStack = effectiveStackBetween(seats, hero, caller.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2) || !actionSizeWithinEffective(caller, effectiveStack, 0.2)) return null;
  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10 || !heroCanActBeforeBet(hero, flopActions)) return null;
  return { villain: caller.player, bet: heroBetsBeforeAnyOtherBet(hero, flopActions) };
}

function ggleaksFoldToCbetFlopSpot(hero, seats, preflopActions, flopActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10 || !Number.isFinite(bigBlind) || bigBlind <= 0) return null;
  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player === hero || open.allIn) return null;
  const villainSeat = seats.find((seat) => seat.name === open.player);
  if (!Number.isFinite(villainSeat?.chips) || villainSeat.chips / bigBlind < 10) return null;
  const effectiveStack = effectiveStackBetween(seats, hero, open.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;
  const openIndex = preflopActions.indexOf(open);
  const heroCall = preflopActions.find((action, index) => index > openIndex && action.player === hero && action.verb === "calls") || null;
  if (!heroCall || heroCall.allIn || lastPlayerAction(preflopActions, hero) !== heroCall || !actionSizeWithinEffective(heroCall, effectiveStack, 0.2)) return null;
  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10) return null;
  const flopBet = flopActions.find((action) => action.player === open.player && action.verb === "bets") || null;
  if (!flopBet) return null;
  const betIndex = flopActions.indexOf(flopBet);
  const heroAction = flopActions.find((action, index) => index > betIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)) || null;
  return heroAction ? { villain: open.player, heroAction } : null;
}

function ggleaksFoldToCbetTurnSpot(hero, seats, preflopActions, flopActions, turnActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return null;
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;

  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player === hero || open.allIn) return null;

  const villainSeat = seats.find((seat) => seat.name === open.player);
  if (!Number.isFinite(villainSeat?.chips) || villainSeat.chips / bigBlind < 10) return null;
  const effectiveStack = effectiveStackBetween(seats, hero, open.player);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;

  const openIndex = preflopActions.indexOf(open);
  const heroCall = preflopActions.find((action, index) => index > openIndex && action.player === hero && action.verb === "calls") || null;
  if (!heroCall || heroCall.allIn || lastPlayerAction(preflopActions, hero) !== heroCall) return null;
  if (!actionSizeWithinEffective(heroCall, effectiveStack, 0.2)) return null;

  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10) return null;

  const flopBet = flopActions.find((action) => action.player === open.player && action.verb === "bets") || null;
  if (!flopBet || lastPlayerAction(flopActions, open.player) !== flopBet) return null;
  const flopBetIndex = flopActions.indexOf(flopBet);
  const flopCall = flopActions.find((action, index) => index > flopBetIndex && action.player === hero && action.verb === "calls") || null;
  if (!flopCall) return null;

  const turnBet = turnActions.find((action) => action.player === open.player && action.verb === "bets") || null;
  if (!turnBet) return null;
  const turnBetIndex = turnActions.indexOf(turnBet);
  const heroAction = turnActions.find((action, index) =>
    index > turnBetIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)
  ) || null;
  return heroAction ? { villain: open.player, heroAction } : null;
}

function hasGgleaksRiverBetOpportunity(hero, seats, preflopActions, riverActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return false;
  if (!postflopActiveSeats(seats, preflopActions).some((seat) => seat.name === hero)) return false;
  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  return Number.isFinite(potBb) && potBb <= 10 && heroCanActBeforeBet(hero, riverActions);
}

function madeGgleaksRiverBet(hero, seats, preflopActions, riverActions, lines, bigBlind) {
  return hasGgleaksRiverBetOpportunity(hero, seats, preflopActions, riverActions, lines, bigBlind)
    && heroBetsBeforeAnyOtherBet(hero, riverActions);
}

function preflopPotBb(lines, preflopActions, bigBlind) {
  if (!Number.isFinite(bigBlind) || bigBlind <= 0) return null;
  const flopIndex = lines.findIndex((line) => line.startsWith("*** FLOP ***") || /^\*\* Dealing Flop \*\*/i.test(line));
  const summaryIndex = lines.findIndex((line) => line.startsWith("*** SUMMARY ***") || /^\*\* Summary \*\*/i.test(line));
  const end = flopIndex >= 0 ? flopIndex : summaryIndex >= 0 ? summaryIndex : lines.length;
  const contributions = calculatePlayerContributions(lines.slice(0, end));
  const pot = [...contributions.values()].reduce((sum, value) => sum + value, 0);
  return pot / bigBlind;
}

function ggleaksCbetFlopOopSpot(hero, seats, positions, preflopActions, flopActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return null;
  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player !== hero || open.allIn) return null;

  const heroPosition = positions.get(hero);
  if (!isOpenRaisePositionForGgleaks(heroPosition)) return null;
  const activeNames = new Set(postflopActiveSeats(seats, preflopActions).map((seat) => seat.name));
  if (!activeNames.has(hero)) return null;

  const callerCandidates = preflopActions
    .map((action, index) => ({ action, index }))
    .filter(({ action }) => {
      if (action.verb !== "calls" || action.player === hero || !activeNames.has(action.player)) return false;
      const position = positions.get(action.player);
      return !["SB", "BB"].includes(position) && postflopPositionRank(position) > postflopPositionRank(heroPosition);
    });
  const caller = callerCandidates[callerCandidates.length - 1]?.action;
  if (!caller) return null;

  const effectiveStack = effectiveStackBetween(seats, hero, caller.player);
  const heroSeat = seats.find((seat) => seat.name === hero);
  if (!Number.isFinite(bigBlind) || bigBlind <= 0 || !Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 10) return null;
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2)) return null;
  if (!actionSizeWithinEffective(caller, effectiveStack, 0.2)) return null;

  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10) return null;
  if (!heroCanActBeforeBet(hero, flopActions)) return null;
  return { villain: caller.player, bet: heroBetsBeforeAnyOtherBet(hero, flopActions) };
}

function ggleaksPfaOopXrSpot(hero, seats, positions, preflopActions, flopActions, lines, bigBlind) {
  if (!hero || seats.length <= 5 || seats.length >= 10) return null;
  const open = firstNonFoldAction(preflopActions);
  const raises = preflopActions.filter((action) => action.verb === "raises");
  if (!open || open !== raises[0] || raises.length !== 1 || open.player !== hero || open.allIn) return null;
  const heroPosition = positions.get(hero);
  if (!isOpenRaisePositionForGgleaks(heroPosition)) return null;

  const heroSeat = seats.find((seat) => seat.name === hero);
  if (!Number.isFinite(bigBlind) || bigBlind <= 0 || !Number.isFinite(heroSeat?.chips) || heroSeat.chips / bigBlind < 10) return null;
  const activeNames = new Set(postflopActiveSeats(seats, preflopActions).map((seat) => seat.name));
  if (!activeNames.has(hero)) return null;
  const callers = preflopActions.filter((action) => {
    if (action.verb !== "calls" || action.player === hero || !activeNames.has(action.player)) return false;
    const position = positions.get(action.player);
    return !["SB", "BB"].includes(position) && postflopPositionRank(position) > postflopPositionRank(heroPosition);
  });
  const villainCall = callers[callers.length - 1];
  if (!villainCall || lastPlayerAction(preflopActions, villainCall.player) !== villainCall) return null;
  const villain = villainCall.player;

  const effectiveStack = effectiveStackBetween(seats, hero, villain);
  if (!actionSizeWithinEffective(open, effectiveStack, 0.2) || !actionSizeWithinEffective(villainCall, effectiveStack, 0.2)) return null;

  const potBb = preflopPotBb(lines, preflopActions, bigBlind);
  if (!Number.isFinite(potBb) || potBb > 10) return null;

  const heroCheckIndex = flopActions.findIndex((action) => action.player === hero && action.verb === "checks");
  if (heroCheckIndex < 0) return null;
  const betBeforeHeroCheck = flopActions.some((action, index) => index < heroCheckIndex && ["bets", "raises"].includes(action.verb));
  if (betBeforeHeroCheck) return null;
  const villainBet = flopActions.find((action, index) => index > heroCheckIndex && action.player === villain && action.verb === "bets");
  if (!villainBet) return null;
  const betIndex = flopActions.indexOf(villainBet);
  const response = flopActions.find((action, index) => index > betIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)) || null;
  return response ? { villain, response } : null;
}

function hasGgleaksPfaOopXrOpportunity(hero, seats, positions, preflopActions, flopActions, lines, bigBlind) {
  return Boolean(ggleaksPfaOopXrSpot(hero, seats, positions, preflopActions, flopActions, lines, bigBlind));
}

function madeGgleaksPfaOopXr(hero, seats, positions, preflopActions, flopActions, lines, bigBlind) {
  return ggleaksPfaOopXrSpot(hero, seats, positions, preflopActions, flopActions, lines, bigBlind)?.response?.verb === "raises";
}

function hasPfaCheckRaiseOpportunity(hero, preflopActions, streetActions) {
  if (lastPreflopAggressor(preflopActions)?.player !== hero) return false;
  return hasCheckRaiseOpportunity(hero, streetActions);
}

function madePfaCheckRaise(hero, preflopActions, streetActions) {
  return hasPfaCheckRaiseOpportunity(hero, preflopActions, streetActions) && madeCheckRaise(hero, streetActions);
}

function hasPfaBarrelCheckRaiseOpportunity(hero, previousStreetActions, streetActions) {
  if (firstAggressor(previousStreetActions)?.player !== hero) return false;
  return hasCheckRaiseOpportunity(hero, streetActions);
}

function madePfaBarrelCheckRaise(hero, previousStreetActions, streetActions) {
  return hasPfaBarrelCheckRaiseOpportunity(hero, previousStreetActions, streetActions) && madeCheckRaise(hero, streetActions);
}

function hasRaiseIpOpportunity(hero, actions) {
  const firstBet = firstAggressor(actions);
  if (!firstBet || firstBet.player === hero) return false;
  const response = responseAfterAggressor(hero, actions, firstBet);
  return Boolean(response && heroActsInPosition(hero, actions));
}

function madeRaiseIp(hero, actions) {
  if (!hasRaiseIpOpportunity(hero, actions)) return false;
  const firstBet = firstAggressor(actions);
  return responseAfterAggressor(hero, actions, firstBet)?.verb === "raises";
}

function hasFoldVsBetOpportunity(hero, actions) {
  const firstBet = firstAggressor(actions);
  if (!firstBet || firstBet.player === hero) return false;
  return Boolean(responseAfterAggressor(hero, actions, firstBet));
}

function madeFoldVsBet(hero, actions) {
  if (!hasFoldVsBetOpportunity(hero, actions)) return false;
  const firstBet = firstAggressor(actions);
  return responseAfterAggressor(hero, actions, firstBet)?.verb === "folds";
}

function firstDecisionIndex(actions, player) {
  return actions.findIndex((action) => action.player === player && ["checks", "bets", "folds", "calls", "raises"].includes(action.verb));
}

function hasSkippedCbetOop(hero, preflopActions, streetActions) {
  if (lastPreflopAggressor(preflopActions)?.player !== hero) return false;
  const heroActionIndex = firstDecisionIndex(streetActions, hero);
  if (heroActionIndex < 0) return false;
  if (streetActions[heroActionIndex]?.verb !== "checks") return false;
  const firstOpponentActionIndex = streetActions.findIndex((action) => action.player !== hero && ["checks", "bets", "folds", "calls", "raises"].includes(action.verb));
  return firstOpponentActionIndex < 0 || heroActionIndex < firstOpponentActionIndex;
}

function hasSkipCbetOpportunity(hero, preflopActions, streetActions) {
  return Boolean(skipCbetFacingBet(hero, preflopActions, streetActions));
}

function skippedCbetAnd(hero, preflopActions, streetActions, responseVerb) {
  const facingBet = skipCbetFacingBet(hero, preflopActions, streetActions);
  if (!facingBet) return false;
  return facingBet.response?.verb === responseVerb;
}

function skipCbetFacingBet(hero, preflopActions, streetActions) {
  if (!hasSkippedCbetOop(hero, preflopActions, streetActions)) return null;
  const heroCheckIndex = firstDecisionIndex(streetActions, hero);
  const betAfterCheck = streetActions.find((action, index) => index > heroCheckIndex && action.player !== hero && action.verb === "bets");
  if (!betAfterCheck) return null;
  const betIndex = streetActions.indexOf(betAfterCheck);
  const response = streetActions.find((action, index) => index > betIndex && action.player === hero && ["folds", "calls", "raises"].includes(action.verb)) || null;
  return response ? { bet: betAfterCheck, response } : null;
}

function summarizeHero(actions) {
  if (!actions.length) return "sem acao";
  return actions.map((action) => `${action.verb}${action.allIn ? " all-in" : ""}`).join(" / ");
}

function xmlDirectChildren(element, name) {
  const expected = String(name || "").toLowerCase();
  return Array.from(element?.children || []).filter((child) => String(child.localName || child.tagName || "").toLowerCase() === expected);
}

function xmlDirectChild(element, name) {
  return xmlDirectChildren(element, name)[0] || null;
}

function xmlChildText(element, name) {
  return String(xmlDirectChild(element, name)?.textContent || "").trim();
}

function h2nXmlNumber(value) {
  const normalized = String(value || "").replace(/,/g, "").replace(/[^\d.-]/g, "");
  const number = Number(normalized);
  return Number.isFinite(number) ? number : 0;
}

function h2nCard(token) {
  const value = String(token || "").trim().toUpperCase();
  if (!value || value === "X") return "";
  const match = value.match(/^([CDHS])(10|[2-9JQKA])$/);
  if (!match) return "";
  const rank = match[2] === "10" ? "T" : match[2];
  return `${rank}${match[1].toLowerCase()}`;
}

function h2nCardList(value) {
  return String(value || "").trim().split(/\s+/).map(h2nCard).filter(Boolean).join(" ");
}

function h2nActionLines(round, initialContributions = new Map(), allInActionNos = new Set()) {
  const contributions = new Map(initialContributions);
  let currentMax = Math.max(0, ...contributions.values());
  const lines = [];
  for (const action of xmlDirectChildren(round, "action")) {
    const player = String(action.getAttribute("player") || "").trim();
    const type = Number(action.getAttribute("type"));
    const amount = h2nXmlNumber(action.getAttribute("sum"));
    if (!player || [1, 2, 15].includes(type)) continue;
    if (type === 0) {
      lines.push(`${player}: folds`);
      continue;
    }
    if (type === 4) {
      lines.push(`${player}: checks`);
      continue;
    }

    const previous = contributions.get(player) || 0;
    const total = type === 23 ? amount : previous + amount;
    const allInSuffix = allInActionNos.has(action.getAttribute("no")) ? " and is all-in" : "";
    if (type === 3) {
      lines.push(`${player}: calls ${amount}`);
    } else if (type === 5) {
      lines.push(`${player}: bets ${amount}`);
    } else if (type === 23) {
      lines.push(`${player}: raises ${Math.max(0, total - currentMax)} to ${total}${allInSuffix}`);
    } else if (type === 7) {
      if (currentMax <= previous) lines.push(`${player}: bets ${amount} and is all-in`);
      else if (total <= currentMax) lines.push(`${player}: calls ${amount} and is all-in`);
      else lines.push(`${player}: raises ${Math.max(0, total - currentMax)} to ${total} and is all-in`);
    } else {
      continue;
    }
    contributions.set(player, total);
    currentMax = Math.max(currentMax, total);
  }
  return lines;
}

function h2nAllInActionNos(game, players, rounds) {
  const stacks = new Map(players.map((player) => [player.name, player.chips]));
  const committed = new Map();
  const preflopContributions = new Map();
  const allIn = new Set();
  for (const action of xmlDirectChildren(rounds.get(0), "action")) {
    const player = String(action.getAttribute("player") || "").trim();
    const type = Number(action.getAttribute("type"));
    const amount = h2nXmlNumber(action.getAttribute("sum"));
    if (!player || !amount) continue;
    committed.set(player, (committed.get(player) || 0) + amount);
    if ([1, 2].includes(type)) preflopContributions.set(player, amount);
  }

  for (let roundNo = 1; roundNo <= 4; roundNo += 1) {
    const contributions = roundNo === 1 ? new Map(preflopContributions) : new Map();
    for (const action of xmlDirectChildren(rounds.get(roundNo), "action")) {
      const player = String(action.getAttribute("player") || "").trim();
      const type = Number(action.getAttribute("type"));
      const amount = h2nXmlNumber(action.getAttribute("sum"));
      if (!player || ![3, 5, 7, 23].includes(type)) continue;
      const previous = contributions.get(player) || 0;
      const paid = type === 23 ? Math.max(0, amount - previous) : amount;
      contributions.set(player, type === 23 ? amount : previous + amount);
      const totalCommitted = (committed.get(player) || 0) + paid;
      committed.set(player, totalCommitted);
      if (type === 7 || totalCommitted >= (stacks.get(player) || Infinity)) {
        allIn.add(action.getAttribute("no"));
      }
    }
  }
  return allIn;
}

function convertH2nXmlRoot(root, selectedGame = null) {
  const rootGeneral = xmlDirectChild(root, "general");
  const game = selectedGame || xmlDirectChild(root, "game");
  const gameGeneral = xmlDirectChild(game, "general");
  if (!rootGeneral || !game || !gameGeneral) return "";

  const handId = String(game.getAttribute("gamecode") || "").replace(/\D/g, "");
  const sessionCode = String(root.getAttribute("sessioncode") || root.getAttribute("session_code") || "").replace(/\D/g, "");
  const tournamentCode = xmlChildText(rootGeneral, "tournamentcode").replace(/\D/g, "") || sessionCode || handId;
  const tournamentName = xmlChildText(rootGeneral, "tournamentname") || xmlChildText(rootGeneral, "tablename") || "Torneio H2N";
  const buyIn = xmlChildText(rootGeneral, "buyin") || xmlChildText(rootGeneral, "totalbuyin") || "Buy-in nao informado";
  const date = xmlChildText(gameGeneral, "startdate") || new Date().toISOString().replace("T", " ").slice(0, 19);
  const forcedRound = xmlDirectChildren(game, "round").find((round) => Number(round.getAttribute("no")) === 0);
  const forcedActions = xmlDirectChildren(forcedRound, "action");
  const forcedAmount = (type) => h2nXmlNumber(forcedActions.find((action) => Number(action.getAttribute("type")) === type)?.getAttribute("sum"));
  const smallBlind = h2nXmlNumber(xmlChildText(gameGeneral, "smallblind")) || forcedAmount(1);
  const bigBlind = h2nXmlNumber(xmlChildText(gameGeneral, "bigblind")) || forcedAmount(2);
  const ante = h2nXmlNumber(xmlChildText(gameGeneral, "ante")) || forcedAmount(15);
  const nickname = xmlChildText(rootGeneral, "nickname");
  if (!handId || !nickname || !smallBlind || !bigBlind) return "";

  const playersNode = xmlDirectChild(gameGeneral, "players");
  const players = xmlDirectChildren(playersNode, "player").map((player) => ({
    seat: h2nXmlNumber(player.getAttribute("seat")),
    name: String(player.getAttribute("name") || "").trim(),
    chips: h2nXmlNumber(player.getAttribute("chips")),
    dealer: player.getAttribute("dealer") === "1",
    bet: h2nXmlNumber(player.getAttribute("bet")),
    win: h2nXmlNumber(player.getAttribute("win")),
  })).filter((player) => player.seat && player.name);
  if (!players.length || !players.some((player) => player.name === nickname)) return "";

  const rounds = new Map(xmlDirectChildren(game, "round").map((round) => [Number(round.getAttribute("no")), round]));
  const allInActionNos = h2nAllInActionNos(game, players, rounds);
  const pocketCards = new Map();
  for (const cards of Array.from(game.getElementsByTagName("cards"))) {
    if (String(cards.getAttribute("type") || "").toLowerCase() !== "pocket") continue;
    const value = h2nCardList(cards.textContent);
    if (value) pocketCards.set(String(cards.getAttribute("player") || "").trim(), value);
  }
  const heroCards = pocketCards.get(nickname) || "";

  const boardByStreet = new Map();
  for (const cards of Array.from(game.getElementsByTagName("cards"))) {
    const type = String(cards.getAttribute("type") || "");
    if (!["Flop", "Turn", "River"].includes(type)) continue;
    const value = h2nCardList(cards.textContent);
    if (value) boardByStreet.set(type, value);
  }

  const dealerSeat = players.find((player) => player.dealer)?.seat || players[0].seat;
  const tableName = xmlChildText(rootGeneral, "tablename") || tournamentName;
  const sourceSite = String(root.localName || root.tagName || "").toLowerCase() === "session" ? "Champion" : "H2N XML";
  const levelBlinds = ante > 0 ? `${smallBlind}/${bigBlind}/${ante}` : `${smallBlind}/${bigBlind}`;
  const lines = [
    `${sourceSite} Hand #${handId}: Tournament #${tournamentCode}, ${tournamentName} | ${buyIn} Hold'em No Limit - Level H2N (${levelBlinds}) - ${date}`,
    `Table '${tableName}' ${players.length}-max Seat #${dealerSeat} is the button`,
    ...players.map((player) => `Seat ${player.seat}: ${player.name} (${player.chips} in chips)`),
  ];

  const blindContributions = new Map();
  for (const action of xmlDirectChildren(rounds.get(0), "action")) {
    const player = String(action.getAttribute("player") || "").trim();
    const type = Number(action.getAttribute("type"));
    const amount = h2nXmlNumber(action.getAttribute("sum"));
    if (!player || !amount) continue;
    if (type === 15) lines.push(`${player}: posts the ante ${amount}`);
    if (type === 1) {
      lines.push(`${player}: posts small blind ${amount}`);
      blindContributions.set(player, amount);
    }
    if (type === 2) {
      lines.push(`${player}: posts big blind ${amount}`);
      blindContributions.set(player, amount);
    }
  }

  lines.push("*** HOLE CARDS ***", `Dealt to ${nickname} [${heroCards || "?? ??"}]`, ...h2nActionLines(rounds.get(1), blindContributions, allInActionNos));
  const flop = boardByStreet.get("Flop");
  const turn = boardByStreet.get("Turn");
  const river = boardByStreet.get("River");
  if (flop) lines.push(`*** FLOP *** [${flop}]`, ...h2nActionLines(rounds.get(2), new Map(), allInActionNos));
  if (turn) lines.push(`*** TURN *** [${flop}] [${turn}]`, ...h2nActionLines(rounds.get(3), new Map(), allInActionNos));
  if (river) lines.push(`*** RIVER *** [${flop} ${turn || ""}] [${river}]`, ...h2nActionLines(rounds.get(4), new Map(), allInActionNos));

  const folded = new Set(Array.from(game.getElementsByTagName("action"))
    .filter((action) => Number(action.getAttribute("type")) === 0)
    .map((action) => String(action.getAttribute("player") || "").trim()));
  const shownPlayers = [...pocketCards.keys()].filter((player) => player !== nickname && !folded.has(player));
  if (shownPlayers.length) {
    lines.push("*** SHOWDOWN ***");
    for (const [player, cards] of pocketCards) {
      if (!folded.has(player)) lines.push(`${player}: shows [${cards}]`);
    }
  }
  for (const player of players) {
    if (player.win > 0) lines.push(`${player.name} collected ${player.win} from pot`);
  }

  lines.push("*** SUMMARY ***", `Total pot ${players.reduce((sum, player) => sum + player.win, 0)}`);
  for (const player of players) {
    const cards = pocketCards.get(player.name);
    if (folded.has(player.name)) lines.push(`Seat ${player.seat}: ${player.name} folded`);
    else if (cards && shownPlayers.length && player.win > 0) lines.push(`Seat ${player.seat}: ${player.name} showed [${cards}] and won (${player.win})`);
    else if (cards && shownPlayers.length) lines.push(`Seat ${player.seat}: ${player.name} showed [${cards}] and lost`);
    else if (player.win > 0) lines.push(`Seat ${player.seat}: ${player.name} won (${player.win})`);
  }
  const heroPlayer = players.find((player) => player.name === nickname);
  const heroNet = heroPlayer.win - heroPlayer.bet;
  const heroBalance = Math.max(0, heroPlayer.chips + heroNet);
  lines.push(`${nickname} balance ${heroBalance}, bet ${heroPlayer.bet}, collected ${heroPlayer.win}, net ${heroNet >= 0 ? "+" : ""}${heroNet}`);
  return lines.join("\n");
}

function normalizeH2nXmlExport(text) {
  if (typeof DOMParser === "undefined") throw new Error("Este ambiente nao possui leitor XML.");
  const source = String(text || "").replace(/<\?xml[^>]*\?>/gi, "").trim();
  if (!source) return "";
  const documentXml = new DOMParser().parseFromString(`<h2n-export>${source}</h2n-export>`, "application/xml");
  if (documentXml.querySelector("parsererror")) throw new Error("XML do H2N invalido ou corrompido.");
  const roots = xmlDirectChildren(documentXml.documentElement, "root");
  const sessions = xmlDirectChildren(documentXml.documentElement, "session");
  const convertedRoots = roots.map((root) => convertH2nXmlRoot(root)).filter(Boolean);
  const convertedSessions = sessions.flatMap((session) => xmlDirectChildren(session, "game").map((game) => convertH2nXmlRoot(session, game))).filter(Boolean);
  const hands = [...convertedRoots, ...convertedSessions];
  // Live room XMLs can be discovered while the current hand is still being
  // written. Treat that snapshot as empty and retry it after its mtime changes.
  if (!hands.length) return "";
  return hands.join("\n\n");
}

async function importPayloadText(name, text) {
  const source = String(text || "");
  const isXmlFile = String(name || "").toLowerCase().endsWith(".xml");
  const isSupportedXml = /<(?:root|session)\b[\s\S]*?<game\s+gamecode=/i.test(source);
  if (isXmlFile && !isSupportedXml) return makeImportTotals();
  return importText(isSupportedXml ? normalizeH2nXmlExport(source) : source, { renderNow: false, sourceName: name });
}

function shouldReplaceImportedHand(existing, incoming) {
  if (!existing || !incoming) return false;
  const existingVersion = Number(existing.parserVersion) || 0;
  const incomingVersion = Number(incoming.parserVersion) || 0;
  if (incomingVersion > existingVersion) return true;

  const existingRawLength = String(existing.raw || "").length;
  const incomingRawLength = String(incoming.raw || "").length;
  if (incomingRawLength > existingRawLength + 40 && incomingRawLength > existingRawLength * 1.02) return true;

  const existingNet = Number(existing.netChips);
  const incomingNet = Number(incoming.netChips);
  const existingCollected = Number(existing.collectedChips);
  const incomingCollected = Number(incoming.collectedChips);
  if (!Number.isFinite(existingNet) && Number.isFinite(incomingNet)) return true;
  if ((existing.site || incoming.site || "").match(/champion|ipoker|h2n xml/i)) {
    if ((existingCollected || 0) <= 0 && incomingCollected > 0) return true;
    if (Number.isFinite(existingNet) && Number.isFinite(incomingNet) && existingNet < 0 && incomingNet > existingNet && incomingCollected > existingCollected) return true;
  }
  return false;
}

async function importText(text, options = {}) {
  const chunks = splitHands(text);
  const parsed = [];
  const batchSize = 120;
  for (let index = 0; index < chunks.length; index += 1) {
    const hand = parseHand(chunks[index], { skipEquity: true });
    if (hand) parsed.push(hand);
    if ((index + 1) % batchSize === 0 && index + 1 < chunks.length) {
      if ((index + 1) % (batchSize * 5) === 0) {
        const source = options.sourceName ? ` de ${escapeHtml(options.sourceName)}` : "";
        setImportStatus(`Lendo${source}: <strong>${fmt(index + 1)}</strong> / <strong>${fmt(chunks.length)}</strong> maos...`);
      }
      await new Promise((resolve) => window.setTimeout(resolve, 0));
    }
  }
  const hudRosterChanged = updateLiveHudTableSnapshots(parsed);
  const summaries = chunks.length ? [] : parseTournamentSummaries(text);
  let added = 0;
  let replaced = 0;
  let duplicates = 0;
  let tournamentsAdded = 0;
  let tournamentDuplicates = 0;
  const equityCandidates = [];
  const addedHands = [];

  for (const hand of parsed) {
    if (!shouldStoreHand(hand)) continue;
    if (state.handIds.has(hand.handId)) {
      const existingIndex = state.hands.findIndex((savedHand) => savedHand?.handId === hand.handId);
      const existingHand = existingIndex >= 0 ? state.hands[existingIndex] : null;
      if (existingHand && shouldReplaceImportedHand(existingHand, hand)) {
        state.hands[existingIndex] = hand;
        addedHands.push(hand);
        if (hand.equityPending) equityCandidates.push({ handId: hand.handId, index: existingIndex });
        sessionHandIds.add(hand.handId);
        if (hand.hero) state.heroes.add(hand.hero);
        replaced += 1;
      }
      duplicates += 1;
      continue;
    }
    state.handIds.add(hand.handId);
    const handIndex = state.hands.length;
    state.hands.push(hand);
    addedHands.push(hand);
    if (hand.equityPending) equityCandidates.push({ handId: hand.handId, index: handIndex });
    sessionHandIds.add(hand.handId);
    added += 1;
    if (hand.hero) state.heroes.add(hand.hero);
  }
  if (added || replaced) {
    replayCache.clear();
    statHandComboCache.clear();
    statDecisionCache.clear();
    statActionTimelineCache.clear();
    statBetSizeCache.clear();
    srpSpotCache.clear();
    threeBetPotSpotCache.clear();
    blindWarSpotCache.clear();
    playerHudCache.signature = "";
    playerHudCache.stats = new Map();
    currentReplayContext = null;
    queueBackgroundEvCalculation(equityCandidates);
    indexOpponentHudHands(addedHands);
  }

  for (const tournament of summaries) {
    if (state.tournamentIds.has(tournament.tournamentId)) {
      const existingIndex = state.tournaments.findIndex((item) => item.tournamentId === tournament.tournamentId);
      if (existingIndex >= 0) state.tournaments[existingIndex] = tournament;
      tournamentDuplicates += 1;
      continue;
    }
    state.tournamentIds.add(tournament.tournamentId);
    state.tournaments.push(tournament);
    tournamentsAdded += 1;
  }

  if (options.renderNow !== false) safeRender();
  return {
    found: chunks.length,
    parsed: parsed.length,
    added,
    replaced,
    duplicates,
    tournamentsFound: summaries.length,
    tournamentsAdded,
    tournamentDuplicates,
    hudRosterChanged,
  };
}

function compareHudHandIds(left, right) {
  const a = String(left || "").replace(/\D/g, "");
  const b = String(right || "").replace(/\D/g, "");
  if (a.length !== b.length) return a.length - b.length;
  return a.localeCompare(b);
}

function hudTableSnapshotFromHand(hand) {
  if (!hand?.raw) return null;
  const raw = String(hand.raw).replace(/\r/g, "");
  const roster = parseSeats(raw.split("\n"))
    .map((seat) => ({ name: seat.name, seat: seat.seat }))
    .filter((seat) => seat.name);
  if (!roster.length) return null;
  const declaredMaxSeats = Number(raw.match(/^Table\s+.+?\s+(\d+)-max\b/im)?.[1]);
  const maxSeats = declaredMaxSeats || Math.max(roster.length, ...roster.map((seat) => Number(seat.seat) || 0));
  const tableName = raw.match(/^Table\s+['\"]?(.+?)['\"]?\s+\d+-max\b/im)?.[1] || "";
  const aliases = [...new Set([
    String(hand.tournamentId || "").replace(/\D/g, ""),
    ...[...tableName.matchAll(/\b\d{8,}\b/g)].map((match) => match[0]),
  ].filter(Boolean))];
  if (!aliases.length && !tableName) return null;
  return {
    handId: String(hand.handId || ""),
    rawLength: raw.length,
    aliases,
    players: roster,
    hero: hand.hero || "",
    maxSeats,
    tableName,
  };
}

function updateLiveHudTableSnapshots(hands) {
  let changed = false;
  for (const hand of hands || []) {
    const snapshot = hudTableSnapshotFromHand(hand);
    if (!snapshot) continue;
    const key = snapshot.aliases[0] || `name:${snapshot.tableName.toLowerCase()}`;
    const current = liveHudTableSnapshots.get(key);
    const order = current ? compareHudHandIds(snapshot.handId, current.handId) : 1;
    if (current && (order < 0 || (order === 0 && snapshot.rawLength < current.rawLength))) continue;
    const rosterSignature = snapshot.players.map((player) => `${player.seat}:${player.name}`).join("|");
    const currentSignature = current?.players?.map((player) => `${player.seat}:${player.name}`).join("|") || "";
    liveHudTableSnapshots.set(key, snapshot);
    if (!current || rosterSignature !== currentSignature || snapshot.tableName !== current.tableName) changed = true;
  }
  return changed;
}

function parseTournamentSummaries(text) {
  const normalized = text.replace(/\r/g, "").trim();
  if (!normalized) return [];
  if (!/(^|\n)\s*(?:PokerStars\s+)?Tournament\s+(?:#|ID\b)|(^|\n)\s*Tournament\s*:/i.test(normalized)) return [];
  const blocks = normalized
    .split(/\n{2,}(?=(?:PokerStars\s+)?Tournament\s+(?:#|ID\b)|Tournament\s*:)/gi)
    .map((block) => block.trim())
    .filter(Boolean);
  return blocks.map(parseTournamentSummary).filter(Boolean);
}

function parseTournamentSummary(block) {
  const lines = block.split("\n").map((line) => line.trim()).filter(Boolean);
  const header = parseTournamentHeader(lines);
  if (!header?.id) return null;

  const buyInLine = findTournamentLine(lines, [
    /\bbuy[-\s]?in\b/i,
    /\bentrada\b/i,
  ]);
  const playersLine = findTournamentLine(lines, [
    /\b(players?|entrants?|entries|field|total players)\b/i,
    /^\s*[\d,.]+\s+players?\b/i,
  ]);
  const prizePoolLine = findTournamentLine(lines, [/\b(total\s+)?prize\s+pool\b/i]);
  const startedLine = findTournamentLine(lines, [
    /\btournament started\b/i,
    /\b(started|start time|date)\s*:/i,
  ]);
  const finishLine = findTournamentLine(lines, [
    /\byou finished\b/i,
    /\bfinished\b/i,
    /\b(place|rank|position|finish)\s*:/i,
  ]);
  const reentryLine = findTournamentLine(lines, [/\bre[-\s]?entr(?:y|ies|ied|ants?)\b/i, /\brebuys?\b/i]);
  const payoutLine = findTournamentLine(lines, [
    /^\d+(?:st|nd|rd|th)?\s*:/i,
    /\b(you received|winnings?|prize|award|paid|payout)\b/i,
    /\bwon\s+[$€£]?[\d,.]+/i,
  ], [/\bprize\s+pool\b/i, /\bbuy[-\s]?in\b/i]);

  const buyInParts = parseBuyInParts(buyInLine);
  const buyInTotal = buyInParts.reduce((sum, value) => sum + value, 0);
  const reentries = parseReentries(reentryLine);
  const totalCost = buyInTotal * (1 + reentries);
  const prize = parsePrize(payoutLine, reentryLine, finishLine);
  const place = parseTournamentPlace(finishLine) ?? parseTournamentPlace(payoutLine);
  const players = parsePlayers(playersLine);
  const prizePool = firstMoney(prizePoolLine);
  const name = header.name || "Torneio sem nome";

  return {
    tournamentId: header.id,
    name,
    format: detectTournamentFormat(name, buyInLine),
    buyInTotal,
    buyInParts,
    totalCost,
    players,
    prizePool,
    startedAt: cleanStartedAt(startedLine),
    place,
    reentries,
    prize,
    profit: prize - totalCost,
  };
}

function parseTournamentHeader(lines) {
  const firstLine = lines[0] || "";
  let match = firstLine.match(/^(?:PokerStars\s+)?Tournament\s+#?([A-Z]*\d+)\s*,\s*(.+?)(?:,\s*Hold'?em|\s+-\s+Hold'?em|$)/i);
  if (match) return { id: match[1], name: cleanTournamentName(match[2]) };

  const idLine = lines.find((line) => /\btournament\s+(?:id\s*:|#)/i.test(line));
  match = idLine?.match(/\btournament\s+(?:id\s*:|#)\s*([A-Z]*\d+)/i);
  const id = match?.[1];
  const nameLine = lines.find((line) => /\btournament\s*:/i.test(line) && !/\btournament\s+id\s*:/i.test(line));
  const name = cleanTournamentName(nameLine?.replace(/^tournament\s*:\s*/i, "") || firstLine.replace(/^(?:PokerStars\s+)?Tournament\s+#?[A-Z]*\d+\s*,?\s*/i, ""));
  return id ? { id, name } : null;
}

function cleanTournamentName(value) {
  return String(value || "")
    .replace(/\bHold'?em\s+No\s+Limit\b.*$/i, "")
    .replace(/\bNo\s+Limit\s+Hold'?em\b.*$/i, "")
    .replace(/[\s,-]+$/g, "")
    .trim();
}

function findTournamentLine(lines, patterns, excludedPatterns = []) {
  return lines.find((line) => {
    if (excludedPatterns.some((pattern) => pattern.test(line))) return false;
    return patterns.some((pattern) => pattern.test(line));
  }) || "";
}

function parseBuyInParts(line) {
  const values = buyInPartsFromText(line);
  if (values.length) return values;
  const value = firstMoney(line);
  return value ? [value] : [];
}

function parseReentries(line) {
  if (!line) return 0;
  const direct = line.match(/(?:made\s+)?(\d+)\s+re[-\s]?entr/i);
  const labeled = line.match(/(?:re[-\s]?entr(?:y|ies)|rebuys?)\s*:?\s*(\d+)/i);
  return toNumber(direct?.[1] || labeled?.[1]) || 0;
}

function parsePrize(...lines) {
  for (const line of lines) {
    const value = firstMoney(line);
    if (value) return value;
  }
  return 0;
}

function parseTournamentPlace(line) {
  if (!line) return null;
  const ordinal = line.match(/\b(\d+)(?:st|nd|rd|th)\b/i);
  if (ordinal) return toNumber(ordinal[1]);
  const ranked = line.match(/\b(?:place|rank|position|finish|finished)\s*:?\s*(\d+)(?:\s*\/\s*\d+)?\b/i);
  if (ranked) return toNumber(ranked[1]);
  const finished = line.match(/\bfinished(?:\s+the\s+tournament)?\s+(?:in\s+)?(\d+)\b/i);
  return finished ? toNumber(finished[1]) : null;
}

function parsePlayers(line) {
  if (!line) return null;
  const labeled = line.match(/\b(?:players?|entrants?|entries|field|total players)\s*:?\s*([\d,.]+)/i);
  const suffix = line.match(/^\s*([\d,.]+)\s+players?\b/i);
  return toNumber(labeled?.[1] || suffix?.[1]);
}

function cleanStartedAt(line) {
  return String(line || "")
    .replace(/^tournament started\s*:?\s*/i, "")
    .replace(/^(started|start time|date)\s*:?\s*/i, "")
    .trim();
}

function firstMoney(line) {
  return moneyValues(line)[0] || 0;
}

function moneyValues(line) {
  if (!line) return [];
  return [...String(line).matchAll(/[$€£]\s*([\d,.]+)/g)].map((match) => money(match[1])).filter((value) => value > 0);
}

function money(value) {
  if (!value) return 0;
  const raw = String(value).trim();
  const lastComma = raw.lastIndexOf(",");
  const lastDot = raw.lastIndexOf(".");
  const decimalSeparator = lastComma > lastDot ? "," : ".";
  let normalized = raw.replace(/[^\d,.]/g, "");
  if (decimalSeparator === ",") {
    normalized = normalized.replace(/\./g, "").replace(",", ".");
  } else {
    normalized = normalized.replace(/,/g, "");
  }
  const number = Number(normalized);
  return Number.isFinite(number) ? number : 0;
}

function detectTournamentFormat(name, buyIn = "") {
  if (isBountyTournamentText(name, buyIn)) return "PKO/KO";
  if (/satellite|ticket/i.test(name)) return "Sat";
  if (/hyper/i.test(name)) return "Hyper";
  if (/turbo/i.test(name)) return "Turbo";
  return "Vanilla";
}

function displayTournamentFormat(savedFormat, ...values) {
  if (values.some((value) => isBountyTournamentText(value))) return "PKO/KO";
  return savedFormat || detectTournamentFormat(...values);
}

async function importFiles(files) {
  const fileList = Array.from(files || []);
  if (!fileList.length) return;

  setImportStatus(`Importando <strong>${fileList.length}</strong> arquivo(s)...`);

  const totals = {
    files: 0,
    entries: 0,
    found: 0,
    parsed: 0,
    added: 0,
    replaced: 0,
    duplicates: 0,
    tournamentsFound: 0,
    tournamentsAdded: 0,
    tournamentDuplicates: 0,
    errors: [],
  };

  for (const file of fileList) {
    try {
      totals.files += 1;
      if (file.name.toLowerCase().endsWith(".zip")) {
        const entries = await readZipTextEntries(file);
        totals.entries += entries.length;
        for (const entry of entries) {
          const stats = await importPayloadText(entry.name, entry.text);
          addImportStats(totals, stats);
        }
      } else if (file.name.toLowerCase().endsWith(".rar")) {
        if (!window.desktopStore?.getFilePath || !window.desktopStore?.readRarEntries) {
          throw new Error("RAR disponivel apenas no aplicativo instalado.");
        }
        const archivePath = window.desktopStore.getFilePath(file);
        const entries = await window.desktopStore.readRarEntries(archivePath);
        totals.entries += entries.length;
        for (const entry of entries) {
          const stats = await importPayloadText(entry.name, entry.text);
          addImportStats(totals, stats);
        }
      } else {
        const text = await readDroppedTextFile(file);
        const stats = await importPayloadText(file.name, text);
        totals.entries += 1;
        addImportStats(totals, stats);
      }
    } catch (error) {
      totals.errors.push(`${file.name}: ${error.message || error}`);
    }
  }

  safeRender();
  if (els.fileInput) els.fileInput.value = "";
  showImportSummary(totals);
}

async function safeImportFiles(files) {
  try {
    await importFiles(files);
  } catch (error) {
    setImportStatus(`Erro na importacao: ${error.message || error}`, "error");
  } finally {
    if (els.fileInput) els.fileInput.value = "";
  }
}

function addImportStats(totals, stats) {
  totals.found += stats.found;
  totals.parsed += stats.parsed;
  totals.added += stats.added;
  totals.replaced += stats.replaced || 0;
  totals.duplicates += stats.duplicates;
  totals.tournamentsFound += stats.tournamentsFound;
  totals.tournamentsAdded += stats.tournamentsAdded;
  totals.tournamentDuplicates += stats.tournamentDuplicates;
  totals.hudRosterChanged = totals.hudRosterChanged || stats.hudRosterChanged === true;
}

function setImportStatus(html, kind = "") {
  if (!els.importStatus) return;
  els.importStatus.className = `import-status ${kind}`.trim();
  els.importStatus.innerHTML = html;
}

function showImportSummary(totals) {
  const base = [
    `<strong>${fmt(totals.added)}</strong> maos novas`,
    `<strong>${fmt(totals.replaced || 0)}</strong> atualizadas`,
    `<strong>${fmt(totals.tournamentsAdded)}</strong> torneios novos`,
    `${fmt(totals.duplicates)} duplicadas`,
    `${fmt(totals.tournamentDuplicates)} torneios duplicados`,
    `${fmt(totals.parsed)} parseadas`,
    `${fmt(totals.entries)} arquivo(s) interno(s)`,
  ].join(" | ");

  if (totals.errors.length) {
    setImportStatus(`${base}<br>Erro(s): ${totals.errors.slice(0, 3).join(" | ")}`, "error");
    return;
  }

  setImportStatus(base, "success");
}

function makeImportTotals() {
  return {
    files: 0,
    entries: 0,
    found: 0,
    parsed: 0,
    added: 0,
    replaced: 0,
    duplicates: 0,
    tournamentsFound: 0,
    tournamentsAdded: 0,
    tournamentDuplicates: 0,
    hudRosterChanged: false,
    errors: [],
  };
}

function updateAutoImportUi() {
  const folders = activeAutoImportFolders();
  if (els.toggleAutoImport) {
    els.toggleAutoImport.disabled = !folders.length || !window.desktopStore?.scanHandHistoryFolder;
    els.toggleAutoImport.textContent = state.autoImportEnabled ? "Desligar auto-import" : "Ligar auto-import";
  }
  if (els.autoImportStatus) {
    els.autoImportStatus.textContent = folders.length
      ? `${state.autoImportEnabled ? "Monitorando" : "Pastas prontas"}: ${fmt(folders.length)} pasta(s)`
      : "Nenhuma pasta monitorada.";
  }
  if (els.autoImportFolderList) {
    els.autoImportFolderList.innerHTML = folders.length
      ? folders.map((folder) => `<span title="${escapeAttr(folder)}">${escapeHtml(folder.replace(/^.*[\\/]/, ""))}</span>`).join("")
      : "";
  }
}

function activeAutoImportFolders() {
  const folders = Array.isArray(state.autoImportFolders) ? state.autoImportFolders : [];
  const combined = [...folders, state.autoImportFolder].filter(Boolean);
  return [...new Map(combined.map((folder) => [folder.toLowerCase(), folder])).values()];
}

function addAutoImportFolder(folder) {
  if (!folder) return false;
  const folders = activeAutoImportFolders();
  if (folders.some((item) => item.toLowerCase() === folder.toLowerCase())) {
    state.autoImportFolders = folders;
    state.autoImportFolder = folders[0] || "";
    return false;
  }
  state.autoImportFolders = [...folders, folder];
  state.autoImportFolder = state.autoImportFolders[0] || "";
  return true;
}

async function chooseAutoImportFolder() {
  if (!window.desktopStore?.chooseHandHistoryFolder) {
    setImportStatus("Escolher pasta so esta disponivel no app desktop.", "error");
    return;
  }
  const folder = await window.desktopStore.chooseHandHistoryFolder();
  if (!folder) return;
  stopAutoImport();
  addAutoImportFolder(folder);
  autoImportSeen.clear();
  updateAutoImportUi();
  await scanAutoImportFolder({ showWhenEmpty: true });
  await saveState({ silent: true });
}

async function detectAutoImportFolders() {
  if (!window.desktopStore?.detectHandHistoryFolders) {
    setImportStatus("Detectar pastas so esta disponivel no app desktop.", "error");
    return;
  }
  stopAutoImport();
  setImportStatus("Procurando pastas padrao das salas...", "success");
  let folders = [];
  try {
    folders = await window.desktopStore.detectHandHistoryFolders();
  } catch (error) {
    setImportStatus(`Erro ao detectar pastas: ${error.message || error}`, "error");
    return;
  }
  let added = 0;
  for (const folder of folders) {
    if (addAutoImportFolder(folder.path)) added += 1;
  }
  autoImportSeen.clear();
  updateAutoImportUi();
  await saveState({ silent: true });
  if (!folders.length) {
    setImportStatus("Nao encontrei pastas padrao de maos ou sumarios. Use Escolher pasta.", "error");
    return;
  }
  const sites = [...new Set(folders.map((folder) => folder.site).filter(Boolean))].join(", ");
  setImportStatus(`Detectei <strong>${fmt(folders.length)}</strong> pasta(s) (${escapeHtml(sites || "sites")}): <strong>${fmt(added)}</strong> nova(s) adicionada(s).`, "success");
  await scanAutoImportFolder({ showWhenEmpty: true });
}

function seenFilesObject() {
  return Object.fromEntries(autoImportSeen.entries());
}

async function scanAutoImportFolder(options = {}) {
  if (autoImportScanPromise) return autoImportScanPromise;
  autoImportScanPromise = runAutoImportScan(options);
  try {
    return await autoImportScanPromise;
  } finally {
    autoImportScanPromise = null;
  }
}

async function runAutoImportScan(options = {}) {
  const folders = activeAutoImportFolders();
  if (!folders.length || !window.desktopStore?.scanHandHistoryFolder) return makeImportTotals();
  const totals = makeImportTotals();
  try {
    for (const folder of folders) {
      const entries = await window.desktopStore.scanHandHistoryFolder(folder, seenFilesObject());
      totals.files += entries.length;
      totals.entries += entries.length;
      for (const entry of entries) {
        try {
          const stats = await importPayloadText(entry.name || entry.path || "", entry.text || "");
          addImportStats(totals, stats);
        } catch (error) {
          totals.errors.push(`${entry.name || entry.path || "arquivo"}: ${error.message || error}`);
        } finally {
          if (entry.path && entry.signature) autoImportSeen.set(entry.path, entry.signature);
        }
      }
      if (totals.entries >= 20) break;
    }
    if (totals.entries) {
      safeRender();
      showImportSummary(totals);
        if (totals.added || totals.tournamentsAdded) {
          scheduleAutoSave();
        }
        if (state.hudEnabled && (totals.added || totals.hudRosterChanged)) {
          await publishHudPayload();
          await window.desktopStore?.syncHudOverlays?.(true);
        }
    } else if (options.showWhenEmpty) {
      setImportStatus("Pasta monitorada sem arquivos novos no momento.", "success");
    }
  } catch (error) {
    totals.errors.push(error.message || String(error));
    setImportStatus(`Erro no auto-import: ${error.message || error}`, "error");
  }
  updateAutoImportUi();
  return totals;
}

function scheduleAutoSave(delay = 20000) {
  autoSavePending = true;
  if (autoSaveTimer) return;
  autoSaveTimer = window.setTimeout(async () => {
    autoSaveTimer = null;
    if (!autoSavePending) return;
    autoSavePending = false;
    await saveState({ silent: true });
  }, delay);
}

async function ensureHudAutoImport() {
  if (!state.hudEnabled || !window.desktopStore?.scanHandHistoryFolder) return;
  if (!activeAutoImportFolders().length) await detectAutoImportFolders();
  if (activeAutoImportFolders().length) startAutoImport();
}

function startAutoImport() {
  if (!activeAutoImportFolders().length || autoImportTimer) return;
  state.autoImportEnabled = true;
  updateAutoImportUi();
  scanAutoImportFolder();
  autoImportTimer = window.setInterval(() => scanAutoImportFolder(), 4000);
}

function stopAutoImport() {
  state.autoImportEnabled = false;
  if (autoImportTimer) {
    window.clearInterval(autoImportTimer);
    autoImportTimer = null;
  }
  if (autoSavePending) scheduleAutoSave(1000);
  updateAutoImportUi();
}

function toggleAutoImport() {
  if (state.autoImportEnabled) stopAutoImport();
  else startAutoImport();
}

async function saveState(options = {}) {
  if (document.body?.classList.contains("hud-overlay-window-mode")) return false;
  try {
    if (window.desktopStore?.save) {
      const payload = makeSavePayload(true);
      await writeSavedPayload(payload);
      if (!options.silent) {
        setImportStatus(`Salvo no app local: <strong>${fmt(state.hands.length)}</strong> maos e <strong>${fmt(state.tournaments.length)}</strong> torneios.`, "success");
      }
      return true;
    }
    if (!storageAvailable()) {
      setImportStatus("Salvar nao esta disponivel neste navegador.", "error");
      return false;
    }
    const fullPayload = makeSavePayload(true);
    try {
      await writeSavedPayload(fullPayload);
      if (!options.silent) {
        setImportStatus(`Salvo comprimido: <strong>${fmt(state.hands.length)}</strong> maos e <strong>${fmt(state.tournaments.length)}</strong> torneios.`, "success");
      }
      return true;
    } catch (fullError) {
      const compactPayload = makeSavePayload(false);
      await writeSavedPayload(compactPayload);
      if (!options.silent) {
        setImportStatus(`Salvo em modo leve: <strong>${fmt(state.hands.length)}</strong> maos e <strong>${fmt(state.tournaments.length)}</strong> torneios. Stats preservadas; para replayer completo, reimporte os arquivos de maos.`, "success");
      }
      return true;
    }
  } catch (error) {
    setImportStatus(`Nao foi possivel salvar. O armazenamento local pode estar cheio. ${error.message || error}`, "error");
    return false;
  }
}

function makeSavePayload(includeRaw) {
  const hands = includeRaw ? state.hands : state.hands.map((hand) => {
    const { raw, ...compactHand } = hand;
    return compactHand;
  });
  return {
    hands,
    tournaments: state.tournaments,
    selectedHero: state.selectedHero,
    graphTimeFilter: state.graphTimeFilter,
    graphDateFrom: state.graphDateFrom,
    graphDateTo: state.graphDateTo,
    statsTimeFilter: state.statsTimeFilter,
    statsDateFrom: state.statsDateFrom,
    statsDateTo: state.statsDateTo,
    autoImportFolder: activeAutoImportFolders()[0] || "",
    autoImportFolders: activeAutoImportFolders(),
    hudEnabled: state.hudEnabled,
    hudFloatingScale: state.hudFloatingScale,
    hudStats: state.hudStats,
    hudSecondRowEnabled: state.hudSecondRowEnabled,
    hudStatsSecondRow: state.hudStatsSecondRow,
    handNotes: state.handNotes,
    statsSchemaVersion: STATS_SCHEMA_VERSION,
    savedAt: new Date().toISOString(),
    compact: !includeRaw,
  };
}

async function writeSavedPayload(payload) {
  const serialized = JSON.stringify(payload);
  const packed = packSavedPayload(serialized, payload.compact);
  if (window.desktopStore?.save) {
    await window.desktopStore.save(JSON.stringify(packed));
    return;
  }
  localStorage.removeItem(STORAGE_KEY);
  localStorage.setItem(STORAGE_KEY, JSON.stringify(packed));
}

function packSavedPayload(serialized, compact) {
  if (typeof fflate !== "undefined" && typeof TextEncoder !== "undefined") {
    const compressed = fflate.deflateSync(new TextEncoder().encode(serialized), { level: 9 });
    return {
      version: 2,
      encoding: "deflate-base64",
      compact: !!compact,
      data: bytesToBase64(compressed),
    };
  }
  return {
    version: 1,
    compact: !!compact,
    data: JSON.parse(serialized),
  };
}

async function loadState() {
  try {
    const raw = window.desktopStore?.load
      ? await window.desktopStore.load()
      : storageAvailable()
        ? localStorage.getItem(STORAGE_KEY)
        : null;
    if (!raw) return;
    const payload = unpackSavedPayload(JSON.parse(raw));
    setBootStatus("Carregando maos salvas...");
    const savedHands = Array.isArray(payload.hands) ? payload.hands : [];
    state.hands = migrateSavedHands(savedHands);
    const savedHandsChanged = state.hands.length !== savedHands.length
      || state.hands.some((hand, index) => hand?.handId !== savedHands[index]?.handId || (Number(hand?.parserVersion) || 0) !== (Number(savedHands[index]?.parserVersion) || 0));
    state.tournaments = Array.isArray(payload.tournaments) ? payload.tournaments : [];
    state.handIds = new Set(state.hands.map((hand) => hand.handId).filter(Boolean));
    state.tournamentIds = new Set(state.tournaments.map((tournament) => tournament.tournamentId).filter(Boolean));
    state.heroes = new Set(state.hands.map((hand) => hand.hero).filter(Boolean));
    state.selectedHero = payload.selectedHero || "all";
    state.graphTimeFilter = payload.graphTimeFilter || "all";
    state.graphDateFrom = payload.graphDateFrom || "";
    state.graphDateTo = payload.graphDateTo || "";
    state.statsTimeFilter = payload.statsTimeFilter || "all";
    state.statsDateFrom = payload.statsDateFrom || "";
    state.statsDateTo = payload.statsDateTo || "";
    state.autoImportFolder = payload.autoImportFolder || "";
    state.autoImportFolders = Array.isArray(payload.autoImportFolders) ? payload.autoImportFolders.filter(Boolean) : [];
    if (state.autoImportFolder && !state.autoImportFolders.some((folder) => folder.toLowerCase() === state.autoImportFolder.toLowerCase())) {
      state.autoImportFolders.unshift(state.autoImportFolder);
    }
    state.autoImportEnabled = false;
    state.hudEnabled = payload.hudEnabled !== false;
    state.hudFloatingScale = Number(payload.hudFloatingScale) || 1;
    state.hudStats = sanitizeHudStats(payload.hudStats);
    state.hudSecondRowEnabled = payload.hudSecondRowEnabled === true;
    state.hudStatsSecondRow = sanitizeHudStats(payload.hudStatsSecondRow, true);
    state.handNotes = payload.handNotes && typeof payload.handNotes === "object" ? payload.handNotes : {};
    if ((Number(payload.statsSchemaVersion) || 0) < STATS_SCHEMA_VERSION) {
      scheduleBackgroundStatsMigration();
    }
    const requiresHandMigration = state.hands.some((hand) => needsBackgroundHandMigration(hand));
    if (requiresHandMigration) {
      scheduleBackgroundHandMigration();
    }
    const pendingEquities = requiresHandMigration ? [] : state.hands
      .map((hand, index) => ({ handId: hand?.handId, index }))
      .filter(({ handId, index }) => handId && state.hands[index]?.equityPending);
    if (pendingEquities.length) queueBackgroundEvCalculation(pendingEquities);
    replayCache.clear();
    statHandComboCache.clear();
    statDecisionCache.clear();
    statActionTimelineCache.clear();
    statBetSizeCache.clear();
    srpSpotCache.clear();
    playerHudCache.signature = "";
    playerHudCache.stats = new Map();
    currentReplayContext = null;
    const mode = payload.compact ? " modo leve" : "";
    const source = window.desktopStore?.load ? "app local" : `salvo${mode}`;
    setImportStatus(`Carregado do ${source}: <strong>${fmt(state.hands.length)}</strong> maos e <strong>${fmt(state.tournaments.length)}</strong> torneios.`, "success");
    if (savedHandsChanged) window.setTimeout(() => saveState({ silent: true }), 750);
  } catch (error) {
    if (window.desktopStore?.reset) await window.desktopStore.reset();
    else if (storageAvailable()) localStorage.removeItem(STORAGE_KEY);
    setImportStatus(`Salvo antigo estava corrompido e foi ignorado.`, "error");
  }
}

function unpackSavedPayload(payload) {
  if (payload?.encoding === "deflate-base64" && typeof fflate !== "undefined" && typeof TextDecoder !== "undefined") {
    const bytes = base64ToBytes(payload.data || "");
    const json = new TextDecoder().decode(fflate.inflateSync(bytes));
    return JSON.parse(json);
  }
  if (payload?.version === 1 && payload.data) return payload.data;
  return payload || {};
}

function bytesToBase64(bytes) {
  let binary = "";
  const chunkSize = 0x8000;
  for (let index = 0; index < bytes.length; index += chunkSize) {
    binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
  }
  return btoa(binary);
}

function base64ToBytes(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }
  return bytes;
}

function migrateSavedHands(hands) {
  let migrated = false;
  const nextHands = hands.map((hand) => {
    if (hand?.raw && needsSavedHandMigration(hand)) {
      const reparsed = parseHand(hand.raw);
      if (reparsed) {
        migrated = true;
        return reparsed;
      }
    }
    return hand;
  }).filter((hand) => {
    const keep = shouldStoreHand(hand);
    if (!keep) migrated = true;
    return keep;
  });
  if (migrated) {
    setImportStatus("Stats e torneios recalculados com o parser novo.", "success");
  }
  return nextHands;
}

function needsSavedHandMigration(hand) {
  return hand.threeBetOpportunity === undefined
    || hand.cbetFlopIPOpportunity === undefined
    || hand.showedDown === undefined
    || hand.riverBetOpportunity === undefined
    || hand.betVsMissedFlopOpportunity === undefined
    || hand.betVsMissedTurnOpportunity === undefined
    || hand.skipCbetFlopOpportunity === undefined
    || hand.donkBetFlopOpportunity === undefined
    || hand.raiseIpTurnOpportunity === undefined
    || hand.heroEndingChips === undefined
    || hand.netChips === undefined
    || hand.evChips === undefined;
}

function parseHandFastStats(raw) {
  return parseHand(raw, { skipEquity: true });
}

function scheduleBackgroundStatsMigration() {
  if (backgroundStatsMigrationRunning) return;
  const candidates = state.hands
    .map((hand, index) => ({ hand, index }))
    .filter(({ hand }) => hand?.raw);
  if (!candidates.length) return;
  backgroundStatsMigrationRunning = true;
  window.setTimeout(() => migrateStatsInBackground(candidates), 250);
}

function scheduleBackgroundHandMigration() {
  if (backgroundHandMigrationRunning) return;
  const candidates = state.hands
    .map((hand, index) => ({ hand, index }))
    .filter(({ hand }) => needsBackgroundHandMigration(hand));
  if (!candidates.length) return;
  backgroundHandMigrationRunning = true;
  window.setTimeout(() => migrateHandsInBackground(candidates), 500);
}

function needsBackgroundHandMigration(hand) {
  return Boolean(hand?.raw)
    && ((Number(hand.parserVersion) || 0) < PARSER_VERSION
      || (Number(hand.equityVersion) || 0) < EQUITY_VERSION);
}

function queueBackgroundEvCalculation(candidates) {
  for (const candidate of candidates || []) {
    if (!candidate?.handId || backgroundEvQueuedIds.has(candidate.handId)) continue;
    let index = candidate.index;
    if (state.hands[index]?.handId !== candidate.handId) {
      index = state.hands.findIndex((hand) => hand.handId === candidate.handId);
    }
    const hand = index >= 0 ? state.hands[index] : null;
    if (!hand?.raw || !hand.hero) continue;
    const lines = String(hand.raw).replace(/\r/g, "").split("\n");
    if (!canCalculateAllInEquity(lines, hand.hero, hand.heroCards)) {
      hand.allInEquityChips = null;
      hand.evChips = hand.netChips;
      hand.equityPending = false;
      hand.equityVersion = EQUITY_VERSION;
      continue;
    }
    backgroundEvQueuedIds.add(candidate.handId);
    backgroundEvQueue.push({ handId: candidate.handId, index });
  }
  if (backgroundEvCalculationRunning || !backgroundEvQueue.length) return;
  backgroundEvCalculationRunning = true;
  window.setTimeout(processBackgroundEvQueue, 700);
}

function updateStoredHandEquity(hand) {
  if (!hand?.raw || !hand.hero) return false;
  const lines = String(hand.raw).replace(/\r/g, "").split("\n");
  const allInEvChips = calculateAllInEvChips(lines, hand.hero, hand.heroCards, hand.investedChips, hand.handId);
  hand.allInEquityChips = allInEvChips;
  hand.evChips = Number.isFinite(allInEvChips) ? allInEvChips : hand.netChips;
  hand.equityPending = false;
  hand.parserVersion = PARSER_VERSION;
  hand.equityVersion = EQUITY_VERSION;
  return true;
}

function scheduleNextEvBatch(callback) {
  if (typeof window.requestIdleCallback === "function" && !document.hidden) {
    window.requestIdleCallback(callback, { timeout: 120 });
    return;
  }
  window.setTimeout(callback, document.hidden ? 0 : 8);
}

function processBackgroundEvQueue() {
  let completed = 0;
  const runBatch = () => {
    const batch = backgroundEvQueue.splice(0, document.hidden ? 3 : 1);
    for (const candidate of batch) {
      backgroundEvQueuedIds.delete(candidate.handId);
      let index = candidate.index;
      if (state.hands[index]?.handId !== candidate.handId) {
        index = state.hands.findIndex((hand) => hand.handId === candidate.handId);
      }
      if (index < 0 || !state.hands[index]?.raw) continue;
      if (updateStoredHandEquity(state.hands[index])) completed += 1;
    }
    if (backgroundEvQueue.length) {
      if (completed > 0 && completed % 40 === 0) {
        setImportStatus(`Calculando equidades em segundo plano: <strong>${fmt(completed)}</strong> prontas, <strong>${fmt(backgroundEvQueue.length)}</strong> restantes.`, "success");
      }
      scheduleNextEvBatch(runBatch);
      return;
    }
    backgroundEvCalculationRunning = false;
    replayCache.clear();
    statDecisionCache.clear();
    statActionTimelineCache.clear();
    statBetSizeCache.clear();
    srpSpotCache.clear();
    safeRender();
    setImportStatus(`Equidades atualizadas em segundo plano: <strong>${fmt(completed)}</strong> maos com all-in.`, "success");
    saveState({ silent: true });
  };
  runBatch();
}

function copyFastStatFields(target, source) {
  const fields = [
    "parserVersion",
    "vpip", "pfr", "allInPre", "openRaise", "rfiOpportunity", "ggleaksRfiOpportunity", "ggleaksRfi", "firstRaisePosition",
    "threeBetOpportunity", "threeBet", "facedThreeBet", "foldToThreeBet",
    "fourBetOpportunity", "fourBet", "fourBetNonAllIn", "squeezeOpportunity", "squeeze",
    "coldCallOpportunity", "coldCall", "coldCallBbOpportunity", "coldCallBb", "bbFacedSteal", "bbFoldVsSteal", "bbFoldVsCO", "bbFoldVsBTN", "bbFoldVsSB",
    "cbetFlopOpportunity", "cbetFlop", "cbetFlopIPOpportunity", "cbetFlopIP", "cbetFlopOOPOpportunity", "cbetFlopOOP",
    "cbetTurnOpportunity", "cbetTurn", "cbetRiverOpportunity", "cbetRiver",
    "facedFlopCbet", "facedTurnCbet", "facedRiverCbet", "foldToFlopCbet", "foldToTurnCbet", "foldToRiverCbet",
    "donkBetFlopOpportunity", "donkBetFlop", "donkBetTurnOpportunity", "donkBetTurn", "donkBetRiverOpportunity", "donkBetRiver",
    "betVsMissedFlopOpportunity", "betVsMissedFlop", "betVsMissedTurnOpportunity", "betVsMissedTurn", "betVsMissedRiverOpportunity", "betVsMissedRiver",
    "xrOopFlopOpportunity", "xrOopFlop", "xrOopTurnOpportunity", "xrOopTurn", "xrOopRiverOpportunity", "xrOopRiver",
    "xrPfaFlopOpportunity", "xrPfaFlop", "xrPfaTurnOpportunity", "xrPfaTurn", "xrPfaRiverOpportunity", "xrPfaRiver",
    "raiseIpFlopOpportunity", "raiseIpFlop", "raiseIpTurnOpportunity", "raiseIpTurn", "raiseIpRiverOpportunity", "raiseIpRiver",
    "foldVsBetFlopOpportunity", "foldVsBetFlop", "foldVsBetTurnOpportunity", "foldVsBetTurn", "foldVsBetRiverOpportunity", "foldVsBetRiver",
    "skipCbetFlopOpportunity", "skipCbetFoldFlopOpportunity", "skipCbetFoldFlop", "skipCbetRaiseFlop",
    "sawFlop", "sawTurn", "sawRiver", "showedDown", "wonShowdown",
    "srpSpot",
  ];
  for (const field of fields) target[field] = source[field];
}

function migrateStatsInBackground(candidates) {
  let cursor = 0;
  let changed = 0;
  const batchSize = 180;
  const runBatch = () => {
    const end = Math.min(candidates.length, cursor + batchSize);
    for (; cursor < end; cursor += 1) {
      const { hand, index } = candidates[cursor];
      const reparsed = parseHandFastStats(hand.raw);
      if (!reparsed) continue;
      copyFastStatFields(state.hands[index], reparsed);
      changed += 1;
    }
    if (cursor < candidates.length) {
      if (cursor % (batchSize * 8) === 0) {
        setImportStatus(`Atualizando stats em segundo plano: <strong>${fmt(cursor)}</strong> / <strong>${fmt(candidates.length)}</strong> maos.`, "success");
      }
      window.setTimeout(runBatch, 0);
      return;
    }
    backgroundStatsMigrationRunning = false;
    replayCache.clear();
    statDecisionCache.clear();
    statActionTimelineCache.clear();
    statBetSizeCache.clear();
    srpSpotCache.clear();
    playerHudCache.signature = "";
    playerHudCache.stats = new Map();
    safeRender();
    setImportStatus(`Stats atualizadas em segundo plano: <strong>${fmt(changed)}</strong> maos recalculadas.`, "success");
    saveState({ silent: true });
  };
  runBatch();
}

function migrateHandsInBackground(candidates) {
  let cursor = 0;
  let changed = 0;
  const equityCandidates = [];
  const batchSize = 120;
  const runBatch = () => {
    const end = Math.min(candidates.length, cursor + batchSize);
    for (; cursor < end; cursor += 1) {
      const { hand, index } = candidates[cursor];
      const reparsed = parseHand(hand.raw, { skipEquity: true });
      if (!reparsed) continue;
      const refreshAllInEquity = shouldRefreshAllInEquityForSite(reparsed.site)
        || (Number(hand.equityVersion) || 0) < EQUITY_VERSION;
      if (!refreshAllInEquity && hand.equityPending === false && Number.isFinite(hand.allInEquityChips)) {
        reparsed.allInEquityChips = hand.allInEquityChips;
        reparsed.evChips = hand.evChips;
        reparsed.equityPending = false;
        reparsed.equityVersion = hand.equityVersion || EQUITY_VERSION;
      } else if (!reparsed.equityPending) {
        reparsed.allInEquityChips = null;
        reparsed.evChips = reparsed.netChips;
        reparsed.equityVersion = EQUITY_VERSION;
      }
      state.hands[index] = reparsed;
      if (reparsed.equityPending) equityCandidates.push({ handId: reparsed.handId, index });
      changed += 1;
    }
    if (cursor < candidates.length) {
      if (cursor % (batchSize * 20) === 0) {
        setImportStatus(`Recalculando EVs em segundo plano: <strong>${fmt(cursor)}</strong> / <strong>${fmt(candidates.length)}</strong> maos.`, "success");
        replayCache.clear();
        statDecisionCache.clear();
        statActionTimelineCache.clear();
        statBetSizeCache.clear();
        srpSpotCache.clear();
        playerHudCache.signature = "";
        playerHudCache.stats = new Map();
        safeRender();
      }
      window.setTimeout(runBatch, 0);
      return;
    }
    backgroundHandMigrationRunning = false;
    state.handIds = new Set(state.hands.map((hand) => hand.handId).filter(Boolean));
    state.heroes = new Set(state.hands.map((hand) => hand.hero).filter(Boolean));
    replayCache.clear();
    statHandComboCache.clear();
    statDecisionCache.clear();
    statActionTimelineCache.clear();
    statBetSizeCache.clear();
    srpSpotCache.clear();
    playerHudCache.signature = "";
    playerHudCache.stats = new Map();
    safeRender();
    setImportStatus(`Leitura atualizada: <strong>${fmt(changed)}</strong> maos. Equidades seguem em segundo plano sem bloquear o app.`, "success");
    saveState({ silent: true });
    queueBackgroundEvCalculation(equityCandidates);
  };
  runBatch();
}

async function resetState() {
  stopAutoImport();
  stopHudAutoSync();
  backgroundEvQueue.length = 0;
  backgroundEvQueuedIds.clear();
  backgroundEvCalculationRunning = false;
  if (window.desktopStore?.syncHudOverlays) await window.desktopStore.syncHudOverlays(false);
  state.hands = [];
  state.tournaments = [];
  state.heroes = new Set();
  state.handIds = new Set();
  state.tournamentIds = new Set();
  liveHudTableSnapshots.clear();
  state.selectedHero = "all";
  state.graphTimeFilter = "all";
  state.graphDateFrom = "";
  state.graphDateTo = "";
  state.statsTimeFilter = "all";
  state.statsDateFrom = "";
  state.statsDateTo = "";
  state.statsSection = "overview";
  state.srpScenario = "defender-oop";
  state.srpStreet = "flop";
  srpSelectedPath = "all";
  state.threeBetPotScenario = "defender-oop";
  state.threeBetPotStreet = "flop";
  threeBetPotSelectedPath = "all";
  state.blindWarSide = "bb";
  state.autoImportFolder = "";
  state.autoImportFolders = [];
  state.hudFloatingScale = 1;
  autoImportSeen.clear();
  sessionHandIds.clear();
  replayCache.clear();
  statHandComboCache.clear();
  statDecisionCache.clear();
  statActionTimelineCache.clear();
  statBetSizeCache.clear();
  srpSpotCache.clear();
  threeBetPotSpotCache.clear();
  blindWarSpotCache.clear();
  playerHudCache.signature = "";
  playerHudCache.stats = new Map();
  currentReplayContext = null;
  if (window.desktopStore?.reset) await window.desktopStore.reset();
  else if (storageAvailable()) localStorage.removeItem(STORAGE_KEY);
  safeRender();
  setImportStatus("Dados resetados. Nada salvo no armazenamento local.", "success");
}

function storageAvailable() {
  try {
    return typeof localStorage !== "undefined";
  } catch (error) {
    return false;
  }
}

function switchTab(tabName) {
  state.activeTab = tabName;
  const showDashboard = tabName === "dashboard";
  const showGraph = tabName === "graph";
  const showReplayer = tabName === "replayer";
  const showHud = tabName === "hud";
  const showStats = tabName === "stats";
  els.dashboardView?.classList.toggle("is-active", showDashboard);
  els.graphView?.classList.toggle("is-active", showGraph);
  els.replayerView?.classList.toggle("is-active", showReplayer);
  els.hudView?.classList.toggle("is-active", showHud);
  els.statsView?.classList.toggle("is-active", showStats);
  els.dashboardTab?.classList.toggle("is-active", showDashboard);
  els.graphTab?.classList.toggle("is-active", showGraph);
  els.replayerTab?.classList.toggle("is-active", showReplayer);
  els.hudTab?.classList.toggle("is-active", showHud);
  els.statsTab?.classList.toggle("is-active", showStats);
  els.sidebarFilters?.classList.toggle("is-hidden", showDashboard);
  safeRender();
}

function switchStatsSection(sectionName) {
  state.statsSection = ["overview", "srp", "three-bet-pot", "blind-war-post", "positions"].includes(sectionName) ? sectionName : "overview";
  document.querySelectorAll(".stats-nav-button").forEach((button) => {
    button.classList.toggle("is-active", button.dataset.statsSection === state.statsSection);
  });
  const sectionMap = {
    overview: els.statsView?.querySelector("#statsOverviewSection"),
    srp: els.statsView?.querySelector("#statsSrpSection"),
    "three-bet-pot": els.statsView?.querySelector("#statsThreeBetPotSection"),
    "blind-war-post": els.statsView?.querySelector("#statsBlindWarPostSection"),
    positions: els.statsView?.querySelector("#statsPositionsSection"),
  };
  Object.entries(sectionMap).forEach(([name, section]) => {
    section?.classList.toggle("is-active", name === state.statsSection);
  });
  safeRender();
}

function switchStatsSectionVisuals() {
  const active = state.statsSection || "overview";
  document.querySelectorAll(".stats-nav-button").forEach((button) => {
    button.classList.toggle("is-active", button.dataset.statsSection === active);
  });
  const sectionMap = {
    overview: els.statsView?.querySelector("#statsOverviewSection"),
    srp: els.statsView?.querySelector("#statsSrpSection"),
    "three-bet-pot": els.statsView?.querySelector("#statsThreeBetPotSection"),
    "blind-war-post": els.statsView?.querySelector("#statsBlindWarPostSection"),
    positions: els.statsView?.querySelector("#statsPositionsSection"),
  };
  Object.entries(sectionMap).forEach(([name, section]) => section?.classList.toggle("is-active", name === active));
}

function applyWindowMode() {
  const search = window.location?.search || "";
  const isReplayWindow = /(?:^\?|&)mode=replayer(?:&|$)/.test(search);
  const isHudOverlay = /(?:^\?|&)mode=hud-overlay(?:&|$)/.test(search);
  document.body?.classList?.toggle("replayer-window-mode", isReplayWindow);
  document.body?.classList?.toggle("hud-overlay-window-mode", isHudOverlay);
  if (els.hudFloatingOverlay) els.hudFloatingOverlay.hidden = !isHudOverlay;
  if (isReplayWindow) switchTab("replayer");
  if (isHudOverlay) renderHudFloatingOverlay();
}

function isHandHistoryTextName(name) {
  const lower = String(name || "").toLowerCase();
  return HAND_HISTORY_TEXT_EXTENSIONS.some((extension) => lower.endsWith(extension));
}

function decodeHandHistoryBytes(bytes) {
  const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
  try {
    return new TextDecoder("utf-8", { fatal: true }).decode(data);
  } catch {}
  try {
    return new TextDecoder("windows-1252").decode(data);
  } catch {}
  return new TextDecoder("utf-8").decode(data);
}

async function readDroppedTextFile(file) {
  return decodeHandHistoryBytes(await file.arrayBuffer());
}

async function readZipTextEntries(file) {
  const buffer = await file.arrayBuffer();
  if (window.fflate?.unzipSync) {
    const unzipped = window.fflate.unzipSync(new Uint8Array(buffer));
    return Object.entries(unzipped)
      .filter(([name]) => isHandHistoryTextName(name))
      .map(([name, bytes]) => ({ name, text: decodeHandHistoryBytes(bytes) }));
  }

  const bytes = new Uint8Array(buffer);
  const view = new DataView(buffer);
  const entries = [];
  const eocdOffset = findEndOfCentralDirectory(bytes);
  if (eocdOffset < 0) throw new Error("ZIP invalido ou nao suportado.");

  const totalEntries = view.getUint16(eocdOffset + 10, true);
  const centralOffset = view.getUint32(eocdOffset + 16, true);
  let offset = centralOffset;

  for (let i = 0; i < totalEntries; i += 1) {
    if (view.getUint32(offset, true) !== 0x02014b50) break;
    const method = view.getUint16(offset + 10, true);
    const compressedSize = view.getUint32(offset + 20, true);
    const fileNameLength = view.getUint16(offset + 28, true);
    const extraLength = view.getUint16(offset + 30, true);
    const commentLength = view.getUint16(offset + 32, true);
    const localHeaderOffset = view.getUint32(offset + 42, true);
    const nameStart = offset + 46;
    const name = decodeHandHistoryBytes(bytes.slice(nameStart, nameStart + fileNameLength));

    if (isHandHistoryTextName(name)) {
      const localNameLength = view.getUint16(localHeaderOffset + 26, true);
      const localExtraLength = view.getUint16(localHeaderOffset + 28, true);
      const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
      const compressed = bytes.slice(dataStart, dataStart + compressedSize);
      const inflated = await unzipPayload(compressed, method);
      entries.push({ name, text: decodeHandHistoryBytes(inflated) });
    }

    offset = nameStart + fileNameLength + extraLength + commentLength;
  }

  return entries;
}

function findEndOfCentralDirectory(bytes) {
  for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 66000); i -= 1) {
    if (bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06) {
      return i;
    }
  }
  return -1;
}

async function unzipPayload(compressed, method) {
  if (method === 0) return compressed;
  if (method !== 8) throw new Error(`Metodo ZIP nao suportado: ${method}`);
  if (!("DecompressionStream" in window)) {
    throw new Error("Este navegador nao suporta descompactacao ZIP local.");
  }
  const stream = new Blob([compressed]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
  return new Uint8Array(await new Response(stream).arrayBuffer());
}

function filteredHands() {
  const hero = state.selectedHero;
  const useSidebarFilters = state.activeTab !== "dashboard";
  const pos = useSidebarFilters ? els.positionFilter?.value || "all" : "all";
  const stack = useSidebarFilters ? els.stackFilter?.value || "all" : "all";
  const blindLevel = useSidebarFilters ? els.blindLevelFilter?.value || "all" : "all";
  const search = useSidebarFilters ? (els.searchInput?.value || "").trim().toLowerCase() : "";

  return state.hands.filter((hand) => {
    if (hero !== "all" && hand.hero !== hero) return false;
    if (pos !== "all" && hand.position !== pos) return false;
    if (stack !== "all") {
      const [min, max] = stack.split("-").map(Number);
      if (!(validHeroStackBb(hand) >= min && validHeroStackBb(hand) < max)) return false;
    }
    if (blindLevel !== "all") {
      const bigBlind = Number(hand.bigBlind);
      if (!Number.isFinite(bigBlind) || bigBlind <= 0) return false;
      if (blindLevel === "early" && !(bigBlind < 400)) return false;
      if (blindLevel === "mid" && !(bigBlind >= 500 && bigBlind <= 2000)) return false;
      if (blindLevel === "late" && !(bigBlind >= 10000)) return false;
    }
    if (search) {
      const haystack = `${hand.handId} ${hand.tournamentId} ${hand.hero} ${hand.heroCards} ${hand.preflopSummary}`.toLowerCase();
      if (!haystack.includes(search)) return false;
    }
    return true;
  });
}

function statsFilteredHands(hands) {
  return timeFilteredHands(hands, state.statsTimeFilter, state.statsDateFrom, state.statsDateTo);
}

function graphFilteredHands(hands) {
  return timeFilteredHands(hands, state.graphTimeFilter, state.graphDateFrom, state.graphDateTo);
}

function timeFilteredHands(hands, mode = "all", from = "", to = "") {
  if (mode === "session") {
    const today = todayDateKey();
    return hands.filter((hand) => handDateKey(hand.date) === today);
  }
  if (mode !== "custom") return hands;
  return hands.filter((hand) => {
    const key = handDateKey(hand.date);
    if (!key) return false;
    if (from && key < from) return false;
    if (to && key > to) return false;
    return true;
  });
}

function updateStatsTimeUi() {
  if (els.statsTimeFilter) els.statsTimeFilter.value = state.statsTimeFilter || "all";
  if (els.statsDateFrom) els.statsDateFrom.value = state.statsDateFrom || "";
  if (els.statsDateTo) els.statsDateTo.value = state.statsDateTo || "";
  if (els.statsDateRange) els.statsDateRange.classList.toggle("is-active", state.statsTimeFilter === "custom");
}

function updateGraphTimeUi() {
  if (els.graphTimeFilter) els.graphTimeFilter.value = state.graphTimeFilter || "all";
  if (els.graphDateFrom) els.graphDateFrom.value = state.graphDateFrom || "";
  if (els.graphDateTo) els.graphDateTo.value = state.graphDateTo || "";
  if (els.graphDateRange) els.graphDateRange.classList.toggle("is-active", state.graphTimeFilter === "custom");
}

function render() {
  renderHeroOptions();
  const hands = filteredHands();
  if (state.activeTab === "dashboard") {
    renderMetrics(hands);
    renderStackBars(hands);
    renderPositionTable(hands);
    renderTournamentTable();
    renderHandsTable(hands);
  }
  if (state.activeTab === "stats") {
    const statsHands = statsFilteredHands(hands);
    updateStatsTimeUi();
    switchStatsSectionVisuals();
    if (state.statsSection === "srp") renderSrpStats(statsHands);
    else if (state.statsSection === "three-bet-pot") renderThreeBetPotStats(statsHands);
    else if (state.statsSection === "blind-war-post") renderBlindWarStats(statsHands);
    else if (state.statsSection === "positions") renderPositionReport(statsHands);
    else {
      renderStats(statsHands);
      renderDetailedStats(statsHands);
    }
  }
  if (state.activeTab === "graph") {
    updateGraphTimeUi();
    renderResultsGraph(graphFilteredHands(hands));
  }
  if (state.activeTab === "replayer") renderReplayer(hands);
  if (state.activeTab === "hud") renderHudPage();
  if (document.body?.classList.contains("hud-overlay-window-mode")) renderHudFloatingOverlay();
}

function safeRender() {
  try {
    render();
  } catch (error) {
    setImportStatus(`Erro ao atualizar a tela: ${error.message || error}`, "error");
  }
}

function nextFrame() {
  return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
}

function renderHeroOptions() {
  if (!els.heroSelect) return;
  const heroes = [...state.heroes].sort();
  els.heroSelect.innerHTML = [
    `<option value="all">Todos os heroes</option>`,
    ...heroes.map((hero) => `<option value="${hero}">${hero}</option>`),
  ].join("");
  els.heroSelect.value = state.selectedHero;
}

function renderMetrics(hands) {
  if (!els.metricsGrid) return;
  const tournaments = new Set(hands.filter((hand) => !hand.isCashGame).map((hand) => hand.tournamentId)).size;
  const cashSessions = new Set(hands.filter((hand) => hand.isCashGame).map((hand) => hand.tournamentId)).size;
  const resultCount = state.tournaments.length;
  const totalCost = state.tournaments.reduce((sum, tournament) => sum + tournament.totalCost, 0);
  const totalPrize = state.tournaments.reduce((sum, tournament) => sum + tournament.prize, 0);
  const profit = totalPrize - totalCost;
  const roi = totalCost ? Math.round((profit / totalCost) * 100) : 0;
  const vpip = hands.filter((hand) => hand.vpip).length;
  const pfr = hands.filter((hand) => hand.pfr).length;
  const threeBet = hands.filter((hand) => hand.threeBet).length;
  const threeBetOpp = hands.filter((hand) => hand.threeBetOpportunity).length;
  const playedDays = new Set(hands.map((hand) => handDateKey(hand.date)).filter(Boolean)).size;
  const avgHandsPerDay = playedDays ? hands.length / playedDays : 0;
  const heroStackHands = hands.filter((hand) => validHeroStackBb(hand) !== null);
  const avgStack = heroStackHands.length
    ? heroStackHands.reduce((sum, hand) => sum + validHeroStackBb(hand), 0) / heroStackHands.length
    : null;
  const metrics = [
    ["Maos", fmt(hands.length)],
    ["Torneios", fmt(tournaments)],
    ["Cash", fmt(cashSessions)],
    ["Resumos", fmt(resultCount)],
    ["ROI", `${roi}%`],
    ["Lucro", `$${profit.toFixed(2)}`],
    ["VPIP", pct(vpip, hands.length)],
    ["PFR", pct(pfr, hands.length)],
    ["3bet total", ratio(threeBet, threeBetOpp)],
    ["Maos/dia", playedDays ? fmt(Math.round(avgHandsPerDay)) : "-"],
    ["Stack medio", avgStack === null ? "-" : `${avgStack.toFixed(1)} bb`],
  ];
  els.metricsGrid.innerHTML = metrics
    .map(([label, value]) => `<div class="metric"><span>${label}</span><strong>${value}</strong></div>`)
    .join("");
}

function validHeroStackBb(hand) {
  const value = Number(hand?.stackBb);
  return Number.isFinite(value) && value > 0 ? value : null;
}

function ratio(numerator, denominator) {
  if (!denominator) return "-";
  return `${Math.round((numerator / denominator) * 100)}%`;
}

function wonHand(hand) {
  return Number(hand?.collectedChips) > 0 || Number(hand?.result) > 0;
}

function renderStats(hands) {
  if (!els.statsGrid) return;
  if (els.statsCaption) els.statsCaption.textContent = `${fmt(hands.length)} maos filtradas`;

  const vpip = hands.filter((hand) => hand.vpip).length;
  const pfr = hands.filter((hand) => hand.pfr).length;
  const threeBet = hands.filter((hand) => hand.threeBet).length;
  const threeBetOpp = hands.filter((hand) => hand.threeBetOpportunity).length;
  const facedThreeBet = hands.filter((hand) => hand.facedThreeBet).length;
  const foldThreeBet = hands.filter((hand) => hand.foldToThreeBet).length;
  const fourBet = hands.filter((hand) => hand.fourBet).length;
  const fourBetOpp = hands.filter((hand) => hand.fourBetOpportunity).length;
  const sawFlop = hands.filter((hand) => hand.sawFlop).length;
  const sawRiver = hands.filter((hand) => hand.sawRiver).length;
  const wonWhenSawFlop = hands.filter((hand) => hand.sawFlop && wonHand(hand)).length;
  const cbetFlopOpp = hands.filter((hand) => hand.cbetFlopOpportunity).length;
  const cbetTurnOpp = hands.filter((hand) => hand.cbetTurnOpportunity).length;
  const cbetRiverOpp = hands.filter((hand) => hand.cbetRiverOpportunity).length;
  const facedFlopCbet = hands.filter((hand) => hand.facedFlopCbet).length;
  const facedTurnCbet = hands.filter((hand) => hand.facedTurnCbet).length;
  const facedRiverCbet = hands.filter((hand) => hand.facedRiverCbet).length;
  const showedDown = hands.filter((hand) => hand.showedDown).length;
  const wonShowdown = hands.filter((hand) => hand.wonShowdown).length;
  const allInPre = hands.filter((hand) => hand.allInPre).length;
  const openRaise = hands.filter((hand) => hand.openRaise).length;
  const callVsAllIn = hands.filter((hand) => hand.callVsAllIn).length;

  const stats = [
    ["VPIP", ratio(vpip, hands.length), `${fmt(vpip)} / ${fmt(hands.length)}`],
    ["PFR", ratio(pfr, hands.length), `${fmt(pfr)} / ${fmt(hands.length)}`],
    ["3Bet", ratio(threeBet, threeBetOpp), `${fmt(threeBet)} / ${fmt(threeBetOpp)}`],
    ["Fold 3Bet", ratio(foldThreeBet, facedThreeBet), `${fmt(foldThreeBet)} / ${fmt(facedThreeBet)}`],
    ["4Bet", ratio(fourBet, fourBetOpp), `${fmt(fourBet)} / ${fmt(fourBetOpp)}`],
    ["CBet flop", ratio(hands.filter((hand) => hand.cbetFlop).length, cbetFlopOpp), `${fmt(cbetFlopOpp)} oportunidades`],
    ["CBet turn", ratio(hands.filter((hand) => hand.cbetTurn).length, cbetTurnOpp), `${fmt(cbetTurnOpp)} oportunidades`],
    ["CBet river", ratio(hands.filter((hand) => hand.cbetRiver).length, cbetRiverOpp), `${fmt(cbetRiverOpp)} oportunidades`],
    ["Fold CBet flop", ratio(hands.filter((hand) => hand.foldToFlopCbet).length, facedFlopCbet), `${fmt(facedFlopCbet)} oportunidades`],
    ["Fold CBet turn", ratio(hands.filter((hand) => hand.foldToTurnCbet).length, facedTurnCbet), `${fmt(facedTurnCbet)} oportunidades`],
    ["Fold CBet river", ratio(hands.filter((hand) => hand.foldToRiverCbet).length, facedRiverCbet), `${fmt(facedRiverCbet)} oportunidades`],
    ["RFI", ratio(openRaise, hands.length), "open raise"],
    ["All-in PF", ratio(allInPre, hands.length), "preflop"],
    ["Call vs all-in", ratio(callVsAllIn, hands.length), "preflop"],
    ["WWSF", ratio(wonWhenSawFlop, sawFlop), "won / saw flop"],
    ["WTSD", ratio(showedDown, sawFlop), "showdown / flop"],
    ["W$SD", ratio(wonShowdown, showedDown), "won at showdown"],
  ];

  els.statsGrid.innerHTML = stats
    .map(([label, value, detail]) => `<div class="stat-card"><span>${label}</span><strong>${value}</strong><small>${detail}</small></div>`)
    .join("");
}

function percentValue(numerator, denominator) {
  if (!denominator) return null;
  return Math.round((numerator / denominator) * 100);
}

function statColor(value, rangeText) {
  if (value === null || value === undefined || value === "-") return "";
  const match = String(rangeText).match(/([\d.]+)-([\d.]+)/);
  if (!match) return "good";
  const low = Number(match[1]);
  const high = Number(match[2]);
  if (value < low) return "warn";
  if (value > high) return "bad";
  return "good";
}

function statRow(label, value, range = "", n = "", popupKey = "") {
  const shown = value === null || value === undefined ? "-" : value;
  const color = statColor(shown, range);
  const reference = range ? `Ref ${range}` : "-";
  const totalHands = Number.isFinite(Number(n)) && Number(n) > 0 ? `${fmt(Number(n))} maos` : "-";
  const popupEntry = popupKey ? statPopupRegistry.get(popupKey) : null;
  if (popupEntry) {
    popupEntry.value = Number.isFinite(Number(value)) ? Number(value) : null;
    popupEntry.opportunities = Number.isFinite(Number(n)) ? Number(n) : popupEntry.hands.length;
    popupEntry.reference = range;
  }
  const popupAttrs = popupKey ? ` data-stat-popup="${escapeAttr(popupKey)}" role="button" tabindex="0"` : "";
  return `<div class="hud-row ${popupKey ? "is-clickable" : ""}"${popupAttrs}>
    <span>${label}</span>
    <span class="hud-value ${color}">${shown}</span>
    <span class="hud-reference">${reference}</span>
    <span class="hud-n">${totalHands}</span>
  </div>`;
}

function registerStatPopup(key, title, hands, details = {}) {
  statPopupRegistry.set(key, { title, hands: [...hands], ...details });
  return key;
}

function streetStatCell(key, title, hands, metric, opportunity) {
  const opportunityHands = hands.filter(opportunity);
  const actionHands = opportunityHands.filter(metric);
  const value = percentValue(actionHands.length, opportunityHands.length);
  registerStatPopup(key, title, actionHands, {
    value,
    opportunities: opportunityHands.length,
    opportunityHands,
  });
  const shown = value ?? "-";
  return `<button class="hud-matrix-stat" data-stat-popup="${escapeAttr(key)}" type="button">${shown}</button>`;
}

function positionStatRows(hands, positions, metric, rangeByPosition = {}, opportunity = () => true, popupPrefix = "", popupDetails = {}) {
  return positions
    .map((position) => {
      const rowHands = hands.filter((hand) => statPosition(hand) === position && opportunity(hand));
      const value = percentValue(rowHands.filter(metric).length, rowHands.length);
      const popupHands = rowHands.filter(metric);
      const popupKey = popupPrefix ? registerStatPopup(`${popupPrefix}-${position}`, `${popupPrefix} ${position}`, popupHands, {
        ...popupDetails,
        opportunityHands: rowHands,
      }) : "";
      return statRow(position, value, rangeByPosition[position] || "", rowHands.length || "", popupKey);
    })
    .join("");
}

function statPosition(hand) {
  if (["UTG", "UTG+1"].includes(hand.position)) return "EP";
  if (["MP", "LJ"].includes(hand.position)) return "MP";
  return hand.position;
}

function reportPosition(hand) {
  if (hand.position === "UTG+1") return "UTG1";
  return hand.position;
}

function renderDetailedStats(hands) {
  if (!els.preflopStats) return;
  statPopupRegistry.clear();

  const count = (predicate) => hands.filter(predicate).length;
  const byPos = (position) => hands.filter((hand) => statPosition(hand) === position);
  const bbHands = byPos("BB");
  const bbVsCO = bbHands.filter((hand) => hand.bbFacedSteal && hand.firstRaisePosition === "CO");
  const bbVsBTN = bbHands.filter((hand) => hand.bbFacedSteal && hand.firstRaisePosition === "BTN");
  const bbVsSB = bbHands.filter((hand) => hand.bbFacedSteal && hand.firstRaisePosition === "SB");
  const threeBetOpportunityHands = hands.filter((hand) => hand.threeBetOpportunity);
  const facedThreeBetHands = hands.filter((hand) => hand.facedThreeBet);
  const squeezeOpportunityHands = hands.filter((hand) => hand.squeezeOpportunity);
  const bbStealOpportunityHands = bbHands.filter((hand) => hand.bbFacedSteal);
  const cbetFlopOpp = count((hand) => hand.cbetFlopOpportunity);
  const cbetTurnOpp = count((hand) => hand.cbetTurnOpportunity);
  const cbetRiverOpp = count((hand) => hand.cbetRiverOpportunity);
  const facedFlopCbet = count((hand) => hand.facedFlopCbet);
  const facedTurnCbet = count((hand) => hand.facedTurnCbet);
  const facedRiverCbet = count((hand) => hand.facedRiverCbet);
  const threeBetOpp = threeBetOpportunityHands.length;
  const fourBetOpp = facedThreeBetHands.length;
  const fourBetRangeHands = hands.filter((hand) => hand.fourBetNonAllIn === true || (hand.fourBetNonAllIn === undefined && hand.fourBet && !hand.allInPre));
  const squeezeOpp = squeezeOpportunityHands.length;
  const cbetFlopIPOpp = count((hand) => hand.cbetFlopIPOpportunity);
  const cbetFlopOOPOpp = count((hand) => hand.cbetFlopOOPOpportunity);
  const betVsMissedFlopOpp = count((hand) => hand.betVsMissedFlopOpportunity);
  const xrOopFlopOpp = count((hand) => hand.xrOopFlopOpportunity);
  const skipCbetOpp = count((hand) => hand.skipCbetFlopOpportunity);
  const riverBetOpp = count((hand) => hand.riverBetOpportunity);
  const showedDown = count((hand) => hand.showedDown);
  const donkFlopOpp = count((hand) => hand.donkBetFlopOpportunity);
  const donkTurnOpp = count((hand) => hand.donkBetTurnOpportunity);
  const donkRiverOpp = count((hand) => hand.donkBetRiverOpportunity);
  const foldVsBetFlopOpp = count((hand) => hand.foldVsBetFlopOpportunity);
  const foldVsBetTurnOpp = count((hand) => hand.foldVsBetTurnOpportunity);
  const foldVsBetRiverOpp = count((hand) => hand.foldVsBetRiverOpportunity);
  const xrOopTurnOpp = count((hand) => hand.xrOopTurnOpportunity);
  const xrOopRiverOpp = count((hand) => hand.xrOopRiverOpportunity);
  const xrPfaFlopOpp = count((hand) => hand.xrPfaFlopOpportunity);
  const xrPfaTurnOpp = count((hand) => hand.xrPfaTurnOpportunity);
  const xrPfaRiverOpp = count((hand) => hand.xrPfaRiverOpportunity);
  const raiseIpFlopOpp = count((hand) => hand.raiseIpFlopOpportunity);
  const raiseIpTurnOpp = count((hand) => hand.raiseIpTurnOpportunity);
  const raiseIpRiverOpp = count((hand) => hand.raiseIpRiverOpportunity);
  const betVsMissedTurnOpp = count((hand) => hand.betVsMissedTurnOpportunity);
  const betVsMissedRiverOpp = count((hand) => hand.betVsMissedRiverOpportunity);
  const sawFlop = count((hand) => hand.sawFlop);
  const wonWhenSawFlop = count((hand) => hand.sawFlop && wonHand(hand));
  const sawRiver = count((hand) => hand.sawRiver);

  els.preflopStats.innerHTML = [
    statRow("VPIP", percentValue(count((hand) => hand.vpip), hands.length), "20-26", hands.length, registerStatPopup("stat-vpip", "VPIP", hands.filter((hand) => hand.vpip))),
    statRow("PFR", percentValue(count((hand) => hand.pfr), hands.length), "16-21", hands.length, registerStatPopup("stat-pfr", "PFR", hands.filter((hand) => hand.pfr), { opportunityHands: hands, decisionSpot: "first", defaultAction: "raise" })),
    statRow("3bet TOT", percentValue(count((hand) => hand.threeBet), threeBetOpp), "7-10", threeBetOpp, registerStatPopup("stat-3bet", "3bet TOT", hands.filter((hand) => hand.threeBet), { opportunityHands: threeBetOpportunityHands, decisionSpot: "after-open", defaultAction: "raise" })),
    statRow("Fold vs 3bet", percentValue(count((hand) => hand.foldToThreeBet), facedThreeBetHands.length), "50-58", facedThreeBetHands.length, registerStatPopup("stat-fold3bet", "Fold vs 3bet", hands.filter((hand) => hand.foldToThreeBet), { opportunityHands: facedThreeBetHands, decisionSpot: "after-threebet", defaultAction: "fold" })),
    statRow("4bet range", percentValue(fourBetRangeHands.length, fourBetOpp), "3-6", fourBetOpp, registerStatPopup("stat-4bet", "4bet range", fourBetRangeHands, { opportunityHands: facedThreeBetHands, decisionSpot: "after-threebet", defaultAction: "raise" })),
    statRow("Squeeze TOT", percentValue(count((hand) => hand.squeeze), squeezeOpp), "7.5-11", squeezeOpp, registerStatPopup("stat-squeeze", "Squeeze TOT", hands.filter((hand) => hand.squeeze), { opportunityHands: squeezeOpportunityHands, decisionSpot: "first", defaultAction: "raise" })),
    statRow("BB fold vs CO", percentValue(bbVsCO.filter((hand) => hand.bbFoldVsCO).length, bbVsCO.length), "33-39", bbVsCO.length, registerStatPopup("stat-bb-fold-co", "BB fold vs CO", bbVsCO.filter((hand) => hand.bbFoldVsCO), { opportunityHands: bbVsCO, decisionSpot: "after-open", defaultAction: "fold", betSizeMode: "open" })),
    statRow("BB fold vs BTN", percentValue(bbVsBTN.filter((hand) => hand.bbFoldVsBTN).length, bbVsBTN.length), "30-37", bbVsBTN.length, registerStatPopup("stat-bb-fold-btn", "BB fold vs BTN", bbVsBTN.filter((hand) => hand.bbFoldVsBTN), { opportunityHands: bbVsBTN, decisionSpot: "after-open", defaultAction: "fold", betSizeMode: "open" })),
    statRow("BB fold vs SB", percentValue(bbVsSB.filter((hand) => hand.bbFoldVsSB).length, bbVsSB.length), "34-39", bbVsSB.length, registerStatPopup("stat-bb-fold-sb", "BB fold vs SB", bbVsSB.filter((hand) => hand.bbFoldVsSB), { opportunityHands: bbVsSB, decisionSpot: "after-open", defaultAction: "fold", betSizeMode: "open" })),
    statRow("BB fold vs CO-SB", percentValue(bbHands.filter((hand) => hand.bbFoldVsSteal).length, bbStealOpportunityHands.length), "34.9-43", bbStealOpportunityHands.length, registerStatPopup("stat-bb-fold-steal", "BB fold vs CO-SB", bbHands.filter((hand) => hand.bbFoldVsSteal), { opportunityHands: bbStealOpportunityHands, decisionSpot: "after-open", defaultAction: "fold", betSizeMode: "open" })),
  ].join("");

  els.postflopStats.innerHTML = [
    statRow("Cbet flop", percentValue(count((hand) => hand.cbetFlop), cbetFlopOpp), "63-77", cbetFlopOpp, registerStatPopup("stat-cbet-flop", "Cbet flop", hands.filter((hand) => hand.cbetFlop))),
    statRow("Cbet flop IP", percentValue(count((hand) => hand.cbetFlopIP), cbetFlopIPOpp), "70-83", cbetFlopIPOpp, registerStatPopup("stat-cbet-flop-ip", "Cbet flop IP", hands.filter((hand) => hand.cbetFlopIP))),
    statRow("Cbet flop OOP", percentValue(count((hand) => hand.cbetFlopOOP), cbetFlopOOPOpp), "39-46", cbetFlopOOPOpp, registerStatPopup("stat-cbet-flop-oop", "Cbet flop OOP", hands.filter((hand) => hand.cbetFlopOOP))),
    statRow("Bet vs missed", percentValue(count((hand) => hand.betVsMissedFlop), betVsMissedFlopOpp), "43-53", betVsMissedFlopOpp, registerStatPopup("stat-bet-vs-missed-flop", "Bet vs missed", hands.filter((hand) => hand.betVsMissedFlop))),
    statRow("Cbet turn", percentValue(count((hand) => hand.cbetTurn), cbetTurnOpp), "53-58", cbetTurnOpp, registerStatPopup("stat-cbet-turn", "Cbet turn", hands.filter((hand) => hand.cbetTurn))),
    statRow("Cbet river", percentValue(count((hand) => hand.cbetRiver), cbetRiverOpp), "53-56", cbetRiverOpp, registerStatPopup("stat-cbet-river", "Cbet river", hands.filter((hand) => hand.cbetRiver))),
    statRow("River Bet", percentValue(count((hand) => hand.riverBet), riverBetOpp), "37-41", riverBetOpp, registerStatPopup("stat-river-bet", "River Bet", hands.filter((hand) => hand.riverBet))),
    statRow("Fold to Cbet", percentValue(count((hand) => hand.foldToFlopCbet), facedFlopCbet), "39-47", facedFlopCbet, registerStatPopup("stat-fold-cbet-flop", "Fold to Cbet", hands.filter((hand) => hand.foldToFlopCbet))),
    statRow("Fold to Cbet turn", percentValue(count((hand) => hand.foldToTurnCbet), facedTurnCbet), "46-52", facedTurnCbet, registerStatPopup("stat-fold-cbet-turn", "Fold to Cbet turn", hands.filter((hand) => hand.foldToTurnCbet))),
    statRow("Fold to Cbet river", percentValue(count((hand) => hand.foldToRiverCbet), facedRiverCbet), "52-60", facedRiverCbet, registerStatPopup("stat-fold-cbet-river", "Fold to Cbet river", hands.filter((hand) => hand.foldToRiverCbet))),
    statRow("XR OOP (blinds)", percentValue(count((hand) => hand.xrOopFlop), xrOopFlopOpp), "17-27", xrOopFlopOpp, registerStatPopup("stat-xr-oop-flop", "XR OOP (blinds)", hands.filter((hand) => hand.xrOopFlop))),
    statRow("Skip CB e XF", percentValue(count((hand) => hand.skipCbetFoldFlop), skipCbetOpp), "43-49", skipCbetOpp, registerStatPopup("stat-skip-cb-xf", "Skip CB e XF", hands.filter((hand) => hand.skipCbetFoldFlop))),
    statRow("Skip CB e XR", percentValue(count((hand) => hand.xrPfaFlop), xrPfaFlopOpp), "20-30", xrPfaFlopOpp, registerStatPopup("stat-skip-cb-xr", "XR OOP aggressor", hands.filter((hand) => hand.xrPfaFlop))),
    statRow("Won when saw flop (WWSF)", percentValue(wonWhenSawFlop, sawFlop), "44-49", sawFlop, registerStatPopup("stat-wwsf", "Won when saw flop (WWSF)", hands.filter((hand) => hand.sawFlop && wonHand(hand)))),
    statRow("Went to SD (WTSD)", percentValue(showedDown, sawFlop), "25-31", sawFlop, registerStatPopup("stat-wtsd", "Went to showdown (WTSD)", hands.filter((hand) => hand.showedDown))),
    statRow("Won at SD (W$SD)", percentValue(count((hand) => hand.wonShowdown), showedDown), "50-54", showedDown, registerStatPopup("stat-won-sd", "Won at SD (W$SD)", hands.filter((hand) => hand.wonShowdown))),
  ].join("");

  els.threeBetByPosition.innerHTML = positionStatRows(hands, ["EP", "MP", "HJ", "CO", "BTN", "SB", "BB"], (hand) => hand.threeBet, {
    EP: "4-7",
    MP: "7-9",
    HJ: "7-9",
    CO: "7-10",
    BTN: "9-12",
    SB: "9-14",
    BB: "9-13",
  }, (hand) => hand.threeBetOpportunity, "3bet", { decisionSpot: "after-open", defaultAction: "raise" });

  els.rfiByPosition.innerHTML = positionStatRows(hands, ["EP", "MP", "HJ", "CO", "BTN", "SB"], (hand) => hand.ggleaksRfi, {
    EP: "13-19",
    MP: "20-27",
    HJ: "22-29",
    CO: "30-37",
    BTN: "49-56",
    SB: "45-66",
  }, (hand) => hand.ggleaksRfiOpportunity, "RFI", { decisionSpot: "first", defaultAction: "raise", betSizeMode: "open" });

  els.ccByPosition.innerHTML = [
    ...["EP", "MP", "HJ", "CO", "BTN", "SB", "BB"].map((position) => {
      const isBb = position === "BB";
      const rowHands = byPos(position).filter((hand) => isBb ? hand.coldCallBbOpportunity : hand.coldCallOpportunity);
      const madeHands = rowHands.filter((hand) => isBb ? hand.coldCallBb : hand.coldCall);
      return statRow(position, percentValue(madeHands.length, rowHands.length), "", rowHands.length || "", registerStatPopup(`CC-${position}`, `CC ${position}`, madeHands, { opportunityHands: rowHands, decisionSpot: "after-open", defaultAction: "call", betSizeMode: "open" }));
    }),
    statRow("TOT", percentValue(count((hand) => hand.coldCall), threeBetOpportunityHands.length), "11-16", threeBetOpportunityHands.length, registerStatPopup("CC-TOT", "CC TOT", hands.filter((hand) => hand.coldCall), { opportunityHands: threeBetOpportunityHands, decisionSpot: "after-open", defaultAction: "call", betSizeMode: "open" })),
  ].join("");

  const matrixRows = [
    ["Donk Bet",
      streetStatCell("street-donk-flop", "Donk Bet Flop", hands, (hand) => hand.donkBetFlop, (hand) => hand.donkBetFlopOpportunity),
      streetStatCell("street-donk-turn", "Donk Bet Turn", hands, (hand) => hand.donkBetTurn, (hand) => hand.donkBetTurnOpportunity),
      streetStatCell("street-donk-river", "Donk Bet River", hands, (hand) => hand.donkBetRiver, (hand) => hand.donkBetRiverOpportunity)],
    ["e Fold",
      streetStatCell("street-fold-flop", "Fold vs Bet Flop", hands, (hand) => hand.foldVsBetFlop, (hand) => hand.foldVsBetFlopOpportunity),
      streetStatCell("street-fold-turn", "Fold vs Bet Turn", hands, (hand) => hand.foldVsBetTurn, (hand) => hand.foldVsBetTurnOpportunity),
      streetStatCell("street-fold-river", "Fold vs Bet River", hands, (hand) => hand.foldVsBetRiver, (hand) => hand.foldVsBetRiverOpportunity)],
    ["XR OOP (blinds)",
      streetStatCell("street-xr-blinds-flop", "XR OOP Blinds Flop", hands, (hand) => hand.xrOopFlop, (hand) => hand.xrOopFlopOpportunity),
      streetStatCell("street-xr-blinds-turn", "XR OOP Blinds Turn", hands, (hand) => hand.xrOopTurn, (hand) => hand.xrOopTurnOpportunity),
      streetStatCell("street-xr-blinds-river", "XR OOP Blinds River", hands, (hand) => hand.xrOopRiver, (hand) => hand.xrOopRiverOpportunity)],
    ["XR (PFA)",
      streetStatCell("street-xr-pfa-flop", "XR PFA Flop", hands, (hand) => hand.xrPfaFlop, (hand) => hand.xrPfaFlopOpportunity),
      streetStatCell("street-xr-pfa-turn", "XR PFA Turn", hands, (hand) => hand.xrPfaTurn, (hand) => hand.xrPfaTurnOpportunity),
      streetStatCell("street-xr-pfa-river", "XR PFA River", hands, (hand) => hand.xrPfaRiver, (hand) => hand.xrPfaRiverOpportunity)],
    ["Raise IP",
      streetStatCell("street-raise-ip-flop", "Raise IP Flop", hands, (hand) => hand.raiseIpFlop, (hand) => hand.raiseIpFlopOpportunity),
      streetStatCell("street-raise-ip-turn", "Raise IP Turn", hands, (hand) => hand.raiseIpTurn, (hand) => hand.raiseIpTurnOpportunity),
      streetStatCell("street-raise-ip-river", "Raise IP River", hands, (hand) => hand.raiseIpRiver, (hand) => hand.raiseIpRiverOpportunity)],
    ["Bet vs missed",
      streetStatCell("street-bvm-flop", "Bet vs Missed Flop", hands, (hand) => hand.betVsMissedFlop, (hand) => hand.betVsMissedFlopOpportunity),
      streetStatCell("street-bvm-turn", "Bet vs Missed Turn", hands, (hand) => hand.betVsMissedTurn, (hand) => hand.betVsMissedTurnOpportunity),
      streetStatCell("street-bvm-river", "Bet vs Missed River", hands, (hand) => hand.betVsMissedRiver, (hand) => hand.betVsMissedRiverOpportunity)],
  ];
  els.streetMatrix.innerHTML = [
    `<div class="hud-matrix-row"><span></span><span>Flop</span><span>Turn</span><span>River</span></div>`,
    ...matrixRows.map((row) => `<div class="hud-matrix-row"><span>${row[0]}</span><span>${row[1]}</span><span>${row[2]}</span><span>${row[3]}</span></div>`),
  ].join("");
}

function renderSrpStats(hands) {
  if (!els.srpStatsContent) return;
  const ready = ensureSrpAnalysis(hands);
  if (!ready) return;
  statPopupRegistry.clear();

  const scenario = state.srpScenario || "defender-oop";
  const street = state.srpStreet || "flop";
  document.querySelectorAll("[data-srp-scenario]").forEach((button) => button.classList.toggle("is-active", button.dataset.srpScenario === scenario));
  document.querySelectorAll("[data-srp-street]").forEach((button) => button.classList.toggle("is-active", button.dataset.srpStreet === street));

  const rows = hands
    .map((hand) => ({ hand, spot: srpSpotForHand(hand) }))
    .filter((entry) => entry.spot?.scenario === scenario && entry.spot?.streets?.[street]);
  const paths = srpPreviousPaths(rows, street);
  renderSrpPathOptions(paths, street);
  const selectedRows = srpSelectedPath === "all" || street === "flop"
    ? rows
    : rows.filter((entry) => srpPathForSpot(entry.spot, street) === srpSelectedPath);
  const roles = scenario === "defender-oop"
    ? [["defender-oop", "Defensor OOP"], ["aggressor-ip", "Agressor IP"]]
    : [["aggressor-oop", "Agressor OOP"], ["defender-ip", "Defensor IP"]];

  if (els.srpSampleCaption) els.srpSampleCaption.textContent = `${fmt(selectedRows.length)} situacoes SRP heads-up`;
  if (!selectedRows.length) {
    els.srpStatsContent.innerHTML = `<div class="srp-empty">Nenhuma situacao encontrada para estes filtros.</div>`;
    return;
  }
  els.srpStatsContent.innerHTML = roles.map(([role, title]) => renderSrpRolePanel(selectedRows, role, title, street)).join("");
}

function renderThreeBetPotStats(hands) {
  if (!els.threeBetPotStatsContent) return;
  const ready = ensureThreeBetPotAnalysis(hands);
  if (!ready) return;
  statPopupRegistry.clear();

  const scenario = state.threeBetPotScenario || "defender-oop";
  const street = state.threeBetPotStreet || "flop";
  document.querySelectorAll("[data-three-bet-pot-scenario]").forEach((button) => {
    button.classList.toggle("is-active", button.dataset.threeBetPotScenario === scenario);
  });
  document.querySelectorAll("[data-three-bet-pot-street]").forEach((button) => {
    button.classList.toggle("is-active", button.dataset.threeBetPotStreet === street);
  });

  const rows = hands
    .map((hand) => ({ hand, spot: threeBetPotSpotForHand(hand) }))
    .filter((entry) => entry.spot?.scenario === scenario && entry.spot?.streets?.[street]);
  const paths = srpPreviousPaths(rows, street);
  renderThreeBetPotPathOptions(paths, street);
  const selectedRows = threeBetPotSelectedPath === "all" || street === "flop"
    ? rows
    : rows.filter((entry) => srpPathForSpot(entry.spot, street) === threeBetPotSelectedPath);
  const roles = scenario === "defender-oop"
    ? [["defender-oop", "Defensor OOP"], ["aggressor-ip", "3bettor IP"]]
    : [["aggressor-oop", "3bettor OOP"], ["defender-ip", "Defensor IP"]];

  if (els.threeBetPotSampleCaption) els.threeBetPotSampleCaption.textContent = `${fmt(selectedRows.length)} situacoes 3bet pot heads-up`;
  if (!selectedRows.length) {
    els.threeBetPotStatsContent.innerHTML = `<div class="srp-empty">Nenhuma situacao 3betada encontrada para estes filtros.</div>`;
    return;
  }
  els.threeBetPotStatsContent.innerHTML = roles
    .map(([role, title]) => renderThreeBetPotRolePanel(selectedRows, role, title, street))
    .join("");
}

function ensureThreeBetPotAnalysis(hands) {
  const missing = hands.filter((hand) => hand?.raw && !threeBetPotSpotCache.has(srpCacheKey(hand)));
  if (!missing.length) {
    if (els.threeBetPotLoading) els.threeBetPotLoading.hidden = true;
    return true;
  }
  const signature = `${hands.length}:${hands[0]?.handId || ""}:${hands[hands.length - 1]?.handId || ""}`;
  if (threeBetPotAnalysisJob?.signature === signature) return false;
  if (threeBetPotAnalysisJob) threeBetPotAnalysisJob.cancelled = true;
  const job = { signature, cancelled: false, cursor: 0, missing, hands };
  threeBetPotAnalysisJob = job;
  els.threeBetPotStatsContent.innerHTML = "";
  if (els.threeBetPotLoading) {
    els.threeBetPotLoading.hidden = false;
    els.threeBetPotLoading.textContent = `Preparando PosFlop 3Bet: 0 / ${fmt(missing.length)} maos`;
  }
  const runBatch = () => {
    if (job.cancelled) return;
    const end = Math.min(job.missing.length, job.cursor + 240);
    for (; job.cursor < end; job.cursor += 1) {
      const hand = job.missing[job.cursor];
      threeBetPotSpotCache.set(srpCacheKey(hand), buildThreeBetPotSpotFromRaw(hand.raw));
    }
    if (els.threeBetPotLoading) {
      els.threeBetPotLoading.textContent = `Preparando PosFlop 3Bet: ${fmt(job.cursor)} / ${fmt(job.missing.length)} maos`;
    }
    if (job.cursor < job.missing.length) {
      window.setTimeout(runBatch, 0);
      return;
    }
    threeBetPotAnalysisJob = null;
    if (els.threeBetPotLoading) els.threeBetPotLoading.hidden = true;
    if (state.activeTab === "stats" && state.statsSection === "three-bet-pot") renderThreeBetPotStats(job.hands);
  };
  window.setTimeout(runBatch, 0);
  return false;
}

function threeBetPotSpotForHand(hand) {
  return threeBetPotSpotCache.get(srpCacheKey(hand)) || null;
}

function renderThreeBetPotPathOptions(paths, street) {
  if (!els.threeBetPotPathBar || !els.threeBetPotPathSelect) return;
  els.threeBetPotPathBar.hidden = street === "flop";
  if (street === "flop") {
    threeBetPotSelectedPath = "all";
    return;
  }
  if (threeBetPotSelectedPath !== "all" && !paths.some(([path]) => path === threeBetPotSelectedPath)) {
    threeBetPotSelectedPath = "all";
  }
  els.threeBetPotPathSelect.innerHTML = [
    `<option value="all">Todas as linhas anteriores</option>`,
    ...paths.map(([path, count]) => `<option value="${escapeAttr(path)}">${escapeHtml(srpPathLabel(path))} (${fmt(count)})</option>`),
  ].join("");
  els.threeBetPotPathSelect.value = threeBetPotSelectedPath;
}

function renderThreeBetPotRolePanel(rows, role, title, street) {
  const roleRows = rows.filter((entry) => entry.spot.role === role);
  const definitions = srpStatDefinitions(role, street);
  const body = definitions.map((definition, index) => {
    const opportunityRows = roleRows.filter((entry) => definition.opportunity(entry.spot.streets[street]));
    const actionRows = opportunityRows.filter((entry) => definition.metric(entry.spot.streets[street]));
    const popupKey = registerStatPopup(
      `three-bet-pot-${state.threeBetPotScenario}-${street}-${role}-${index}-${threeBetPotSelectedPath}`,
      `${title} - ${definition.label} ${street}`,
      actionRows.map((entry) => entry.hand),
      { opportunityHands: opportunityRows.map((entry) => entry.hand) }
    );
    const value = percentValue(actionRows.length, opportunityRows.length);
    return `<button class="srp-stat-row" data-stat-popup="${escapeAttr(popupKey)}" title="${escapeAttr(definition.title)}" type="button">
      <span>${definition.label}</span><strong>${value ?? "-"}${value === null ? "" : "%"}</strong><small>${fmt(opportunityRows.length)}</small>
    </button>`;
  }).join("");
  return `<section class="srp-role-panel"><h3>${title}</h3>
    <div class="srp-role-head"><span>Acao</span><span>Stat</span><span>Amostra</span></div>${body}
  </section>`;
}

const BLIND_WAR_STREETS = ["flop", "turn", "river"];

function renderBlindWarStats(hands) {
  if (!els.blindWarStatsContent) return;
  const ready = ensureBlindWarAnalysis(hands);
  if (!ready) return;
  statPopupRegistry.clear();

  const side = state.blindWarSide === "sb" ? "sb" : "bb";
  document.querySelectorAll("[data-blind-war-side]").forEach((button) => {
    button.classList.toggle("is-active", button.dataset.blindWarSide === side);
  });
  const rows = hands
    .map((hand) => ({ hand, spot: blindWarSpotForHand(hand) }))
    .filter((entry) => entry.spot?.side === side);

  if (els.blindWarSampleCaption) {
    els.blindWarSampleCaption.textContent = `${fmt(rows.length)} situacoes SB x BB heads-up`;
  }
  if (!rows.length) {
    els.blindWarStatsContent.innerHTML = `<div class="srp-empty">Nenhuma situacao de blind war encontrada para estes filtros.</div>`;
    return;
  }
  els.blindWarStatsContent.innerHTML = blindWarGroups(side)
    .map((group) => group.response
      ? renderBlindWarResponseGroup(rows, side, group)
      : renderBlindWarGroup(rows, side, group))
    .join("");
}

function ensureBlindWarAnalysis(hands) {
  const missing = hands.filter((hand) => hand?.raw && !blindWarSpotCache.has(srpCacheKey(hand)));
  if (!missing.length) {
    if (els.blindWarLoading) els.blindWarLoading.hidden = true;
    return true;
  }
  const signature = `${hands.length}:${hands[0]?.handId || ""}:${hands[hands.length - 1]?.handId || ""}`;
  if (blindWarAnalysisJob?.signature === signature) return false;
  if (blindWarAnalysisJob) blindWarAnalysisJob.cancelled = true;
  const job = { signature, cancelled: false, cursor: 0, missing, hands };
  blindWarAnalysisJob = job;
  els.blindWarStatsContent.innerHTML = "";
  if (els.blindWarLoading) {
    els.blindWarLoading.hidden = false;
    els.blindWarLoading.textContent = `Preparando BlindWar Pos: 0 / ${fmt(missing.length)} maos`;
  }
  const runBatch = () => {
    if (job.cancelled) return;
    const end = Math.min(job.missing.length, job.cursor + 240);
    for (; job.cursor < end; job.cursor += 1) {
      const hand = job.missing[job.cursor];
      blindWarSpotCache.set(srpCacheKey(hand), buildBlindWarSpotFromRaw(hand.raw));
    }
    if (els.blindWarLoading) {
      els.blindWarLoading.textContent = `Preparando BlindWar Pos: ${fmt(job.cursor)} / ${fmt(job.missing.length)} maos`;
    }
    if (job.cursor < job.missing.length) {
      window.setTimeout(runBatch, 0);
      return;
    }
    blindWarAnalysisJob = null;
    if (els.blindWarLoading) els.blindWarLoading.hidden = true;
    if (state.activeTab === "stats" && state.statsSection === "blind-war-post") renderBlindWarStats(job.hands);
  };
  window.setTimeout(runBatch, 0);
  return false;
}

function blindWarSpotForHand(hand) {
  return blindWarSpotCache.get(srpCacheKey(hand)) || null;
}

function blindWarLine(spot, street) {
  return spot?.streets?.[street] || null;
}

function blindWarPreviousStreet(street) {
  if (street === "turn") return "flop";
  if (street === "river") return "turn";
  return "";
}

function blindWarFacedBet(line) {
  return Boolean(line?.facedBet && ["folds", "calls", "raises"].includes(line.responseToBet));
}

function blindWarResponse(line) {
  if (!blindWarFacedBet(line)) return "";
  if (line.responseToBet === "folds") return "fold";
  if (line.responseToBet === "calls") return "call";
  if (line.responseToBet === "raises") return "raise";
  return "";
}

function blindWarCanBet(line) {
  return Boolean(line && ["B", "X"].includes(line.root));
}

function blindWarBet(line) {
  return line?.root === "B";
}

function blindWarCheckedFacingBet(line) {
  return Boolean(line?.checked && blindWarFacedBet(line));
}

function blindWarFoldedToRaise(line) {
  return line?.facedRaise && line.responseToRaise === "folds";
}

function blindWarContextRows(rows, context) {
  return rows.filter((entry) => entry.spot.context === context);
}

function blindWarGroups(side) {
  if (side === "bb") {
    return [
      {
        key: "after-rfi",
        title: "Depois de pagar RFI",
        context: "sb-open",
        stats: [
          blindWarFacingBetStat("Fold to CBet", "fold"),
          blindWarFacingBetStat("Raise vs CBet", "raise"),
          blindWarBetStat("Bet vs Missed"),
          blindWarBarrelStat("BvM + Barrel"),
          blindWarFoldRaiseStat("BvM + F2 Check Raise"),
        ],
      },
      {
        key: "after-iso",
        title: "Depois do iso",
        context: "bb-iso",
        stats: [
          blindWarBetStat("CBet"),
          blindWarFoldRaiseStat("F2 XR"),
          blindWarProbeResponseStat("F2 Probe"),
          blindWarFacingBetStat("F2 Donk", "fold"),
          blindWarDelayedBetStat("Delayed CBet"),
        ],
      },
      {
        key: "limped-pot",
        title: "Pote limpado",
        context: "limped",
        stats: [
          blindWarBarrelStat("BvM + Barrel"),
          blindWarFoldRaiseStat("BvM + F2 Check Raise"),
        ],
      },
      {
        key: "vs-stabs",
        title: "Contra stabs",
        context: "limped",
        response: true,
        events: blindWarBbResponseEvents(),
      },
    ];
  }
  return [
    {
      key: "open-raised",
      title: "Open Raised",
      context: "sb-open",
      stats: [
        blindWarBetStat("CBet"),
        blindWarFoldRaiseStat("CBet + Fold"),
        blindWarCheckResponseStat("Check-Fold", "fold"),
        blindWarCheckResponseStat("Check-Call", "call"),
        blindWarCheckResponseStat("Check-Raise", "raise"),
        blindWarDelayedBetStat("Delayed CBet"),
      ],
    },
    {
      key: "limp-call-pot",
      title: "Limp-call pot",
      context: "bb-iso",
      stats: [
        blindWarFacingBetStat("Fold to CBet", "fold"),
        blindWarCheckResponseStat("Check-Raise", "raise"),
        blindWarProbeBetStat("Probe"),
        blindWarBetStat("Lead"),
        blindWarProbeResponseStat("F2 Delayed"),
      ],
    },
    {
      key: "limped-pot",
      title: "Limped Pot",
      context: "limped",
      stats: [
        blindWarFacingBetStat("F2 BvM", "fold"),
        blindWarCheckResponseStat("Check Raise", "raise"),
      ],
    },
    {
      key: "stabs",
      title: "Stabs",
      context: "limped",
      stats: [
        blindWarStreetBetStat("Stab Flop", ["flop"]),
        blindWarBarrelStat("Stab + Bet"),
        blindWarTripleBarrelStat("Stab Bet Bet"),
        blindWarDelayedBetStat("Delayed stab"),
        blindWarRiverProbeStat("Probe River"),
      ],
    },
  ];
}

function blindWarFacingBetStat(label, response) {
  return {
    label,
    opportunity: (spot, street) => blindWarFacedBet(blindWarLine(spot, street)),
    metric: (spot, street) => blindWarResponse(blindWarLine(spot, street)) === response,
  };
}

function blindWarCheckResponseStat(label, response) {
  return {
    label,
    opportunity: (spot, street) => blindWarCheckedFacingBet(blindWarLine(spot, street)),
    metric: (spot, street) => blindWarResponse(blindWarLine(spot, street)) === response,
  };
}

function blindWarBetStat(label) {
  return {
    label,
    opportunity: (spot, street) => blindWarCanBet(blindWarLine(spot, street)),
    metric: (spot, street) => blindWarBet(blindWarLine(spot, street)),
  };
}

function blindWarStreetBetStat(label, streets) {
  return {
    label,
    streets,
    opportunity: (spot, street) => blindWarCanBet(blindWarLine(spot, street)),
    metric: (spot, street) => blindWarBet(blindWarLine(spot, street)),
  };
}

function blindWarBarrelStat(label) {
  return {
    label,
    opportunity: (spot, street) => {
      const line = blindWarLine(spot, street);
      if (street === "flop") return blindWarCanBet(line);
      const previous = blindWarLine(spot, blindWarPreviousStreet(street));
      return blindWarBet(previous) && blindWarCanBet(line);
    },
    metric: (spot, street) => blindWarBet(blindWarLine(spot, street)),
  };
}

function blindWarFoldRaiseStat(label) {
  return {
    label,
    opportunity: (spot, street) => Boolean(blindWarLine(spot, street)?.facedRaise),
    metric: (spot, street) => blindWarFoldedToRaise(blindWarLine(spot, street)),
  };
}

function blindWarDelayedBetStat(label) {
  return {
    label,
    streets: ["turn", "river"],
    opportunity: (spot, street) => {
      const previous = blindWarLine(spot, blindWarPreviousStreet(street));
      return previous?.root === "X" && blindWarCanBet(blindWarLine(spot, street));
    },
    metric: (spot, street) => blindWarBet(blindWarLine(spot, street)),
  };
}

function blindWarProbeBetStat(label) {
  return {
    label,
    streets: ["turn", "river"],
    opportunity: (spot, street) => {
      const previous = blindWarLine(spot, blindWarPreviousStreet(street));
      return previous?.root === "X" && blindWarCanBet(blindWarLine(spot, street));
    },
    metric: (spot, street) => blindWarBet(blindWarLine(spot, street)),
  };
}

function blindWarProbeResponseStat(label) {
  return {
    label,
    streets: ["turn", "river"],
    opportunity: (spot, street) => {
      const previous = blindWarLine(spot, blindWarPreviousStreet(street));
      return previous?.root === "X" && blindWarFacedBet(blindWarLine(spot, street));
    },
    metric: (spot, street) => blindWarResponse(blindWarLine(spot, street)) === "fold",
  };
}

function blindWarTripleBarrelStat(label) {
  return {
    label,
    streets: ["river"],
    opportunity: (spot) => blindWarBet(blindWarLine(spot, "flop"))
      && blindWarBet(blindWarLine(spot, "turn"))
      && blindWarCanBet(blindWarLine(spot, "river")),
    metric: (spot) => blindWarBet(blindWarLine(spot, "river")),
  };
}

function blindWarRiverProbeStat(label) {
  return {
    label,
    streets: ["river"],
    opportunity: (spot) => blindWarLine(spot, "turn")?.root === "X"
      && blindWarCanBet(blindWarLine(spot, "river")),
    metric: (spot) => blindWarBet(blindWarLine(spot, "river")),
  };
}

function blindWarBbResponseEvents() {
  const directBet = (spot, street) => blindWarFacedBet(blindWarLine(spot, street));
  return [
    { label: "vs stab flop", opportunity: (spot) => directBet(spot, "flop"), street: "flop" },
    { label: "+ bet turn", opportunity: (spot) => directBet(spot, "flop") && directBet(spot, "turn"), street: "turn" },
    { label: "+ bet river", opportunity: (spot) => directBet(spot, "turn") && directBet(spot, "river"), street: "river" },
    { label: "vs delayed stab", opportunity: (spot) => blindWarLine(spot, "flop")?.root === "X" && directBet(spot, "turn"), street: "turn" },
    { label: "BvM + X: vs Probe river", opportunity: (spot) => blindWarBet(blindWarLine(spot, "flop")) && blindWarLine(spot, "turn")?.root === "X" && directBet(spot, "river"), street: "river" },
  ];
}

function renderBlindWarGroup(rows, side, group) {
  const contextRows = blindWarContextRows(rows, group.context);
  const body = group.stats.map((definition, index) => {
    const cells = BLIND_WAR_STREETS.map((street) => renderBlindWarMetricCell(
      contextRows,
      side,
      group,
      definition,
      index,
      street
    )).join("");
    return `<div class="blind-war-stat-row"><span>${escapeHtml(definition.label)}</span>${cells}</div>`;
  }).join("");
  return `<section class="blind-war-group"><h3>${escapeHtml(group.title)}</h3>
    <div class="blind-war-matrix-head"><span>Stat</span><span>Flop</span><span>Turn</span><span>River</span></div>${body}
  </section>`;
}

function renderBlindWarMetricCell(rows, side, group, definition, index, street) {
  if (definition.streets && !definition.streets.includes(street)) {
    return `<button class="blind-war-stat-cell" type="button" disabled>-</button>`;
  }
  const opportunityRows = rows.filter((entry) => definition.opportunity(entry.spot, street));
  const actionRows = opportunityRows.filter((entry) => definition.metric(entry.spot, street));
  if (!opportunityRows.length) {
    return `<button class="blind-war-stat-cell" type="button" disabled>-</button>`;
  }
  const popupKey = registerStatPopup(
    `blind-war-${side}-${group.key}-${index}-${street}`,
    `${group.title} - ${definition.label} ${street}`,
    actionRows.map((entry) => entry.hand),
    { opportunityHands: opportunityRows.map((entry) => entry.hand) }
  );
  return `<button class="blind-war-stat-cell" data-stat-popup="${escapeAttr(popupKey)}" type="button">
    ${percentValue(actionRows.length, opportunityRows.length)}%<small>${fmt(opportunityRows.length)}</small>
  </button>`;
}

function renderBlindWarResponseGroup(rows, side, group) {
  const contextRows = blindWarContextRows(rows, group.context);
  const body = group.events.map((event, index) => {
    const opportunityRows = contextRows.filter((entry) => event.opportunity(entry.spot));
    const cells = ["fold", "call", "raise"].map((response) => renderBlindWarResponseCell(
      opportunityRows,
      side,
      group,
      event,
      index,
      response
    )).join("");
    return `<div class="blind-war-response-row"><span>${escapeHtml(event.label)}</span>${cells}</div>`;
  }).join("");
  return `<section class="blind-war-group is-wide"><h3>${escapeHtml(group.title)}</h3>
    <div class="blind-war-response-head"><span>Spot</span><span>Fold</span><span>Call</span><span>Raise</span></div>${body}
  </section>`;
}

function renderBlindWarResponseCell(rows, side, group, event, index, response) {
  if (!rows.length) return `<button class="blind-war-stat-cell" type="button" disabled>-</button>`;
  const actionRows = rows.filter((entry) => blindWarResponse(blindWarLine(entry.spot, event.street)) === response);
  const popupKey = registerStatPopup(
    `blind-war-${side}-${group.key}-${index}-${response}`,
    `${group.title} - ${event.label} ${response}`,
    actionRows.map((entry) => entry.hand),
    { opportunityHands: rows.map((entry) => entry.hand) }
  );
  return `<button class="blind-war-stat-cell" data-stat-popup="${escapeAttr(popupKey)}" type="button">
    ${percentValue(actionRows.length, rows.length)}%<small>${fmt(rows.length)}</small>
  </button>`;
}

function ensureSrpAnalysis(hands) {
  const missing = hands.filter((hand) => hand?.raw && hand.srpSpot === undefined && !srpSpotCache.has(srpCacheKey(hand)));
  if (!missing.length) {
    if (els.srpLoading) els.srpLoading.hidden = true;
    return true;
  }
  const signature = `${hands.length}:${hands[0]?.handId || ""}:${hands[hands.length - 1]?.handId || ""}`;
  if (srpAnalysisJob?.signature === signature) return false;
  if (srpAnalysisJob) srpAnalysisJob.cancelled = true;
  const job = { signature, cancelled: false, cursor: 0, missing, hands };
  srpAnalysisJob = job;
  if (els.srpStatsContent) els.srpStatsContent.innerHTML = "";
  if (els.srpLoading) {
    els.srpLoading.hidden = false;
    els.srpLoading.textContent = `Preparando PosFlop SRP: 0 / ${fmt(missing.length)} maos`;
  }
  const runBatch = () => {
    if (job.cancelled) return;
    const end = Math.min(job.missing.length, job.cursor + 240);
    for (; job.cursor < end; job.cursor += 1) {
      const hand = job.missing[job.cursor];
      srpSpotCache.set(srpCacheKey(hand), buildSrpSpotFromRaw(hand.raw));
    }
    if (els.srpLoading) els.srpLoading.textContent = `Preparando PosFlop SRP: ${fmt(job.cursor)} / ${fmt(job.missing.length)} maos`;
    if (job.cursor < job.missing.length) {
      window.setTimeout(runBatch, 0);
      return;
    }
    srpAnalysisJob = null;
    if (els.srpLoading) els.srpLoading.hidden = true;
    if (state.activeTab === "stats" && state.statsSection === "srp") renderSrpStats(job.hands);
  };
  window.setTimeout(runBatch, 0);
  return false;
}

function srpCacheKey(hand) {
  return `${hand?.handId || "sem-id"}:${String(hand?.raw || "").length}`;
}

function srpSpotForHand(hand) {
  if (hand?.srpSpot !== undefined) return hand.srpSpot;
  return srpSpotCache.get(srpCacheKey(hand)) || null;
}

function srpPreviousPaths(rows, street) {
  if (street === "flop") return [];
  const counts = new Map();
  for (const entry of rows) {
    const path = srpPathForSpot(entry.spot, street);
    if (path) counts.set(path, (counts.get(path) || 0) + 1);
  }
  return [...counts.entries()].sort((a, b) => b[1] - a[1]);
}

function srpPathForSpot(spot, street) {
  const flop = spot?.streets?.flop?.code;
  if (street === "turn") return flop || "";
  const turn = spot?.streets?.turn?.code;
  return flop && turn ? `${flop} > ${turn}` : "";
}

function renderSrpPathOptions(paths, street) {
  if (!els.srpPathBar || !els.srpPathSelect) return;
  els.srpPathBar.hidden = street === "flop";
  if (street === "flop") {
    srpSelectedPath = "all";
    return;
  }
  if (srpSelectedPath !== "all" && !paths.some(([path]) => path === srpSelectedPath)) srpSelectedPath = "all";
  els.srpPathSelect.innerHTML = [
    `<option value="all">Todas as linhas anteriores</option>`,
    ...paths.map(([path, count]) => `<option value="${escapeAttr(path)}">${escapeHtml(srpPathLabel(path))} (${fmt(count)})</option>`),
  ].join("");
  els.srpPathSelect.value = srpSelectedPath;
}

function srpPathLabel(path) {
  return String(path).split(" > ").map((code, index) => `${index === 0 ? "Flop" : "Turn"} ${code}`).join(" / ");
}

function renderSrpRolePanel(rows, role, title, street) {
  const roleRows = rows.filter((entry) => entry.spot.role === role);
  const definitions = srpStatDefinitions(role, street);
  const body = definitions.map((definition, index) => {
    const opportunityRows = roleRows.filter((entry) => definition.opportunity(entry.spot.streets[street]));
    const actionRows = opportunityRows.filter((entry) => definition.metric(entry.spot.streets[street]));
    const popupKey = registerStatPopup(`srp-${state.srpScenario}-${street}-${role}-${index}-${srpSelectedPath}`, `${title} - ${definition.label} ${street}`, actionRows.map((entry) => entry.hand), {
      opportunityHands: opportunityRows.map((entry) => entry.hand),
    });
    const value = percentValue(actionRows.length, opportunityRows.length);
    return `<button class="srp-stat-row" data-stat-popup="${escapeAttr(popupKey)}" title="${escapeAttr(definition.title)}" type="button">
      <span>${definition.label}</span><strong>${value ?? "-"}${value === null ? "" : "%"}</strong><small>${fmt(opportunityRows.length)}</small>
    </button>`;
  }).join("");
  return `<section class="srp-role-panel"><h3>${title}</h3>
    <div class="srp-role-head"><span>Acao</span><span>Stat</span><span>Amostra</span></div>${body}
  </section>`;
}

function srpStatDefinitions(role, street) {
  const isFlop = street === "flop";
  const rows = [];
  const add = (label, opportunity, metric, title) => rows.push({ label, opportunity, metric, title });
  const exists = (line) => Boolean(line);
  const checkedFacingBet = (line) => Boolean(line?.checked && line.facedBet);
  const betFacingRaise = (line) => Boolean(line?.root === "B" && line.facedRaise);
  const raisedFacingReraise = (line) => Boolean((line?.root === "R" || line?.code?.startsWith("XR")) && line.facedReraise);
  const checkedTo = (line) => Boolean(line && ["X", "B"].includes(line.root));
  const facedBet = (line) => Boolean(line?.facedBet && ["F", "C", "R"].includes(line.root));

  if (role === "defender-oop") {
    add("Check", exists, (line) => line.root === "X", "Primeira acao: check");
    add("Check-Fold", checkedFacingBet, (line) => line.responseToBet === "folds", "Check e fold contra aposta");
    add("Check-Call", checkedFacingBet, (line) => line.responseToBet === "calls", "Check e call contra aposta");
    add("Check-Raise", checkedFacingBet, (line) => line.responseToBet === "raises", "Check e raise contra aposta");
    add("Check-Raise-Fold", raisedFacingReraise, (line) => line.responseToReraise === "folds", "Check-raise e fold contra novo raise");
    add("Check-Raise-Call", raisedFacingReraise, (line) => line.responseToReraise === "calls", "Check-raise e call contra novo raise");
    add(isFlop ? "Donk Bet" : "Lead Bet", exists, (line) => line.root === "B", "Aposta antes do agressor");
    add("Bet-Fold", betFacingRaise, (line) => line.responseToRaise === "folds", "Aposta e fold contra raise");
    add("Bet-Call", betFacingRaise, (line) => line.responseToRaise === "calls", "Aposta e call contra raise");
    add("Bet-Raise", betFacingRaise, (line) => line.responseToRaise === "raises", "Aposta e novo raise");
  } else if (role === "aggressor-ip") {
    add("Check back", checkedTo, (line) => line.root === "X", "Check em posicao depois do check adversario");
    add(isFlop ? "Cbet" : "Bet", checkedTo, (line) => line.root === "B", "Aposta em posicao depois do check adversario");
    add("Bet-Fold", betFacingRaise, (line) => line.responseToRaise === "folds", "Aposta e fold contra check-raise");
    add("Bet-Call", betFacingRaise, (line) => line.responseToRaise === "calls", "Aposta e call contra check-raise");
    add("Bet-Raise", betFacingRaise, (line) => line.responseToRaise === "raises", "Aposta e novo raise");
    add("Fold vs Donk", facedBet, (line) => line.root === "F", "Fold contra donk/lead");
    add("Call vs Donk", facedBet, (line) => line.root === "C", "Call contra donk/lead");
    add("Raise vs Donk", facedBet, (line) => line.root === "R", "Raise contra donk/lead");
    add("Raise-Fold", raisedFacingReraise, (line) => line.responseToReraise === "folds", "Raise e fold contra novo raise");
    add("Raise-Call", raisedFacingReraise, (line) => line.responseToReraise === "calls", "Raise e call contra novo raise");
  } else if (role === "aggressor-oop") {
    add(isFlop ? "Cbet" : "Bet", exists, (line) => line.root === "B", "Aposta como agressor fora de posicao");
    add("Bet-Fold", betFacingRaise, (line) => line.responseToRaise === "folds", "Aposta e fold contra raise");
    add("Bet-Call", betFacingRaise, (line) => line.responseToRaise === "calls", "Aposta e call contra raise");
    add("Bet-Raise", betFacingRaise, (line) => line.responseToRaise === "raises", "Aposta e novo raise");
    add("Check", exists, (line) => line.root === "X", "Check como agressor fora de posicao");
    add("Check-Fold", checkedFacingBet, (line) => line.responseToBet === "folds", "Check e fold contra aposta");
    add("Check-Call", checkedFacingBet, (line) => line.responseToBet === "calls", "Check e call contra aposta");
    add("Check-Raise", checkedFacingBet, (line) => line.responseToBet === "raises", "Check e raise contra aposta");
    add("Check-Raise-Fold", raisedFacingReraise, (line) => line.responseToReraise === "folds", "Check-raise e fold contra novo raise");
    add("Check-Raise-Call", raisedFacingReraise, (line) => line.responseToReraise === "calls", "Check-raise e call contra novo raise");
  } else {
    add("Fold vs Bet", facedBet, (line) => line.root === "F", "Fold contra aposta do agressor");
    add("Call Bet", facedBet, (line) => line.root === "C", "Call contra aposta do agressor");
    add("Raise vs Bet", facedBet, (line) => line.root === "R", "Raise contra aposta do agressor");
    add("Raise-Fold", raisedFacingReraise, (line) => line.responseToReraise === "folds", "Raise e fold contra novo raise");
    add("Raise-Call", raisedFacingReraise, (line) => line.responseToReraise === "calls", "Raise e call contra novo raise");
    add("Check back", checkedTo, (line) => line.root === "X", "Check em posicao depois do check adversario");
    add("Bet vs missed", checkedTo, (line) => line.root === "B", "Aposta depois do agressor dar check");
    add("Bet-Fold", betFacingRaise, (line) => line.responseToRaise === "folds", "Aposta e fold contra check-raise");
    add("Bet-Call", betFacingRaise, (line) => line.responseToRaise === "calls", "Aposta e call contra check-raise");
    add("Bet-Raise", betFacingRaise, (line) => line.responseToRaise === "raises", "Aposta e novo raise");
  }
  return rows;
}

function renderPositionReport(hands) {
  if (!els.positionReportTable) return;
  const profitMap = buildHandProfitMap(state.hands);
  const positions = ["UTG", "UTG1", "MP", "LJ", "HJ", "CO", "BTN", "SB", "BB"];
  const rows = positions.map((position) => {
    const rowHands = hands.filter((hand) => reportPosition(hand) === position);
    return buildPositionReportRow(position, rowHands, profitMap);
  }).filter((row) => row.hands > 0);
  const summary = buildPositionReportRow("SUMMARY", hands, profitMap);
  els.positionReportTable.innerHTML = [...rows, summary]
    .map((row) => `<tr class="${row.position === "SUMMARY" ? "summary-row" : ""}">
      <td>${row.position}</td>
      <td>${fmt(row.hands)}</td>
      <td class="${row.netWon >= 0 ? "number-positive" : "number-negative"}">${formatChips(row.netWon)}</td>
      <td class="${row.netWonEv >= 0 ? "number-positive" : "number-negative"}">${formatChips(row.netWonEv)}</td>
      <td class="${row.bb100 >= 0 ? "number-positive" : "number-negative"}">${row.bb100.toFixed(1)}</td>
      <td class="${row.evBb100 >= 0 ? "number-positive" : "number-negative"}">${row.evBb100.toFixed(1)}</td>
    </tr>`)
    .join("");
}

function buildPositionReportRow(position, hands, profitMap) {
  const resultHands = hands.filter((hand) => profitMap.has(hand.handId));
  const netWon = resultHands.reduce((sum, hand) => sum + profitMap.get(hand.handId).chips, 0);
  const netWonBb = resultHands.reduce((sum, hand) => sum + profitMap.get(hand.handId).bb, 0);
  const netWonEv = resultHands.reduce((sum, hand) => sum + profitMap.get(hand.handId).evChips, 0);
  const netWonEvBb = resultHands.reduce((sum, hand) => sum + profitMap.get(hand.handId).evBb, 0);
  const bb100 = resultHands.length ? (netWonBb / resultHands.length) * 100 : 0;
  const evBb100 = resultHands.length ? (netWonEvBb / resultHands.length) * 100 : 0;
  return {
    position,
    hands: resultHands.length,
    netWon,
    netWonEv,
    bb100,
    evBb100,
  };
}

function buildHandProfitMap(hands) {
  const profitMap = new Map();
  const groups = new Map();
  for (const hand of hands) {
    if (hand.handId && Number.isFinite(hand.netChips) && Number.isFinite(hand.bigBlind) && hand.bigBlind > 0) {
      const evChips = Number.isFinite(hand.evChips) ? hand.evChips : hand.netChips;
      profitMap.set(hand.handId, {
        chips: hand.netChips,
        bb: hand.netChips / hand.bigBlind,
        evChips,
        evBb: evChips / hand.bigBlind,
      });
      continue;
    }
    if (!hand.handId || !hand.hero || !hand.tournamentId || !Number.isFinite(hand.heroChips)) continue;
    const key = `${hand.hero}::${hand.tournamentId}`;
    if (!groups.has(key)) groups.set(key, []);
    groups.get(key).push(hand);
  }

  for (const group of groups.values()) {
    group.sort(compareHandsChronologically);
    for (let index = 0; index < group.length - 1; index += 1) {
      const hand = group[index];
      const nextHand = group[index + 1];
      if (!Number.isFinite(nextHand.heroChips) || !Number.isFinite(hand.bigBlind) || hand.bigBlind <= 0) continue;
      const chips = nextHand.heroChips - hand.heroChips;
      const evChips = Number.isFinite(hand.evChips) ? hand.evChips : chips;
      profitMap.set(hand.handId, {
        chips,
        bb: chips / hand.bigBlind,
        evChips,
        evBb: evChips / hand.bigBlind,
      });
    }
  }

  return profitMap;
}

function compareHandsChronologically(a, b) {
  const dateCompare = (a.date || "").localeCompare(b.date || "");
  if (dateCompare !== 0) return dateCompare;
  return String(a.handId || "").localeCompare(String(b.handId || ""));
}

function buildGraphSeries(hands) {
  const profitMap = buildHandProfitMap(state.hands);
  const sorted = [...hands]
    .filter((hand) => profitMap.has(hand.handId))
    .sort(compareHandsChronologically);
  let net = 0;
  let ev = 0;
  let showdown = 0;
  let nonShowdown = 0;

  return sorted.map((hand, index) => {
    const profit = profitMap.get(hand.handId);
    net += profit.bb;
    ev += profit.evBb;
    if (hand.showedDown) showdown += profit.bb;
    else nonShowdown += profit.bb;
    return {
      index: index + 1,
      net,
      ev,
      showdown,
      nonShowdown,
    };
  });
}

function renderResultsGraph(hands) {
  if (!els.resultsGraph || !els.graphSummary) return;
  const canvas = els.resultsGraph;
  const wrapper = canvas.parentElement;
  if (!wrapper || !wrapper.clientWidth) return;

  const series = buildGraphSeries(hands);
  const width = Math.max(720, wrapper.clientWidth);
  const height = Math.max(420, Math.round(width * 0.47));
  const dpr = window.devicePixelRatio || 1;
  canvas.style.width = `${width}px`;
  canvas.style.height = `${height}px`;
  canvas.width = Math.round(width * dpr);
  canvas.height = Math.round(height * dpr);

  const ctx = canvas.getContext("2d");
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, width, height);
  ctx.fillStyle = "#202326";
  ctx.fillRect(0, 0, width, height);

  if (!series.length) {
    drawEmptyGraph(ctx, width, height);
    els.graphSummary.innerHTML = `
      <span><strong>0</strong> maos</span>
      <span><strong>0.0</strong> bb/100</span>
      <span><strong>0.0</strong> EV bb/100</span>
    `;
    return;
  }

  const pad = { left: 54, right: 58, top: 26, bottom: 44 };
  const plotW = width - pad.left - pad.right;
  const plotH = height - pad.top - pad.bottom;
  let { minY, maxY } = graphBounds(series);
  if (minY === maxY) {
    minY -= 5;
    maxY += 5;
  }
  const margin = (maxY - minY) * 0.08;
  minY -= margin;
  maxY += margin;

  const x = (index) => pad.left + ((index - 1) / Math.max(1, series.length - 1)) * plotW;
  const y = (value) => pad.top + ((maxY - value) / (maxY - minY)) * plotH;

  drawGraphGrid(ctx, width, height, pad, minY, maxY, series.length, x, y);
  const drawableSeries = sampleGraphSeries(series, Math.max(900, Math.round(plotW * 1.8)));
  drawLine(ctx, drawableSeries, "net", x, y, "#00c783", 2.2);
  drawLine(ctx, drawableSeries, "ev", x, y, "#f5ec3d", 2);
  drawLine(ctx, drawableSeries, "showdown", x, y, "#3ea2ff", 2);
  drawLine(ctx, drawableSeries, "nonShowdown", x, y, "#e83f78", 2);

  const last = series[series.length - 1];
  const bb100 = (last.net / series.length) * 100;
  const evBb100 = (last.ev / series.length) * 100;
  els.graphSummary.innerHTML = `
    <span><strong>${fmt(series.length)}</strong> maos</span>
    <span><strong class="${last.net >= 0 ? "number-positive" : "number-negative"}">${last.net.toFixed(1)}</strong> BB</span>
    <span><strong class="${last.ev >= 0 ? "number-positive" : "number-negative"}">${last.ev.toFixed(1)}</strong> EV BB</span>
    <span><strong class="${bb100 >= 0 ? "number-positive" : "number-negative"}">${bb100.toFixed(1)}</strong> bb/100</span>
    <span><strong class="${evBb100 >= 0 ? "number-positive" : "number-negative"}">${evBb100.toFixed(1)}</strong> EV bb/100</span>
  `;
}

function graphBounds(series) {
  let minY = 0;
  let maxY = 0;
  for (const point of series) {
    if (point.net < minY) minY = point.net;
    if (point.ev < minY) minY = point.ev;
    if (point.showdown < minY) minY = point.showdown;
    if (point.nonShowdown < minY) minY = point.nonShowdown;
    if (point.net > maxY) maxY = point.net;
    if (point.ev > maxY) maxY = point.ev;
    if (point.showdown > maxY) maxY = point.showdown;
    if (point.nonShowdown > maxY) maxY = point.nonShowdown;
  }
  return { minY, maxY };
}

function sampleGraphSeries(series, maxPoints) {
  if (series.length <= maxPoints) return series;
  const sampled = [];
  const step = series.length / maxPoints;
  let next = 0;
  for (let index = 0; index < series.length; index += 1) {
    if (index >= Math.floor(next)) {
      sampled.push(series[index]);
      next += step;
    }
  }
  const last = series[series.length - 1];
  if (sampled[sampled.length - 1] !== last) sampled.push(last);
  return sampled;
}

function drawGraphGrid(ctx, width, height, pad, minY, maxY, handCount, x, y) {
  ctx.save();
  ctx.strokeStyle = "#343a40";
  ctx.lineWidth = 1;
  ctx.fillStyle = "#8d979d";
  ctx.font = "12px Inter, sans-serif";
  ctx.textAlign = "right";
  ctx.textBaseline = "middle";

  for (let step = 0; step <= 8; step += 1) {
    const value = minY + ((maxY - minY) / 8) * step;
    const yy = y(value);
    ctx.beginPath();
    ctx.moveTo(pad.left, yy);
    ctx.lineTo(width - pad.right, yy);
    ctx.stroke();
    ctx.fillText(value.toFixed(0), width - 12, yy);
  }

  ctx.strokeStyle = "#596269";
  ctx.beginPath();
  ctx.moveTo(pad.left, y(0));
  ctx.lineTo(width - pad.right, y(0));
  ctx.stroke();

  ctx.textAlign = "center";
  ctx.textBaseline = "top";
  for (let step = 0; step <= 4; step += 1) {
    const hand = Math.round(1 + ((handCount - 1) / 4) * step);
    const xx = x(hand);
    ctx.strokeStyle = "#2d3338";
    ctx.beginPath();
    ctx.moveTo(xx, pad.top);
    ctx.lineTo(xx, height - pad.bottom);
    ctx.stroke();
    ctx.fillText(fmt(hand), xx, height - pad.bottom + 12);
  }
  ctx.restore();
}

function drawLine(ctx, series, key, x, y, color, width) {
  ctx.save();
  ctx.strokeStyle = color;
  ctx.lineWidth = width;
  ctx.lineJoin = "round";
  ctx.lineCap = "round";
  ctx.beginPath();
  series.forEach((point, index) => {
    const xx = x(point.index);
    const yy = y(point[key]);
    if (index === 0) ctx.moveTo(xx, yy);
    else ctx.lineTo(xx, yy);
  });
  ctx.stroke();
  ctx.restore();
}

function drawEmptyGraph(ctx, width, height) {
  ctx.save();
  ctx.strokeStyle = "#343a40";
  for (let y = 60; y < height - 40; y += 32) {
    ctx.beginPath();
    ctx.moveTo(54, y);
    ctx.lineTo(width - 58, y);
    ctx.stroke();
  }
  ctx.fillStyle = "#9aa6ad";
  ctx.font = "15px Inter, sans-serif";
  ctx.textAlign = "center";
  ctx.fillText("Importe maos para gerar o grafico.", width / 2, height / 2);
  ctx.restore();
}

function renderReplayer(hands) {
  if (!els.tournamentList || !els.replayHandList || !els.replayTable) return;
  if (els.replayStackUnit && els.replayStackUnit.value !== state.replayStackUnit) {
    els.replayStackUnit.value = state.replayStackUnit;
  }
  const context = buildReplayContext(hands);
  currentReplayContext = context;
  renderReplayTournamentList(context.tournaments);
  renderReplayHandList(context.tournamentHands);
  renderReplayStage(context.selectedHand, context.selected, context.tournamentHands);
}

function buildReplayContext(hands) {
  const tournaments = buildReplayTournaments(filterReplayHandsByDate(hands));
  ensureReplaySelection(tournaments);
  const selected = tournaments.find((tournament) => tournament.id === state.selectedTournamentId);
  const tournamentHands = selected ? replayFilteredHands(selected.hands) : [];
  if (!state.selectedReplayHandId || !tournamentHands.some((hand) => hand.handId === state.selectedReplayHandId)) {
    state.selectedReplayHandId = tournamentHands[0]?.handId || "";
    state.selectedReplayActionIndex = 0;
  }

  const selectedHand = tournamentHands.find((hand) => hand.handId === state.selectedReplayHandId) || selected?.hands[0] || null;
  return { tournaments, selected, tournamentHands, selectedHand };
}

function renderReplayStageOnly() {
  if (!currentReplayContext) currentReplayContext = buildReplayContext(filteredHands());
  const handStillSelected = currentReplayContext.tournamentHands.some((hand) => hand.handId === state.selectedReplayHandId);
  if (!handStillSelected) currentReplayContext = buildReplayContext(filteredHands());
  renderReplayStage(currentReplayContext.selectedHand, currentReplayContext.selected, currentReplayContext.tournamentHands);
}

function renderReplayOnly() {
  currentReplayContext = null;
  renderReplayer(filteredHands());
}

function showStatPopup(key) {
  const entry = statPopupRegistry.get(key);
  if (!entry || !els.statPopup) return;
  statPopupUi.key = key;
  statPopupUi.tab = entry.decisionSpot ? (entry.defaultAction || "raise") : "range";
  statPopupUi.filterType = "";
  statPopupUi.filterValue = "";
  statPopupUi.sourceHands = [];
  statPopupUi.filteredHands = [];
  statPopupUi.selectedHandId = "";
  statPopupUi.actionIndex = 0;
  statPopupUi.listLimit = 250;
  if (els.statPopupTitle) els.statPopupTitle.textContent = entry.title;
  if (els.statPopupMeta) {
    const opportunities = Number(entry.opportunities) || entry.hands.length;
    els.statPopupMeta.textContent = `${fmt(entry.hands.length)} ocorrencias em ${fmt(opportunities)} oportunidades`;
  }
  els.statPopupHandsPanel.hidden = true;
  els.statPopupReplayPanel.hidden = true;
  els.statPopupWorkspace?.classList.remove("has-hands", "has-replay");
  updateStatPopupTabs(entry);
  renderStatPopupSummary(entry);
  renderStatPopupContent();
  els.statPopup.hidden = false;
  document.body?.classList.add("stat-popup-open");
}

function closeStatPopup() {
  if (els.statPopup) els.statPopup.hidden = true;
  document.body?.classList.remove("stat-popup-open");
  statPopupUi.key = "";
  statPopupUi.filteredHands = [];
  statPopupUi.selectedHandId = "";
}

function activeStatPopupEntry() {
  return statPopupRegistry.get(statPopupUi.key) || null;
}

function statHandProfit(hand) {
  if (!Number.isFinite(hand?.netChips) || !Number.isFinite(hand?.bigBlind) || hand.bigBlind <= 0) return null;
  return {
    chips: hand.netChips,
    bb: hand.netChips / hand.bigBlind,
  };
}

function renderStatPopupSummary(entry) {
  if (!els.statPopupSummary) return;
  const hands = entry.hands || [];
  const resultHands = hands.map(statHandProfit).filter(Boolean);
  const netBb = resultHands.reduce((sum, profit) => sum + profit.bb, 0);
  const won = hands.filter(wonHand).length;
  const sawFlop = hands.filter((hand) => hand.sawFlop).length;
  const showdown = hands.filter((hand) => hand.showedDown).length;
  const wonShowdown = hands.filter((hand) => hand.wonShowdown).length;
  const opportunities = Number(entry.opportunities) || hands.length;
  const frequency = Number.isFinite(entry.value) ? `${entry.value}%` : ratio(hands.length, opportunities);
  const bb100 = resultHands.length ? (netBb / resultHands.length) * 100 : null;
  const items = [
    ["Stat", frequency],
    ["Amostra", fmt(hands.length)],
    ["Won hand", ratio(won, hands.length)],
    ["WTSD", ratio(showdown, sawFlop)],
    ["W$SD", ratio(wonShowdown, showdown)],
    ["bb/100", bb100 === null ? "-" : `${bb100 >= 0 ? "+" : ""}${bb100.toFixed(1)}`],
  ];
  els.statPopupSummary.innerHTML = items.map(([label, value]) => `<div>
    <span>${escapeHtml(label)}</span>
    <strong>${escapeHtml(value)}</strong>
  </div>`).join("");
}

const RANGE_RANKS = ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"];

function statHandCombo(hand) {
  const signature = String(hand?.heroCards || "");
  const cached = statHandComboCache.get(hand?.handId);
  if (cached?.signature === signature) return cached.combo;
  const cards = parseCardList(signature);
  if (cards.length !== 2) return "";
  const sorted = [...cards].sort((a, b) => RANGE_RANKS.indexOf(a[0]) - RANGE_RANKS.indexOf(b[0]));
  const firstRank = sorted[0][0];
  const secondRank = sorted[1][0];
  const combo = firstRank === secondRank
    ? `${firstRank}${secondRank}`
    : `${firstRank}${secondRank}${sorted[0][1] === sorted[1][1] ? "s" : "o"}`;
  statHandComboCache.set(hand?.handId, { signature, combo });
  return combo;
}

function statDecisionAction(hand, decisionSpot) {
  if (!hand?.raw || !hand?.hero || !decisionSpot) return "";
  const signature = `${hand.handId || ""}|${String(hand.raw).length}|${decisionSpot}`;
  if (statDecisionCache.has(signature)) return statDecisionCache.get(signature);
  const lines = String(hand.raw).split("\n").map((line) => line.trim()).filter(Boolean);
  const actions = parseActionsFromLines(parsePreflop(lines));
  const raises = actions
    .map((action, index) => ({ action, index }))
    .filter((item) => item.action.verb === "raises");
  let afterIndex = -1;
  if (decisionSpot === "after-open") afterIndex = raises[0]?.index ?? -1;
  if (decisionSpot === "after-threebet") afterIndex = raises[1]?.index ?? -1;
  const decision = actions.find((action, index) => (
    index > afterIndex
    && action.player === hand.hero
    && ["folds", "calls", "raises"].includes(action.verb)
  ));
  const category = decision?.verb === "folds"
    ? "fold"
    : decision?.verb === "calls"
      ? "call"
      : decision?.verb === "raises"
        ? "raise"
        : "";
  statDecisionCache.set(signature, category);
  return category;
}

function statDecisionHands(entry, action) {
  if (!entry?.decisionSpot || !Array.isArray(entry.opportunityHands)) return [];
  return entry.opportunityHands.filter((hand) => statDecisionAction(hand, entry.decisionSpot) === action);
}

function setLimitedCache(cache, key, value, limit) {
  cache.set(key, value);
  if (cache.size > limit) cache.delete(cache.keys().next().value);
  return value;
}

function statActionTimeline(hand) {
  if (!hand?.raw) return null;
  const signature = `${hand.handId || ""}|${String(hand.raw).length}|${String(hand.raw).slice(0, 24)}`;
  const cached = statActionTimelineCache.get(signature);
  if (cached) return cached;
  const lines = String(hand.raw).split("\n").map((line) => line.trim()).filter(Boolean);
  const boards = {
    Preflop: [],
    Flop: parseBoardFromLine(lines.find((line) => line.startsWith("*** FLOP ***") || line.startsWith("** Dealing Flop **")) || ""),
    Turn: parseBoardFromLine(lines.find((line) => line.startsWith("*** TURN ***") || line.startsWith("** Dealing Turn **")) || ""),
    River: parseBoardFromLine(lines.find((line) => line.startsWith("*** RIVER ***") || line.startsWith("** Dealing River **")) || ""),
  };
  const actions = [];
  const streetPaid = new Map();
  let street = "Preflop";
  let pot = 0;
  for (const line of lines) {
    if (line.startsWith("*** FLOP ***") || line.startsWith("** Dealing Flop **")) {
      street = "Flop";
      streetPaid.clear();
      continue;
    }
    if (line.startsWith("*** TURN ***") || line.startsWith("** Dealing Turn **")) {
      street = "Turn";
      streetPaid.clear();
      continue;
    }
    if (line.startsWith("*** RIVER ***") || line.startsWith("** Dealing River **")) {
      street = "River";
      streetPaid.clear();
      continue;
    }
    const action = parseActionLine(line);
    const potBefore = Math.max(0, pot);
    const contribution = replayMoneyChange(line, action, streetPaid);
    pot += contribution;
    if (action) actions.push({ ...action, street, potBefore, contribution });
  }
  return setLimitedCache(statActionTimelineCache, signature, { actions, boards }, 10000);
}

function statTimelineDecision(timeline, hero, decisionSpot) {
  if (!timeline || !hero || !decisionSpot) return null;
  const preflop = timeline.actions.filter((action) => action.street === "Preflop");
  const raises = preflop
    .map((action, index) => ({ action, index }))
    .filter((item) => item.action.verb === "raises");
  let afterIndex = -1;
  if (decisionSpot === "after-open") afterIndex = raises[0]?.index ?? -1;
  if (decisionSpot === "after-threebet") afterIndex = raises[1]?.index ?? -1;
  return preflop.find((action, index) => (
    index > afterIndex
    && action.player === hero
    && ["folds", "calls", "raises"].includes(action.verb)
  )) || null;
}

function statBetStreet(key) {
  if (/skip-cb/i.test(key)) return "Flop";
  if (/flop/i.test(key)) return "Flop";
  if (/turn/i.test(key)) return "Turn";
  if (/river/i.test(key)) return "River";
  return "";
}

function statTriggerRaise(timeline, decisionSpot) {
  const raises = timeline?.actions.filter((action) => action.street === "Preflop" && action.verb === "raises") || [];
  if (decisionSpot === "after-threebet") return raises[1] || null;
  if (decisionSpot === "after-open") return raises[0] || null;
  return null;
}

function statPostflopBetAction(timeline, hand, key, street) {
  const actions = timeline?.actions.filter((action) => action.street === street) || [];
  const isFacedBet = /fold|(?:^|-)xf(?:-|$)/i.test(key);
  if (isFacedBet) {
    const responseIndex = actions.findIndex((action) => action.player === hand.hero && ["folds", "calls", "raises"].includes(action.verb));
    if (responseIndex < 0) return null;
    return [...actions.slice(0, responseIndex)].reverse().find((action) => (
      action.player !== hand.hero && ["bets", "raises"].includes(action.verb)
    )) || null;
  }
  return actions.find((action) => action.player === hand.hero && ["bets", "raises"].includes(action.verb)) || null;
}

function rankValue(card) {
  return "23456789TJQKA".indexOf(card?.[0]) + 2;
}

function straightDrawMissingRanks(heroCards, boardCards) {
  const combinedRanks = new Set([...heroCards, ...boardCards].map(rankValue));
  const heroRanks = new Set(heroCards.map(rankValue));
  const windows = [
    [14, 13, 12, 11, 10], [13, 12, 11, 10, 9], [12, 11, 10, 9, 8], [11, 10, 9, 8, 7],
    [10, 9, 8, 7, 6], [9, 8, 7, 6, 5], [8, 7, 6, 5, 4], [7, 6, 5, 4, 3],
    [6, 5, 4, 3, 2], [14, 5, 4, 3, 2],
  ];
  const missing = new Set();
  for (const windowRanks of windows) {
    const absent = windowRanks.filter((rank) => !combinedRanks.has(rank));
    const heroContributes = windowRanks.some((rank) => heroRanks.has(rank));
    if (absent.length === 1 && heroContributes) missing.add(absent[0]);
  }
  return missing;
}

const STAT_STRENGTH_BUCKETS = [
  { id: "two-plus", label: "2 pares+" },
  { id: "top-strong", label: "Top pair forte" },
  { id: "pair-weak", label: "Par medio/fraco" },
  { id: "draw-strong", label: "Draw forte" },
  { id: "draw-weak", label: "Draw fraco" },
  { id: "ace-high", label: "A high" },
  { id: "trash", label: "Trash" },
];

function highCardOrDrawStrength(heroCards, boardCards, street) {
  if (street !== "River") {
    const suitCounts = new Map();
    for (const card of [...heroCards, ...boardCards]) suitCounts.set(card[1], (suitCounts.get(card[1]) || 0) + 1);
    const flushDraw = [...suitCounts.entries()].some(([suit, count]) => count === 4 && heroCards.some((card) => card[1] === suit));
    const straightMissing = straightDrawMissingRanks(heroCards, boardCards);
    if (flushDraw || straightMissing.size >= 2) return "draw-strong";
    if (straightMissing.size === 1) return "draw-weak";
  }
  return heroCards.some((card) => card[0] === "A") ? "ace-high" : "trash";
}

function classifyStatHandStrength(hand, boardCards, street) {
  const heroCards = parseCardList(hand?.heroCards);
  if (heroCards.length !== 2 || boardCards.length < 3) return "trash";
  const category = Math.floor(evaluateBestHoldem([...heroCards, ...boardCards]) / (15 ** 5));
  if (category >= 2) return "two-plus";
  if (category === 1) {
    const boardRanks = boardCards.map(rankValue);
    const heroRanks = heroCards.map(rankValue);
    const topBoardRank = Math.max(...boardRanks);
    const isPocketPair = heroRanks[0] === heroRanks[1];
    const pairedHeroIndexes = heroRanks.map((rank, index) => boardRanks.includes(rank) ? index : -1).filter((index) => index >= 0);
    if (isPocketPair && heroRanks[0] > topBoardRank) return "top-strong";
    const topPairIndex = pairedHeroIndexes.find((index) => heroRanks[index] === topBoardRank);
    if (topPairIndex !== undefined) {
      const kicker = heroRanks[topPairIndex === 0 ? 1 : 0];
      return kicker >= 10 ? "top-strong" : "pair-weak";
    }
    if (isPocketPair || pairedHeroIndexes.length) return "pair-weak";
    return highCardOrDrawStrength(heroCards, boardCards, street);
  }
  return highCardOrDrawStrength(heroCards, boardCards, street);
}

function statBetSizeRecord(hand, entry, key = statPopupUi.key) {
  const cacheKey = `${hand?.handId || ""}|${key}`;
  if (statBetSizeCache.has(cacheKey)) return statBetSizeCache.get(cacheKey);
  const timeline = statActionTimeline(hand);
  if (!timeline) return null;
  let action = null;
  let street = "Preflop";
  if (entry?.decisionSpot) {
    const decision = statTimelineDecision(timeline, hand.hero, entry.decisionSpot);
    action = entry.defaultAction === "raise" && decision?.verb === "raises"
      ? decision
      : statTriggerRaise(timeline, entry.decisionSpot);
  } else {
    street = statBetStreet(key);
    if (!street) return null;
    action = statPostflopBetAction(timeline, hand, key, street);
  }
  if (!action) return null;
  const preflop = street === "Preflop";
  const sizeValue = preflop
    ? Number(action.amount) / Number(hand.bigBlind)
    : Number(action.contribution) / Number(action.potBefore) * 100;
  if (!Number.isFinite(sizeValue) || sizeValue <= 0) return null;
  const record = {
    hand,
    street,
    sizeValue,
    strength: preflop ? "" : classifyStatHandStrength(hand, timeline.boards[street] || [], street),
  };
  return setLimitedCache(statBetSizeCache, cacheKey, record, 30000);
}

function statBetSizeDefinitions(entry, preflop) {
  if (!preflop) return [
    ["bet-20", "Bet <= 20%", 20], ["bet-39", "Bet 21-39%", 39], ["bet-59", "Bet 40-59%", 59],
    ["bet-79", "Bet 60-79%", 79], ["bet-100", "Bet 80-100%", 100], ["bet-150", "Bet 101-150%", 150],
    ["bet-151", "Bet > 150%", Infinity],
  ];
  if (entry?.betSizeMode === "open") return [
    ["open-20", "Open <= 2bb", 2], ["open-24", "Open 2.1-2.4bb", 2.4],
    ["open-29", "Open 2.5-2.9bb", 2.9], ["open-30", "Open 3bb+", Infinity],
  ];
  return [
    ["raise-50", "Raise <= 5bb", 5], ["raise-70", "Raise 5.1-7bb", 7], ["raise-90", "Raise 7.1-9bb", 9],
    ["raise-120", "Raise 9.1-12bb", 12], ["raise-121", "Raise > 12bb", Infinity],
  ];
}

function statBetSizeBucket(record, entry) {
  if (!record) return null;
  const definitions = statBetSizeDefinitions(entry, record.street === "Preflop");
  return definitions.find((definition) => record.sizeValue <= definition[2]) || definitions[definitions.length - 1];
}

function statStrengthBar(records) {
  if (!records.length || !records.some((record) => record.strength)) return `<span class="stat-strength-empty">-</span>`;
  const counts = new Map(STAT_STRENGTH_BUCKETS.map((bucket) => [bucket.id, 0]));
  for (const record of records) counts.set(record.strength, (counts.get(record.strength) || 0) + 1);
  return `<span class="stat-strength-bar">${STAT_STRENGTH_BUCKETS.map((bucket) => {
    const count = counts.get(bucket.id) || 0;
    const width = (count / records.length) * 100;
    return count ? `<i class="strength-${bucket.id}" style="width:${width.toFixed(2)}%" title="${escapeAttr(bucket.label)} ${Math.round(width)}%"></i>` : "";
  }).join("")}</span>`;
}

function renderStatBetSizes(entry) {
  const records = entry.hands.map((hand) => statBetSizeRecord(hand, entry)).filter(Boolean);
  if (!records.length) return `<div class="empty-note">Sem apostas mensuraveis nesta stat.</div>`;
  const preflop = records[0].street === "Preflop";
  const definitions = statBetSizeDefinitions(entry, preflop);
  const rows = definitions.map((definition) => ({
    definition,
    records: records.filter((record) => statBetSizeBucket(record, entry)?.[0] === definition[0]),
  }));
  const legend = preflop ? "" : `<div class="stat-strength-legend">${STAT_STRENGTH_BUCKETS.map((bucket) => `<span><i class="strength-${bucket.id}"></i>${escapeHtml(bucket.label)}</span>`).join("")}</div>`;
  return `${legend}<div class="stat-bet-size-table">
    <div class="stat-bet-size-head"><span>Bet size</span><span>Freq.</span><span>bb/hand</span><span>Range</span><span>Maos</span></div>
    ${rows.map(({ definition, records: bucketRecords }) => {
      const profits = bucketRecords.map((record) => statHandProfit(record.hand)).filter(Boolean);
      const bbPerHand = profits.length ? profits.reduce((sum, profit) => sum + profit.bb, 0) / profits.length : null;
      const frequency = records.length ? (bucketRecords.length / records.length) * 100 : 0;
      return `<button class="stat-bet-size-row" data-stat-bet-bucket="${escapeAttr(definition[0])}" type="button" ${bucketRecords.length ? "" : "disabled"}>
        <span>${escapeHtml(definition[1])}</span>
        <strong>${frequency < 10 && frequency > 0 ? frequency.toFixed(1) : Math.round(frequency)}%</strong>
        <em class="${bbPerHand !== null && bbPerHand < 0 ? "is-negative" : ""}">${bbPerHand === null ? "-" : `${bbPerHand >= 0 ? "+" : ""}${bbPerHand.toFixed(2)}`}</em>
        ${statStrengthBar(bucketRecords)}
        <small>${fmt(bucketRecords.length)}</small>
      </button>`;
    }).join("")}
  </div>`;
}

function updateStatPopupTabs(entry) {
  const hasDecisionRanges = Boolean(entry?.decisionSpot && Array.isArray(entry.opportunityHands));
  els.statPopupTabs?.querySelectorAll("[data-stat-decision-tab]").forEach((button) => {
    button.hidden = !hasDecisionRanges;
  });
  const rangeTab = els.statPopupTabs?.querySelector("[data-stat-range-tab]");
  if (rangeTab) rangeTab.hidden = hasDecisionRanges;
}

function rangeComboForCell(rowIndex, columnIndex) {
  const rowRank = RANGE_RANKS[rowIndex];
  const columnRank = RANGE_RANKS[columnIndex];
  if (rowIndex === columnIndex) return `${rowRank}${columnRank}`;
  return rowIndex < columnIndex ? `${rowRank}${columnRank}s` : `${columnRank}${rowRank}o`;
}

function renderStatRange(hands) {
  const counts = new Map();
  for (const hand of hands) {
    const combo = statHandCombo(hand);
    if (combo) counts.set(combo, (counts.get(combo) || 0) + 1);
  }
  const maxCount = Math.max(1, ...counts.values());
  return `<div class="stat-range-grid">${RANGE_RANKS.flatMap((_, rowIndex) => RANGE_RANKS.map((__, columnIndex) => {
    const combo = rangeComboForCell(rowIndex, columnIndex);
    const count = counts.get(combo) || 0;
    const alpha = count ? (0.22 + (count / maxCount) * 0.68).toFixed(2) : "0";
    return `<button class="stat-range-cell ${count ? "has-hands" : ""}" data-stat-range="${escapeAttr(combo)}" style="--range-alpha:${alpha}" type="button" ${count ? "" : "disabled"}>
      <span>${escapeHtml(combo)}</span><strong>${count ? fmt(count) : ""}</strong>
    </button>`;
  })).join("")}</div>`;
}

function renderStatDistribution(rows, filterType) {
  const total = rows.reduce((sum, row) => sum + row.count, 0);
  if (!total) return `<div class="empty-note">Sem dados suficientes nesta amostra.</div>`;
  const max = Math.max(...rows.map((row) => row.count), 1);
  return `<div class="stat-distribution">${rows.filter((row) => row.count).map((row) => `<button data-stat-filter-type="${escapeAttr(filterType)}" data-stat-filter-value="${escapeAttr(row.value)}" type="button">
    <span>${escapeHtml(row.label)}</span>
    <i><b style="width:${Math.max(3, (row.count / max) * 100).toFixed(1)}%"></b></i>
    <strong>${ratio(row.count, total)}</strong>
    <small>${fmt(row.count)}</small>
  </button>`).join("")}</div>`;
}

function renderStatPositions(hands) {
  const positions = ["UTG", "UTG1", "MP", "LJ", "HJ", "CO", "BTN", "SB", "BB"];
  return renderStatDistribution(positions.map((position) => ({
    label: position,
    value: position,
    count: hands.filter((hand) => reportPosition(hand) === position).length,
  })), "position");
}

function statStackBucket(stackBb) {
  const stack = Number(stackBb);
  if (!Number.isFinite(stack)) return "unknown";
  if (stack < 8) return "0-8";
  if (stack < 15) return "8-15";
  if (stack < 25) return "15-25";
  if (stack < 40) return "25-40";
  return "40+";
}

function renderStatStacks(hands) {
  const buckets = [
    ["0-8bb", "0-8"],
    ["8-15bb", "8-15"],
    ["15-25bb", "15-25"],
    ["25-40bb", "25-40"],
    ["40bb+", "40+"],
  ];
  return renderStatDistribution(buckets.map(([label, value]) => ({
    label,
    value,
    count: hands.filter((hand) => statStackBucket(hand.stackBb) === value).length,
  })), "stack");
}

function renderStatPopupContent() {
  const entry = activeStatPopupEntry();
  if (!entry || !els.statPopupContent) return;
  els.statPopupTabs?.querySelectorAll("[data-stat-popup-tab]").forEach((button) => {
    button.classList.toggle("is-active", button.dataset.statPopupTab === statPopupUi.tab);
  });
  if (["fold", "raise", "call"].includes(statPopupUi.tab) && entry.decisionSpot) {
    statPopupUi.sourceHands = statDecisionHands(entry, statPopupUi.tab);
    els.statPopupContent.innerHTML = renderStatRange(statPopupUi.sourceHands);
  } else if (statPopupUi.tab === "bet-sizes") {
    statPopupUi.sourceHands = entry.hands;
    els.statPopupContent.innerHTML = renderStatBetSizes(entry);
  } else if (statPopupUi.tab === "positions") {
    statPopupUi.sourceHands = entry.hands;
    els.statPopupContent.innerHTML = renderStatPositions(entry.hands);
  } else if (statPopupUi.tab === "stacks") {
    statPopupUi.sourceHands = entry.hands;
    els.statPopupContent.innerHTML = renderStatStacks(entry.hands);
  } else {
    statPopupUi.sourceHands = entry.hands;
    els.statPopupContent.innerHTML = renderStatRange(entry.hands);
  }
}

function filteredStatHands(hands, filterType = "", filterValue = "") {
  if (!Array.isArray(hands)) return [];
  const filtered = hands.filter((hand) => {
    if (filterType === "combo") return statHandCombo(hand) === filterValue;
    if (filterType === "position") return reportPosition(hand) === filterValue;
    if (filterType === "stack") return statStackBucket(hand.stackBb) === filterValue;
    if (filterType === "bet-size") {
      const entry = activeStatPopupEntry();
      const record = statBetSizeRecord(hand, entry);
      return statBetSizeBucket(record, entry)?.[0] === filterValue;
    }
    return true;
  });
  return [...filtered].sort(compareHandsChronologically).reverse();
}

function statFilterTitle(type, value) {
  const action = ["fold", "raise", "call"].includes(statPopupUi.tab)
    ? `${statPopupUi.tab[0].toUpperCase()}${statPopupUi.tab.slice(1)} - `
    : "";
  if (type === "combo") return `${action}Range ${value}`;
  if (type === "position") return `${action}Posicao ${value}`;
  if (type === "stack") return `${action}Stack ${value}bb`;
  if (type === "bet-size") {
    const entry = activeStatPopupEntry();
    const sampleRecord = entry?.hands.map((hand) => statBetSizeRecord(hand, entry)).find(Boolean);
    const definition = statBetSizeDefinitions(entry, sampleRecord?.street === "Preflop").find((item) => item[0] === value);
    return definition?.[1] || "Bet size";
  }
  return `${action}Todas as maos`;
}

function openStatHands(filterType = "", filterValue = "") {
  const entry = activeStatPopupEntry();
  if (!entry) return;
  statPopupUi.filterType = filterType;
  statPopupUi.filterValue = filterValue;
  statPopupUi.filteredHands = filteredStatHands(statPopupUi.sourceHands, filterType, filterValue);
  statPopupUi.listLimit = 250;
  statPopupUi.selectedHandId = "";
  statPopupUi.actionIndex = 0;
  els.statPopupHandsPanel.hidden = false;
  els.statPopupReplayPanel.hidden = true;
  els.statPopupWorkspace?.classList.add("has-hands");
  els.statPopupWorkspace?.classList.remove("has-replay");
  if (els.statPopupHandsTitle) els.statPopupHandsTitle.textContent = statFilterTitle(filterType, filterValue);
  renderStatHandList();
}

function renderStatHandList() {
  if (!els.statPopupList) return;
  const hands = statPopupUi.filteredHands;
  const shownHands = hands.slice(0, statPopupUi.listLimit || 250);
  if (els.statPopupHandsMeta) {
    els.statPopupHandsMeta.textContent = hands.length > shownHands.length
      ? `${fmt(hands.length)} maos - mostrando ${fmt(shownHands.length)}`
      : `${fmt(hands.length)} maos`;
  }
  const rows = shownHands.map((hand) => {
    const profit = statHandProfit(hand);
    const outcomeClass = replayHandOutcomeClass(hand, profit);
    const active = hand.handId === statPopupUi.selectedHandId ? "is-active" : "";
    const result = profit ? `${profit.bb >= 0 ? "+" : ""}${profit.bb.toFixed(1)}bb` : "-";
    const board = statHandBoardHtml(hand);
    return `<button class="stat-popup-hand ${outcomeClass} ${active}" data-popup-hand-id="${escapeAttr(hand.handId)}" type="button">
      <strong><span>${escapeHtml(hand.heroCards || "--")} - ${escapeHtml(hand.position || "-")}</span><em>${escapeHtml(result)}</em></strong>
      <div class="stat-popup-hand-middle"><span>${escapeHtml(hand.preflopSummary || "sem acao")}</span>${board}</div>
      <small><span>${escapeHtml(hand.date || "")}</span><em>#${escapeHtml(hand.handId)}</em></small>
    </button>`;
  }).join("");
  const loadMore = hands.length > shownHands.length
    ? `<button class="stat-load-more" data-stat-load-more type="button">Mostrar mais</button>`
    : "";
  els.statPopupList.innerHTML = rows || `<div class="empty-note">Nenhuma mao encontrada neste filtro.</div>`;
  els.statPopupList.insertAdjacentHTML("beforeend", loadMore);
}

function statHandBoardHtml(hand) {
  const boards = statActionTimeline(hand)?.boards;
  if (!boards) return "";
  const cards = boards.River?.length ? boards.River : boards.Turn?.length ? boards.Turn : boards.Flop || [];
  if (!cards.length) return "";
  return `<div class="stat-hand-board" aria-label="Board ${escapeAttr(cards.join(" "))}">${cards.map((card) => {
    const rank = card?.[0] || "";
    const suit = card?.[1] || "";
    const suitSymbol = { c: "&clubs;", d: "&diams;", h: "&hearts;", s: "&spades;" }[suit] || "";
    const color = { c: "mini-club", d: "mini-diamond", h: "mini-heart", s: "mini-spade" }[suit] || "";
    return `<i class="mini-board-card ${color}"><b>${escapeHtml(rank)}</b><b>${suitSymbol}</b></i>`;
  }).join("")}</div>`;
}

function openStatReplay(handId) {
  const hand = statPopupUi.filteredHands.find((item) => item.handId === handId);
  if (!hand) return;
  statPopupUi.selectedHandId = handId;
  statPopupUi.actionIndex = 0;
  els.statPopupReplayPanel.hidden = false;
  els.statPopupWorkspace?.classList.add("has-replay");
  renderStatHandList();
  renderStatReplay();
}

function renderStatReplay() {
  const hand = statPopupUi.filteredHands.find((item) => item.handId === statPopupUi.selectedHandId);
  if (!hand || !els.statPopupReplayTable) return;
  const replay = getReplayHand(hand);
  const actionCount = replay.actions.length;
  statPopupUi.actionIndex = clamp(statPopupUi.actionIndex, 0, Math.max(0, actionCount - 1));
  if (els.statPopupReplayTitle) {
    const heroStack = formatReplayStack(hand.heroChips, hand.bigBlind, statPopupUi.stackUnit);
    els.statPopupReplayTitle.textContent = `${hand.heroCards || "--"} - ${hand.position || "-"} - ${heroStack}`;
  }
  if (els.statPopupReplayMeta) {
    els.statPopupReplayMeta.textContent = `${hand.date || ""} - Mao #${hand.handId}`;
  }
  if (els.statPopupReplayStep) {
    els.statPopupReplayStep.textContent = actionCount ? `${statPopupUi.actionIndex + 1} / ${actionCount}` : "0 / 0";
  }
  if (els.statPopupReplayUnit && els.statPopupReplayUnit.value !== statPopupUi.stackUnit) {
    els.statPopupReplayUnit.value = statPopupUi.stackUnit;
  }
  renderReplayTable(replay, hand, statPopupUi.actionIndex, els.statPopupReplayTable, statPopupUi.stackUnit);
  const handIndex = statPopupUi.filteredHands.findIndex((item) => item.handId === hand.handId);
  const controlStates = {
    "prev-hand": handIndex <= 0,
    "next-hand": handIndex < 0 || handIndex >= statPopupUi.filteredHands.length - 1,
    "prev-action": statPopupUi.actionIndex <= 0,
    "next-action": !actionCount || statPopupUi.actionIndex >= actionCount - 1,
  };
  els.statPopupReplayPanel?.querySelectorAll("[data-stat-replay]").forEach((button) => {
    button.disabled = Boolean(controlStates[button.dataset.statReplay]);
  });
  const activeRow = els.statPopupList?.querySelector(`[data-popup-hand-id="${CSS.escape(hand.handId)}"]`);
  activeRow?.scrollIntoView?.({ block: "nearest" });
}

function changeStatReplayAction(direction) {
  const hand = statPopupUi.filteredHands.find((item) => item.handId === statPopupUi.selectedHandId);
  if (!hand) return;
  const replay = getReplayHand(hand);
  statPopupUi.actionIndex = clamp(statPopupUi.actionIndex + direction, 0, Math.max(0, replay.actions.length - 1));
  renderStatReplay();
}

function changeStatReplayHand(direction) {
  const index = statPopupUi.filteredHands.findIndex((item) => item.handId === statPopupUi.selectedHandId);
  const next = statPopupUi.filteredHands[index + direction];
  if (next) openStatReplay(next.handId);
}

function closeStatReplay() {
  statPopupUi.selectedHandId = "";
  els.statPopupReplayPanel.hidden = true;
  els.statPopupWorkspace?.classList.remove("has-replay");
  renderStatHandList();
}

function closeStatHands() {
  closeStatReplay();
  statPopupUi.filteredHands = [];
  els.statPopupHandsPanel.hidden = true;
  els.statPopupWorkspace?.classList.remove("has-hands");
}

function filterReplayHandsByDate(hands) {
  const fromRaw = els.replayDateFrom?.value || "";
  const toRaw = els.replayDateTo?.value || "";
  const from = /^\d{4}-\d{2}-\d{2}$/.test(fromRaw) ? fromRaw : "";
  const to = /^\d{4}-\d{2}-\d{2}$/.test(toRaw) ? toRaw : "";
  if (!from && !to) return hands;
  return hands.filter((hand) => {
    const key = replayDateKey(hand.date);
    if (!key) return false;
    if (from && key < from) return false;
    if (to && key > to) return false;
    return true;
  });
}

function replayDateKey(value) {
  const text = String(value || "");
  const match = text.match(/(\d{4})[/-](\d{2})[/-](\d{2})/);
  return match ? `${match[1]}-${match[2]}-${match[3]}` : "";
}

function handDateKey(value) {
  const text = String(value || "");
  const iso = text.match(/(\d{4})[/-](\d{2})[/-](\d{2})/);
  if (iso) return `${iso[1]}-${iso[2]}-${iso[3]}`;
  const local = text.match(/(\d{2})[/-](\d{2})[/-](\d{4})/);
  if (local) return `${local[3]}-${local[2]}-${local[1]}`;
  return "";
}

function todayDateKey() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function buildReplayTournaments(hands) {
  const summaries = new Map(state.tournaments.map((tournament) => [tournament.tournamentId, tournament]));
  const grouped = new Map();
  for (const hand of hands) {
    if (!grouped.has(hand.tournamentId)) grouped.set(hand.tournamentId, []);
    grouped.get(hand.tournamentId).push(hand);
  }

  return [...grouped.entries()].map(([id, tournamentHands]) => {
    const sortedHands = [...tournamentHands].sort(compareHandsChronologically);
    const summary = summaries.get(id);
    const first = sortedHands[0];
    const last = sortedHands[sortedHands.length - 1];
    const isCash = sortedHands.some((hand) => hand.isCashGame);
    const title = isCash
      ? `${first?.site || "Cash"} ${first?.buyIn || "Cash"}`
      : summary?.name || first?.buyIn || `Torneio #${id}`;
    return {
      id,
      title,
      format: isCash ? "Cash" : displayTournamentFormat(summary?.format, title, first?.buyIn || ""),
      startedAt: summary?.startedAt || first?.date || "",
      place: summary?.place || "",
      profit: Number.isFinite(summary?.profit) ? summary.profit : null,
      hands: sortedHands,
      firstDate: first?.date || "",
      lastDate: last?.date || "",
    };
  }).sort((a, b) => (b.lastDate || "").localeCompare(a.lastDate || ""));
}

function ensureReplaySelection(tournaments) {
  if (state.selectedTournamentId && tournaments.some((tournament) => tournament.id === state.selectedTournamentId)) return;
  state.selectedTournamentId = tournaments[0]?.id || "";
  state.selectedReplayHandId = "";
}

function renderReplayTournamentList(tournaments) {
  const query = (els.tournamentSearch?.value || "").trim().toLowerCase();
  const type = els.tournamentTypeFilter?.value || "all";
  const filtered = tournaments.filter((tournament) => {
    if (type !== "all" && tournament.format !== type) return false;
    if (!query) return true;
    const haystack = `${tournament.title} ${tournament.id} ${tournament.startedAt} ${tournament.format}`.toLowerCase();
    return haystack.includes(query);
  });

  if (!filtered.length) {
    els.tournamentList.innerHTML = `<div class="empty-note">Nenhum torneio encontrado.</div>`;
    return;
  }

  els.tournamentList.innerHTML = filtered.map((tournament) => {
    const active = tournament.id === state.selectedTournamentId ? "is-active" : "";
    const result = tournament.place ? `${fmt(tournament.place)}o` : tournament.profit === null ? "-" : `$${tournament.profit.toFixed(2)}`;
    return `<button class="replay-card ${active}" data-tournament-id="${escapeAttr(tournament.id)}" type="button">
      <strong>${escapeHtml(tournament.title)}</strong>
      <span>${escapeHtml(tournament.format)} Â· ${fmt(tournament.hands.length)} maos Â· ${escapeHtml(result)}</span>
      <small>#${escapeHtml(tournament.id)} Â· ${escapeHtml(tournament.firstDate || "-")}</small>
    </button>`;
  }).join("");
}

function replayFilteredHands(hands) {
  const filter = els.replayHandFilter?.value || "all";
  const profitMap = buildHandProfitMap(state.hands);
  return hands.filter((hand) => {
    const profit = profitMap.get(hand.handId);
    if (filter === "allin") return hand.allInPre || /all-in/.test(hand.raw || "");
    if (filter === "lost") return profit ? profit.chips < 0 : hand.result === 0;
    if (filter === "showdown") return hand.showedDown;
    if (filter === "river") return hand.sawRiver;
    if (filter === "threebet") return hand.threeBet || hand.facedThreeBet || hand.fourBet;
    if (filter === "bigpot") return profit ? Math.abs(profit.bb) >= 20 : false;
    return true;
  });
}

function renderReplayHandList(hands) {
  const profitMap = buildHandProfitMap(state.hands);
  if (!hands.length) {
    els.replayHandList.innerHTML = `<div class="empty-note">Nenhuma mao neste filtro.</div>`;
    return;
  }
  els.replayHandList.innerHTML = hands.map((hand) => {
    const profit = profitMap.get(hand.handId);
    const active = hand.handId === state.selectedReplayHandId ? "is-active" : "";
    const outcomeClass = replayHandOutcomeClass(hand, profit);
    const outcomeLabel = replayHandOutcomeLabel(outcomeClass);
    const result = profit ? `${profit.bb >= 0 ? "+" : ""}${profit.bb.toFixed(1)}bb` : "-";
    const flags = [
      hand.allInPre || /all-in/.test(hand.raw || "") ? "All-in" : "",
      hand.showedDown ? "SD" : "",
      hand.sawRiver ? "River" : "",
      hand.threeBet || hand.fourBet ? "3/4bet" : "",
    ].filter(Boolean).join(" - ");
    const notes = handNoteLabels(hand.handId);
    return `<button class="replay-card hand-card ${active} ${outcomeClass}" data-hand-id="${escapeAttr(hand.handId)}" type="button">
      <strong>
        <span>${escapeHtml(hand.heroCards || "--")} - ${escapeHtml(hand.position || "-")}</span>
        <em class="hand-result-badge">${escapeHtml(result)}</em>
      </strong>
      <span>${escapeHtml(hand.preflopSummary || "sem acao")} ${flags ? `- ${escapeHtml(flags)}` : ""}</span>
      ${notes.length ? `<span class="hand-note-badges">${notes.map((note) => `<i>${escapeHtml(note)}</i>`).join("")}</span>` : ""}
      <small>${escapeHtml(hand.date || "")} - #${escapeHtml(hand.handId)} <em>${escapeHtml(outcomeLabel)}</em></small>
    </button>`;
  }).join("");
  updateReplayHandActive();
}

function replayHandOutcomeClass(hand, profit) {
  if (!hand.vpip) return "is-unplayed";
  const bb = profit?.bb;
  if (Number.isFinite(bb)) {
    if (bb > 0) return "is-won";
    if (bb < 0) return "is-lost";
  }
  if (Number.isFinite(hand.result)) {
    if (hand.result > 0) return "is-won";
    if (hand.result === 0) return "is-lost";
  }
  return "is-neutral";
}

function replayHandOutcomeLabel(outcomeClass) {
  if (outcomeClass === "is-won") return "Ganha";
  if (outcomeClass === "is-lost") return "Perdida";
  if (outcomeClass === "is-unplayed") return "Ante/fold";
  return "Neutra";
}

function renderReplayStage(hand, tournament, tournamentHands = []) {
  if (!hand) {
    els.replayHandTitle.textContent = "Selecione uma mao";
    els.replayHandMeta.textContent = "Importe maos e escolha um torneio para rever.";
    els.replayTable.innerHTML = `<div class="empty-note">Sem mao selecionada.</div>`;
    if (els.replayActionList) els.replayActionList.innerHTML = "";
    if (els.replayNotes) els.replayNotes.innerHTML = "";
    updateReplayControls(null, [], null);
    return;
  }
  const replay = getReplayHand(hand);
  state.selectedReplayActionIndex = clamp(state.selectedReplayActionIndex, 0, Math.max(0, replay.actions.length - 1));
  els.replayHandTitle.textContent = `${hand.heroCards || "--"} Â· ${hand.position || "-"} Â· ${hand.stackBb || "-"}bb`;
  els.replayHandMeta.textContent = `${tournament?.title || hand.buyIn || ""} Â· ${hand.date || ""} Â· Mao #${hand.handId}`;
  renderReplayTable(replay, hand, state.selectedReplayActionIndex);
  renderReplayActions(replay);
  renderReplayNotes(hand);
  updateReplayControls(replay, tournamentHands, hand);
  updateReplayHandActive();
}

function getReplayHand(hand) {
  const signature = `${hand.handId || ""}|${String(hand.raw || "").length}|${String(hand.raw || "").slice(0, 24)}`;
  const cached = replayCache.get(hand.handId);
  if (cached?.signature === signature) return cached.replay;
  const replay = parseReplayHand(hand);
  replayCache.set(hand.handId, { signature, replay });
  return replay;
}

function parseReplayHand(hand) {
  const lines = String(hand.raw || "").split("\n").map((line) => line.trim()).filter(Boolean);
  const buttonSeat = lines.find((line) => line.startsWith("Table "))?.match(/Seat\s+#?(\d+)\s+is the button/i)?.[1];
  const seats = parseSeats(lines);
  const positions = assignPositions(seats, toNumber(buttonSeat));
  const actions = [];
  const blinds = [];
  const forcedPosts = [];
  const potTimeline = [];
  let pot = 0;
  const streetPaid = new Map();
  const boards = {
    Preflop: [],
    Flop: parseBoardFromLine(lines.find((line) => line.startsWith("*** FLOP ***") || line.startsWith("** Dealing Flop **")) || ""),
    Turn: parseBoardFromLine(lines.find((line) => line.startsWith("*** TURN ***") || line.startsWith("** Dealing Turn **")) || ""),
    River: parseBoardFromLine(lines.find((line) => line.startsWith("*** RIVER ***") || line.startsWith("** Dealing River **")) || ""),
    Showdown: parseBoardFromLine(lines.find((line) => line.startsWith("*** RIVER ***") || line.startsWith("** Dealing River **")) || lines.find((line) => line.startsWith("*** TURN ***") || line.startsWith("** Dealing Turn **")) || lines.find((line) => line.startsWith("*** FLOP ***") || line.startsWith("** Dealing Flop **")) || ""),
  };
  let street = "Preflop";
  for (const line of lines) {
    if (line.startsWith("*** FLOP ***") || line.startsWith("** Dealing Flop **")) {
      street = "Flop";
      streetPaid.clear();
      actions.push({ type: "street", street, text: `Flop ${boards.Flop.join(" ")}`, pot: Math.max(0, pot) });
      potTimeline.push(Math.max(0, pot));
      continue;
    }
    if (line.startsWith("*** TURN ***") || line.startsWith("** Dealing Turn **")) {
      street = "Turn";
      streetPaid.clear();
      actions.push({ type: "street", street, text: `Turn ${boards.Turn.slice(-1).join(" ")}`, pot: Math.max(0, pot) });
      potTimeline.push(Math.max(0, pot));
      continue;
    }
    if (line.startsWith("*** RIVER ***") || line.startsWith("** Dealing River **")) {
      street = "River";
      streetPaid.clear();
      actions.push({ type: "street", street, text: `River ${boards.River.slice(-1).join(" ")}`, pot: Math.max(0, pot) });
      potTimeline.push(Math.max(0, pot));
      continue;
    }
    if (line.startsWith("*** SHOWDOWN ***") || line.startsWith("*** SHOW DOWN ***") || line.startsWith("** Summary **")) {
      street = "Showdown";
      actions.push({ type: "showdown", street, text: "Showdown", pot: Math.max(0, pot) });
      potTimeline.push(Math.max(0, pot));
      continue;
    }

    const action = parseActionLine(line);
    const allIn = action ? null : line.match(/^(.+?)\s+is\s+all-?In\.?$/i);
    if (allIn) {
      const player = allIn[1].trim();
      const previous = [...actions].reverse().find((item) => item.player === player);
      if (previous) previous.allIn = true;
      continue;
    }
    const showMatch = line.match(/^(.+?)(?::\s*|\s+)shows \[([^\]]+)\]/);
    if (showMatch) {
      actions.push({
        type: "show",
        player: showMatch[1],
        cards: parseCardList(showMatch[2]),
        street,
        text: line,
        pot: Math.max(0, pot),
      });
      potTimeline.push(Math.max(0, pot));
      continue;
    }
    const blindPost = line.match(/^(.+?)(?::)?\s+posts\s+(small blind|big blind)\s+\(?[$€£]?\s*([\d,.]+)\)?/i);
    if (blindPost) {
      blinds.push({
        player: blindPost[1],
        label: /small blind/i.test(blindPost[2]) ? "SB" : "BB",
        amount: toNumber(blindPost[3]) || 0,
      });
    }
    const forcedPost = line.match(/^(.+?)(?::)?\s+posts\s+(?:the )?(ante|small blind|big blind|button blind|small & big blinds|dead blind)\s+\(?[$€£]?\s*([\d,.]+)\)?/i);
    if (forcedPost) {
      forcedPosts.push({
        player: forcedPost[1],
        kind: forcedPost[2],
        amount: toNumber(forcedPost[3]) || 0,
      });
    }
    const moneyChange = replayMoneyChange(line, action, streetPaid);
    pot += moneyChange;
    if (action) {
      actions.push({ ...action, street, text: line, pot: Math.max(0, pot) });
      potTimeline.push(Math.max(0, pot));
    }
    else if (/posts|collected|Uncalled bet/.test(line) && !line.startsWith("Total pot")) {
      const player = line.match(/^(.+?): posts/)?.[1]
        || line.match(/^(.+?) collected/)?.[1]
        || line.match(/returned to (.+)$/)?.[1]
        || "";
      if (!/: posts /.test(line) && player) {
        actions.push({
          type: /collected/.test(line) ? "collect" : "return",
          player,
          street,
          text: line,
          pot: Math.max(0, pot),
        });
        potTimeline.push(Math.max(0, pot));
      }
    }
  }
  return {
    seats: seats.map((seat) => ({ ...seat, position: positions.get(seat.name) || "" })),
    buttonSeat: Number(buttonSeat),
    boards,
    actions,
    blinds,
    forcedPosts,
    startingPot: potTimeline[0] || Math.max(0, pot),
    shownHands: parseShownHands(lines),
  };
}

function replayMoneyChange(line, action, streetPaid) {
  if (/^\*\*\* (FLOP|TURN|RIVER) \*\*\*/.test(line) || /^\*\* Dealing (Flop|Turn|River) \*\*/.test(line)) {
    streetPaid.clear();
    return 0;
  }
  const post = line.match(/^(.+?)(?::)?\s+posts\s+(?:the )?(ante|small blind|big blind|button blind|small & big blinds|dead blind)\s+\(?[$€£]?\s*([\d,.]+)\)?/i);
  if (post) {
    const player = post[1];
    const amount = toNumber(post[3]) || 0;
    if (!/ante/i.test(post[2])) streetPaid.set(player, (streetPaid.get(player) || 0) + amount);
    return amount;
  }
  const returned = parseReturnedBetLine(line);
  if (returned) {
    const amount = returned.amount || 0;
    const player = returned.player;
    streetPaid.set(player, Math.max(0, (streetPaid.get(player) || 0) - amount));
    return -amount;
  }
  if (!action) return 0;
  if (action.verb === "calls" || action.verb === "bets") {
    streetPaid.set(action.player, (streetPaid.get(action.player) || 0) + action.amount);
    return action.amount;
  }
  if (action.verb === "raises") {
    const total = action.amount || 0;
    const paid = Math.max(0, total - (streetPaid.get(action.player) || 0));
    streetPaid.set(action.player, total);
    return paid;
  }
  return 0;
}

function replayStacksAtAction(replay, actionIndex) {
  const stacks = new Map((replay.seats || []).map((seat) => [seat.name, Number(seat.chips) || 0]));
  const streetCommitted = new Map();
  const changeStack = (player, amount) => {
    if (!player || !stacks.has(player) || !Number.isFinite(amount)) return;
    stacks.set(player, stacks.get(player) + amount);
  };

  for (const post of replay.forcedPosts || []) {
    const amount = Number(post.amount) || 0;
    changeStack(post.player, -amount);
    if (!/ante/i.test(post.kind || "")) {
      streetCommitted.set(post.player, (streetCommitted.get(post.player) || 0) + amount);
    }
  }

  for (let index = 0; index <= actionIndex; index += 1) {
    const action = replay.actions[index];
    if (!action) continue;
    if (action.type === "street" || action.type === "showdown") {
      streetCommitted.clear();
      continue;
    }

    const returned = parseReturnedBetLine(action.text || "");
    if (returned) {
      const amount = returned.amount || 0;
      const player = returned.player;
      changeStack(player, amount);
      streetCommitted.set(player, Math.max(0, (streetCommitted.get(player) || 0) - amount));
      continue;
    }

    const collected = String(action.text || "").match(new RegExp(`^(.+?) collected \\(?[$€£]?\\s*([\\d,.]+)\\)?${COLLECTED_POT_SUFFIX_PATTERN}`, "i"));
    if (collected) {
      changeStack(collected[1], toNumber(collected[2]) || 0);
      continue;
    }

    if (!action.player || !Number.isFinite(action.amount)) continue;
    if (action.verb === "calls" || action.verb === "bets") {
      changeStack(action.player, -action.amount);
      streetCommitted.set(action.player, (streetCommitted.get(action.player) || 0) + action.amount);
    }
    else if (action.verb === "raises") {
      const total = action.amount || 0;
      const paid = Math.max(0, total - (streetCommitted.get(action.player) || 0));
      changeStack(action.player, -paid);
      streetCommitted.set(action.player, total);
    }
    if (action.allIn && stacks.has(action.player)) {
      stacks.set(action.player, 0);
    }
  }

  return stacks;
}

function formatReplayStack(value, bigBlind, unit) {
  const safeValue = Math.max(0, Number(value) || 0);
  if (unit === "bb" && Number.isFinite(bigBlind) && bigBlind > 0) {
    return `${(safeValue / bigBlind).toFixed(1)}bb`;
  }
  return fmt(Math.round(safeValue));
}

function getPlayerHudStats() {
  if (document.body?.classList.contains("hud-overlay-window-mode") && playerHudCache.signature === "overlay-payload") {
    return playerHudCache.stats;
  }
  const lastHandId = state.hands[state.hands.length - 1]?.handId || "";
  const signature = `${state.hands.length}|${lastHandId}|${opponentHudIndexedHandIds.size}`;
  if (playerHudCache.signature === signature) return playerHudCache.stats;
  const stats = buildHeroHudStats(state.hands);
  for (const [player, source] of opponentHudStats) {
    const target = getOrCreatePlayerHudStats(stats, player);
    mergeHudStatRecord(target, source);
  }
  playerHudCache.signature = signature;
  playerHudCache.stats = stats;
  return stats;
}

function mergeHudStatRecord(target, source) {
  for (const [key, value] of Object.entries(source || {})) {
    if (Number.isFinite(Number(value))) target[key] = (Number(target[key]) || 0) + Number(value);
  }
}

function indexOpponentHudHands(hands) {
  let changed = false;
  for (const hand of hands || []) {
    if (!hand?.raw || !hand.handId || opponentHudIndexedHandIds.has(hand.handId)) continue;
    const handStats = buildPlayerHudStats([hand]);
    if (hand.hero) handStats.delete(hand.hero);
    for (const [player, source] of handStats) {
      const target = getOrCreatePlayerHudStats(opponentHudStats, player);
      mergeHudStatRecord(target, source);
    }
    opponentHudIndexedHandIds.add(hand.handId);
    changed = true;
  }
  if (changed) playerHudCache.signature = "";
}

function startOpponentHudIndex() {
  if (opponentHudIndexRunning) return;
  if (opponentHudIndexCursor < 0 && !opponentHudIndexedHandIds.size) opponentHudIndexCursor = state.hands.length - 1;
  if (opponentHudIndexCursor < 0) return;
  opponentHudIndexRunning = true;
  const runBatch = (deadline) => {
    let processed = 0;
    while (opponentHudIndexCursor >= 0 && processed < 8 && (processed === 0 || !deadline || deadline.timeRemaining() > 2)) {
      indexOpponentHudHands([state.hands[opponentHudIndexCursor]]);
      opponentHudIndexCursor -= 1;
      processed += 1;
    }
    if (opponentHudIndexCursor >= 0) {
      window.setTimeout(() => {
        if (typeof window.requestIdleCallback === "function") window.requestIdleCallback(runBatch, { timeout: 300 });
        else runBatch(null);
      }, state.hudEnabled ? 160 : 35);
      return;
    }
    opponentHudIndexRunning = false;
    publishHudPayload();
  };
  if (typeof window.requestIdleCallback === "function") window.requestIdleCallback(runBatch, { timeout: 400 });
  else window.setTimeout(() => runBatch(null), 50);
}

function buildHeroHudStats(hands) {
  const stats = new Map();
  const fieldMap = {
    vpip: "vpip", pfr: "pfr",
    threeBetOpp: "threeBetOpportunity", threeBet: "threeBet",
    foldToThreeBetOpp: "facedThreeBet", foldToThreeBet: "foldToThreeBet",
    fourBetOpp: "fourBetOpportunity", fourBet: "fourBet",
    squeezeOpp: "squeezeOpportunity", squeeze: "squeeze",
    cbetFlopOpp: "cbetFlopOpportunity", cbetFlop: "cbetFlop",
    cbetFlopIPOpp: "cbetFlopIPOpportunity", cbetFlopIP: "cbetFlopIP",
    cbetFlopOOPOpp: "cbetFlopOOPOpportunity", cbetFlopOOP: "cbetFlopOOP",
    cbetTurnOpp: "cbetTurnOpportunity", cbetTurn: "cbetTurn",
    cbetRiverOpp: "cbetRiverOpportunity", cbetRiver: "cbetRiver",
    foldToCbetFlopOpp: "facedFlopCbet", foldToCbetFlop: "foldToFlopCbet",
    foldToCbetTurnOpp: "facedTurnCbet", foldToCbetTurn: "foldToTurnCbet",
    foldToCbetRiverOpp: "facedRiverCbet", foldToCbetRiver: "foldToRiverCbet",
    betVsMissedFlopOpp: "betVsMissedFlopOpportunity", betVsMissedFlop: "betVsMissedFlop",
    betVsMissedTurnOpp: "betVsMissedTurnOpportunity", betVsMissedTurn: "betVsMissedTurn",
    betVsMissedRiverOpp: "betVsMissedRiverOpportunity", betVsMissedRiver: "betVsMissedRiver",
    xrOopFlopOpp: "xrOopFlopOpportunity", xrOopFlop: "xrOopFlop",
    xrOopTurnOpp: "xrOopTurnOpportunity", xrOopTurn: "xrOopTurn",
    xrOopRiverOpp: "xrOopRiverOpportunity", xrOopRiver: "xrOopRiver",
    donkFlopOpp: "donkBetFlopOpportunity", donkFlop: "donkBetFlop",
    donkTurnOpp: "donkBetTurnOpportunity", donkTurn: "donkBetTurn",
    donkRiverOpp: "donkBetRiverOpportunity", donkRiver: "donkBetRiver",
    foldVsBetFlopOpp: "foldVsBetFlopOpportunity", foldVsBetFlop: "foldVsBetFlop",
    foldVsBetTurnOpp: "foldVsBetTurnOpportunity", foldVsBetTurn: "foldVsBetTurn",
    foldVsBetRiverOpp: "foldVsBetRiverOpportunity", foldVsBetRiver: "foldVsBetRiver",
    raiseIpFlopOpp: "raiseIpFlopOpportunity", raiseIpFlop: "raiseIpFlop",
    raiseIpTurnOpp: "raiseIpTurnOpportunity", raiseIpTurn: "raiseIpTurn",
    raiseIpRiverOpp: "raiseIpRiverOpportunity", raiseIpRiver: "raiseIpRiver",
    riverBetOpp: "riverBetOpportunity", riverBet: "riverBet",
    sawFlop: "sawFlop",
    showedDown: "showedDown", wonShowdown: "wonShowdown",
    skipCbetFlopOpp: "skipCbetFlopOpportunity",
    skipCbetFoldFlop: "skipCbetFoldFlop",
    skipCbetRaiseFlop: "skipCbetRaiseFlop",
  };
  for (const hand of hands || []) {
    if (!hand?.hero) continue;
    const playerStats = getOrCreatePlayerHudStats(stats, hand.hero);
    playerStats.hands += 1;
    for (const [target, source] of Object.entries(fieldMap)) {
      if (hand[source]) playerStats[target] += 1;
    }
    if (hand.sawFlop && wonHand(hand)) playerStats.wonWhenSawFlop += 1;
  }
  return stats;
}

function emptyPlayerHudStats() {
  return {
    hands: 0,
    vpip: 0,
    pfr: 0,
    threeBetOpp: 0,
    threeBet: 0,
    foldToThreeBetOpp: 0,
    foldToThreeBet: 0,
    fourBetOpp: 0,
    fourBet: 0,
    squeezeOpp: 0,
    squeeze: 0,
    cbetFlopOpp: 0,
    cbetFlop: 0,
    cbetFlopIPOpp: 0,
    cbetFlopIP: 0,
    cbetFlopOOPOpp: 0,
    cbetFlopOOP: 0,
    cbetTurnOpp: 0,
    cbetTurn: 0,
    cbetRiverOpp: 0,
    cbetRiver: 0,
    foldToCbetFlopOpp: 0,
    foldToCbetFlop: 0,
    foldToCbetTurnOpp: 0,
    foldToCbetTurn: 0,
    foldToCbetRiverOpp: 0,
    foldToCbetRiver: 0,
    betVsMissedFlopOpp: 0,
    betVsMissedFlop: 0,
    betVsMissedTurnOpp: 0,
    betVsMissedTurn: 0,
    betVsMissedRiverOpp: 0,
    betVsMissedRiver: 0,
    xrOopFlopOpp: 0,
    xrOopFlop: 0,
    xrOopTurnOpp: 0,
    xrOopTurn: 0,
    xrOopRiverOpp: 0,
    xrOopRiver: 0,
    donkFlopOpp: 0,
    donkFlop: 0,
    donkTurnOpp: 0,
    donkTurn: 0,
    donkRiverOpp: 0,
    donkRiver: 0,
    foldVsBetFlopOpp: 0,
    foldVsBetFlop: 0,
    foldVsBetTurnOpp: 0,
    foldVsBetTurn: 0,
    foldVsBetRiverOpp: 0,
    foldVsBetRiver: 0,
    raiseIpFlopOpp: 0,
    raiseIpFlop: 0,
    raiseIpTurnOpp: 0,
    raiseIpTurn: 0,
    raiseIpRiverOpp: 0,
    raiseIpRiver: 0,
    riverBetOpp: 0,
    riverBet: 0,
    sawFlop: 0,
    wonWhenSawFlop: 0,
    showedDown: 0,
    wonShowdown: 0,
    skipCbetFlopOpp: 0,
    skipCbetFoldFlop: 0,
    skipCbetRaiseFlop: 0,
  };
}

function getOrCreatePlayerHudStats(stats, player) {
  if (!player) return null;
  if (!stats.has(player)) stats.set(player, emptyPlayerHudStats());
  return stats.get(player);
}

function buildPlayerHudStats(hands) {
  const stats = new Map();
  for (const hand of hands || []) {
    const lines = String(hand.raw || "").split("\n").map((line) => line.trim()).filter(Boolean);
    if (!lines.length) continue;
    const tableLine = lines.find((line) => line.startsWith("Table "));
    const buttonMatch = tableLine?.match(/Seat\s+#?(\d+)\s+is the button/i);
    const seats = parseSeats(lines);
    const positions = assignPositions(seats, buttonMatch ? Number(buttonMatch[1]) : null);
    const players = seats.map((seat) => seat.name).filter(Boolean);
    for (const player of players) {
      getOrCreatePlayerHudStats(stats, player).hands += 1;
    }

    const preflop = parsePreflop(lines).map(parseActionLine).filter(Boolean);
    const flop = parseStreetActions(lines, "FLOP");
    const turn = parseStreetActions(lines, "TURN");
    const river = parseStreetActions(lines, "RIVER");
    countPreflopHudStats(stats, players, preflop, seats, positions, hand.bigBlind);
    countFlopHudStats(stats, players, preflop, flop, seats, positions, lines, hand.bigBlind);
    countLaterStreetHudStats(stats, players, preflop, flop, turn, river, seats, positions, lines, hand.bigBlind);
    countShowdownHudStats(stats, players, lines);
  }
  return stats;
}

function firstPlayerResponse(actions, player, afterIndex, verbs = ["folds", "calls", "raises"]) {
  return actions.find((action, index) => index > afterIndex && action.player === player && verbs.includes(action.verb)) || null;
}

function countPreflopHudStats(stats, players, preflop, seats = [], positions = new Map(), bigBlind = null) {
  const raiseIndexes = preflop
    .map((action, index) => ({ action, index }))
    .filter((item) => item.action.verb === "raises");
  const firstRaise = raiseIndexes[0];
  const secondRaise = raiseIndexes[1];
  const secondRaiseIndex = secondRaise?.index ?? -1;

  for (const player of players) {
    const playerStats = getOrCreatePlayerHudStats(stats, player);
    const playerActions = preflop.filter((action) => action.player === player);
    if (playerActions.some((action) => ["calls", "bets", "raises"].includes(action.verb))) playerStats.vpip += 1;
    if (playerActions.some((action) => action.verb === "raises")) playerStats.pfr += 1;

    if (firstRaise && firstRaise.action.player !== player) {
      const response = firstPlayerResponse(preflop, player, firstRaise.index);
      const responseIndex = response ? preflop.indexOf(response) : -1;
      if (response && (!secondRaise || responseIndex <= secondRaiseIndex)) {
        playerStats.threeBetOpp += 1;
        if (response.verb === "raises") playerStats.threeBet += 1;
      }
    }
  }

  if (firstRaise && secondRaise && firstRaise.action.player !== secondRaise.action.player) {
    const openerStats = getOrCreatePlayerHudStats(stats, firstRaise.action.player);
    const foldSpot = ggleaksFoldToThreeBetSpot(firstRaise.action.player, seats, positions, preflop, bigBlind);
    const response = firstPlayerResponse(preflop, firstRaise.action.player, secondRaise.index);
    if (foldSpot) {
      openerStats.foldToThreeBetOpp += 1;
      if (foldSpot.heroAction.verb === "folds") openerStats.foldToThreeBet += 1;
    }
    if (response) {
      openerStats.fourBetOpp += 1;
      if (response.verb === "raises") openerStats.fourBet += 1;
    }
  }

  for (const player of players) {
    if (!firstRaise || firstRaise.action.player === player) continue;
    const firstActionIndex = preflop.findIndex((action) => action.player === player && ["folds", "calls", "raises"].includes(action.verb));
    if (firstActionIndex < 0 || firstActionIndex <= firstRaise.index) continue;
    const callsBeforePlayer = preflop.filter((action, index) => index > firstRaise.index && index < firstActionIndex && action.player !== player && action.verb === "calls");
    const raisesBeforePlayer = preflop.some((action, index) => index > firstRaise.index && index < firstActionIndex && action.verb === "raises");
    if (!callsBeforePlayer.length || raisesBeforePlayer) continue;
    const playerStats = getOrCreatePlayerHudStats(stats, player);
    playerStats.squeezeOpp += 1;
    if (preflop[firstActionIndex]?.verb === "raises") playerStats.squeeze += 1;
  }
}

function countFlopHudStats(stats, players, preflop, flop, seats = [], positions = new Map(), lines = [], bigBlind = null) {
  if (!flop.length) return;
  const isHeadsUp = postflopActiveSeats(seats, preflop).length === 2;
  for (const player of players) {
    const playerStats = getOrCreatePlayerHudStats(stats, player);
    if (hasCbetOpportunity(player, preflop, flop)) {
      playerStats.cbetFlopOpp += 1;
      if (madeCbet(player, preflop, flop)) playerStats.cbetFlop += 1;
    }
    const cbetIpSpot = ggleaksCbetFlopIpSpot(player, seats, positions, preflop, flop, lines, bigBlind);
    if (cbetIpSpot) {
      playerStats.cbetFlopIPOpp += 1;
      if (cbetIpSpot.bet) playerStats.cbetFlopIP += 1;
    }
    const cbetOopSpot = ggleaksCbetFlopOopSpot(player, seats, positions, preflop, flop, lines, bigBlind);
    if (cbetOopSpot) {
      playerStats.cbetFlopOOPOpp += 1;
      if (cbetOopSpot.bet) playerStats.cbetFlopOOP += 1;
    }
    const foldToCbetFlopSpot = ggleaksFoldToCbetFlopSpot(player, seats, preflop, flop, lines, bigBlind);
    if (foldToCbetFlopSpot) {
      playerStats.foldToCbetFlopOpp += 1;
      if (foldToCbetFlopSpot.heroAction.verb === "folds") playerStats.foldToCbetFlop += 1;
    }
    if (hasBetVsMissedOpportunity(player, preflop, flop)) {
      playerStats.betVsMissedFlopOpp += 1;
      if (madeBetVsMissed(player, preflop, flop)) playerStats.betVsMissedFlop += 1;
    }
    if (hasBlindCheckRaiseOpportunity(player, seats, positions, preflop, flop)) {
      playerStats.xrOopFlopOpp += 1;
      if (madeBlindCheckRaise(player, seats, positions, preflop, flop)) playerStats.xrOopFlop += 1;
    }
    if (hasSkipCbetOpportunity(player, preflop, flop)) {
      playerStats.skipCbetFlopOpp += 1;
      if (skippedCbetAnd(player, preflop, flop, "folds")) playerStats.skipCbetFoldFlop += 1;
      if (skippedCbetAnd(player, preflop, flop, "raises")) playerStats.skipCbetRaiseFlop += 1;
    }
  }
}

function countLaterStreetHudStats(stats, players, preflop, flop, turn, river, seats = [], positions = new Map(), lines = [], bigBlind = null) {
  const potBb = preflopPotBb(lines, preflop, bigBlind);
  for (const player of players) {
    const playerStats = getOrCreatePlayerHudStats(stats, player);
    const donkStats = ggleaksDonkStats(player, seats, positions, preflop, flop, turn, river, potBb, bigBlind);
    const cbetTurnSpot = ggleaksCbetTurnSpot(player, seats, positions, preflop, flop, turn, lines, bigBlind);
    if (cbetTurnSpot) {
      playerStats.cbetTurnOpp += 1;
      if (cbetTurnSpot.bet) playerStats.cbetTurn += 1;
    }
    const cbetRiverSpot = ggleaksCbetRiverSpot(player, seats, preflop, flop, turn, river, lines, bigBlind);
    if (cbetRiverSpot) {
      playerStats.cbetRiverOpp += 1;
      if (cbetRiverSpot.bet) playerStats.cbetRiver += 1;
    }
    const foldToCbetTurnSpot = ggleaksFoldToCbetTurnSpot(player, seats, preflop, flop, turn, lines, bigBlind);
    if (foldToCbetTurnSpot) {
      playerStats.foldToCbetTurnOpp += 1;
      if (foldToCbetTurnSpot.heroAction.verb === "folds") playerStats.foldToCbetTurn += 1;
    }
    if (facedBarrel(player, turn, river)) {
      playerStats.foldToCbetRiverOpp += 1;
      if (foldedFacingBarrel(player, turn, river)) playerStats.foldToCbetRiver += 1;
    }
    if (hasBetVsMissedTurnOpportunity(player, preflop, flop, turn)) {
      playerStats.betVsMissedTurnOpp += 1;
      if (madeBetVsMissedTurn(player, preflop, flop, turn)) playerStats.betVsMissedTurn += 1;
    }
    if (hasBetVsMissedRiverOpportunity(player, preflop, flop, turn, river)) {
      playerStats.betVsMissedRiverOpp += 1;
      if (madeBetVsMissedRiver(player, preflop, flop, turn, river)) playerStats.betVsMissedRiver += 1;
    }
    if (hasBlindCheckRaiseOpportunity(player, seats, positions, preflop, turn)) {
      playerStats.xrOopTurnOpp += 1;
      if (madeBlindCheckRaise(player, seats, positions, preflop, turn)) playerStats.xrOopTurn += 1;
    }
    if (hasBlindCheckRaiseOpportunity(player, seats, positions, preflop, river)) {
      playerStats.xrOopRiverOpp += 1;
      if (madeBlindCheckRaise(player, seats, positions, preflop, river)) playerStats.xrOopRiver += 1;
    }
    if (donkStats.flopOpportunity) {
      playerStats.donkFlopOpp += 1;
      if (donkStats.flop) playerStats.donkFlop += 1;
    }
    if (donkStats.turnOpportunity) {
      playerStats.donkTurnOpp += 1;
      if (donkStats.turn) playerStats.donkTurn += 1;
    }
    if (donkStats.riverOpportunity) {
      playerStats.donkRiverOpp += 1;
      if (donkStats.river) playerStats.donkRiver += 1;
    }
    if (hasFoldVsBetOpportunity(player, flop)) {
      playerStats.foldVsBetFlopOpp += 1;
      if (madeFoldVsBet(player, flop)) playerStats.foldVsBetFlop += 1;
    }
    if (hasFoldVsBetOpportunity(player, turn)) {
      playerStats.foldVsBetTurnOpp += 1;
      if (madeFoldVsBet(player, turn)) playerStats.foldVsBetTurn += 1;
    }
    if (hasFoldVsBetOpportunity(player, river)) {
      playerStats.foldVsBetRiverOpp += 1;
      if (madeFoldVsBet(player, river)) playerStats.foldVsBetRiver += 1;
    }
    if (hasRaiseIpOpportunity(player, flop)) {
      playerStats.raiseIpFlopOpp += 1;
      if (madeRaiseIp(player, flop)) playerStats.raiseIpFlop += 1;
    }
    if (hasRaiseIpOpportunity(player, turn)) {
      playerStats.raiseIpTurnOpp += 1;
      if (madeRaiseIp(player, turn)) playerStats.raiseIpTurn += 1;
    }
    if (hasRaiseIpOpportunity(player, river)) {
      playerStats.raiseIpRiverOpp += 1;
      if (madeRaiseIp(player, river)) playerStats.raiseIpRiver += 1;
    }
    if (hasGgleaksRiverBetOpportunity(player, seats, preflop, river, lines, bigBlind)) {
      playerStats.riverBetOpp += 1;
      if (madeGgleaksRiverBet(player, seats, preflop, river, lines, bigBlind)) playerStats.riverBet += 1;
    }
  }
}

function countShowdownHudStats(stats, players, lines) {
  const preflop = parsePreflop(lines).map(parseActionLine).filter(Boolean);
  const flopWasDealt = streetWasDealt(lines, "FLOP");
  for (const player of players) {
    const playerStats = getOrCreatePlayerHudStats(stats, player);
    const sawFlop = flopWasDealt && !playerFoldedOnStreet(player, preflop);
    if (sawFlop) {
      playerStats.sawFlop += 1;
      if (playerWonHand(lines, player)) playerStats.wonWhenSawFlop += 1;
    }
    if (heroReachedShowdown(lines, player)) {
      playerStats.showedDown += 1;
      if (heroWonShowdown(lines, player)) playerStats.wonShowdown += 1;
    }
  }
}

function playerWonHand(lines, player) {
  if (!player) return false;
  const escaped = escapeRegExp(player);
  const partyLine = partyPlayerSummaryLine(lines, player);
  if (partyLine) return /\bnet\s+\+|collected\s+[\d,.]+/i.test(partyLine);
  return lines.some((line) =>
    new RegExp(`^${escaped} collected \\(?[$€£]?\\s*[\\d,.]+\\)?${COLLECTED_POT_SUFFIX_PATTERN}|Seat \\d+: ${escaped} .* (won|collected)`, "i").test(line)
  );
}

function hudPct(made, total) {
  if (!total) return "-";
  return `${Math.round((made / total) * 100)}%`;
}

function hudCount(value) {
  const number = Number(value) || 0;
  if (number < 1000) return fmt(number);
  if (number < 10000) return `${(number / 1000).toFixed(1).replace(".0", "")}k`;
  return `${Math.round(number / 1000)}k`;
}

function sanitizeHudStats(stats, secondRow = false) {
  const validKeys = new Set(HUD_STAT_OPTIONS.map((option) => option.key));
  const defaults = secondRow
    ? ["fourBet", "squeeze", "cbetFlopIP", "cbetFlopOOP", "betVsMissedFlop", "skipCbetFoldFlop", "skipCbetRaiseFlop"]
    : ["hands", "vpip", "pfr", "threeBet", "foldToThreeBet", "cbetFlop", "foldToCbetFlop"];
  const source = Array.isArray(stats) && stats.length ? stats : defaults;
  return source.slice(0, 7).map((key, index) => validKeys.has(key) ? key : defaults[index] || "hands");
}

function hudStatOption(key) {
  return HUD_STAT_OPTIONS.find((option) => option.key === key) || HUD_STAT_OPTIONS[0];
}

function playerHudHtml(player, placement = "", forceVisible = false) {
  if (!state.hudEnabled && !forceVisible) return "";
  const stats = getPlayerHudStats().get(player) || emptyPlayerHudStats();
  const firstRow = sanitizeHudStats(state.hudStats).map((key) => {
    const option = hudStatOption(key);
    return [option.label, option.value(stats)];
  });
  const secondRow = state.hudSecondRowEnabled
    ? sanitizeHudStats(state.hudStatsSecondRow, true).map((key) => {
      const option = hudStatOption(key);
      return [option.label, option.value(stats)];
    })
    : [];
  const rows = [...firstRow, ...secondRow];
  return `<div class="seat-hud ${state.hudSecondRowEnabled ? "has-two-rows" : ""} ${placement}" aria-label="HUD de ${escapeHtml(player)}">
    ${rows.map(([label, value]) => `<span><b>${label}</b><i>${escapeHtml(value)}</i></span>`).join("")}
  </div>`;
}

function renderHudPage() {
  if (!els.hudView) return;
  state.hudStats = sanitizeHudStats(state.hudStats);
  state.hudStatsSecondRow = sanitizeHudStats(state.hudStatsSecondRow, true);
  if (els.hudToggle) els.hudToggle.textContent = state.hudEnabled ? "Desligar HUD" : "Ligar HUD";
  if (els.hudSecondRowToggle) els.hudSecondRowToggle.checked = state.hudSecondRowEnabled;
  if (els.hudStatus) els.hudStatus.textContent = state.hudEnabled ? "HUD ligada" : "HUD desligada";
  renderHudPlayerSelect();
  renderHudConfig();
  renderHudPreview();
}

function renderHudPlayerSelect() {
  if (!els.hudPlayerSelect) return;
  const heroes = [...state.heroes].sort();
  const selectedHero = state.selectedHero !== "all" && state.heroes.has(state.selectedHero) ? state.selectedHero : "";
  const current = els.hudPlayerSelect.value || selectedHero || heroes[0] || "";
  els.hudPlayerSelect.innerHTML = heroes.length
    ? heroes.map((hero) => `<option value="${escapeHtml(hero)}">${escapeHtml(hero)}</option>`).join("")
    : `<option value="">Sem hero</option>`;
  els.hudPlayerSelect.value = heroes.includes(current) ? current : selectedHero || heroes[0] || "";
}

function renderHudConfig() {
  if (!els.hudConfigGrid || !els.hudSecondConfigGrid) return;
  const optionsHtml = (selected) => HUD_STAT_OPTIONS
    .map((option) => `<option value="${option.key}" ${option.key === selected ? "selected" : ""}>${option.name} (${option.label})</option>`)
    .join("");
  els.hudConfigGrid.innerHTML = state.hudStats.map((key, index) => `
    <label>
      Coluna ${index + 1}
      <select data-hud-slot="${index}">
        ${optionsHtml(key)}
      </select>
    </label>
  `).join("");
  els.hudSecondConfigGrid.innerHTML = state.hudStatsSecondRow.map((key, index) => `
    <label>
      Coluna ${index + 1}
      <select data-hud-second-slot="${index}" ${state.hudSecondRowEnabled ? "" : "disabled"}>
        ${optionsHtml(key)}
      </select>
    </label>
  `).join("");
}

function renderHudPreview() {
  if (!els.hudPreview) return;
  const player = els.hudPlayerSelect?.value || (state.selectedHero !== "all" ? state.selectedHero : [...state.heroes][0]) || "";
  els.hudPreview.innerHTML = player
    ? `<div class="hud-preview-card"><strong>${escapeHtml(player)}</strong>${playerHudHtml(player, "", true)}</div>`
    : `<div class="empty-note">Importe maos para ver a previa da HUD.</div>`;
  renderHudFloatingOverlay();
}

function floatingHudPlayer() {
  return els.hudPlayerSelect?.value || (state.selectedHero !== "all" ? state.selectedHero : [...state.heroes][0]) || "";
}

function renderHudFloatingOverlay() {
  if (!els.hudFloatingContent || !els.hudFloatingPanel) return;
  const player = floatingHudPlayer();
  const userScale = clampHudScale(hudOverlayScaleOverride ?? (Number(state.hudFloatingScale) || 1));
  if (hudOverlayScaleOverride === null) state.hudFloatingScale = userScale;
  const scale = clampHudScale(autoFloatingHudScale() * userScale, 0.55, 2);
  els.hudFloatingPanel.style.setProperty("--hud-float-scale", scale.toFixed(2));
  const params = new URLSearchParams(window.location?.search || "");
  const windowTitle = params.get("hudTitle") || "";
  if (els.hudFloatingTitle) els.hudFloatingTitle.textContent = windowTitle || (player ? `HUD - ${player}` : "HUD");
  const players = Array.isArray(state.hudOverlayPlayers) && state.hudOverlayPlayers.length
    ? state.hudOverlayPlayers
    : (player ? [player] : []);
  els.hudFloatingContent.innerHTML = players.length && state.hudEnabled
    ? players.map((name) => `<div class="floating-hud-player"><strong>${escapeHtml(name)}</strong>${playerHudHtml(name, "floating")}</div>`).join("")
    : `<div class="empty-note">Sem hero importado.</div>`;
}

function autoFloatingHudScale() {
  if (!document.body?.classList.contains("hud-overlay-window-mode")) return 1;
  const baseWidth = state.hudSecondRowEnabled ? 266 : 232;
  const baseHeight = state.hudSecondRowEnabled ? 74 : 54;
  const availableWidth = Math.max(180, window.innerWidth - 18);
  const availableHeight = Math.max(52, window.innerHeight - 40);
  return clampHudScale(Math.min(availableWidth / baseWidth, availableHeight / baseHeight), 0.65, 1.35);
}

function clampHudScale(value, min = 0.7, max = 1.8) {
  return Math.max(min, Math.min(max, Number(value) || 1));
}

async function syncFloatingHud() {
  if (!state.hudEnabled) {
    stopHudAutoSync();
    if (window.desktopStore?.syncHudOverlays) await window.desktopStore.syncHudOverlays(false);
    setImportStatus("HUD desligada. Nenhuma janela de HUD sera aberta.", "success");
    return;
  }
  if (window.desktopStore?.syncHudOverlays) {
    await ensureHudAutoImport();
    await publishHudPayload();
    const result = await window.desktopStore.syncHudOverlays(true);
    const blocked = result?.blocked ? ` ${fmt(result.blocked)} janela(s) bloqueada(s) por politica.` : "";
    const manual = result?.manual ? ` ${fmt(result.manual)} janela(s) exigem modo manual/permissao da sala.` : "";
    setImportStatus(`HUD sincronizada: <strong>${fmt(result?.opened || 0)}</strong> mesa(s) permitida(s).${blocked}${manual}`, "success");
    startHudAutoSync();
    return;
  }
  window.open(`${window.location.pathname}?mode=hud-overlay`, "mtt-edge-hud", "width=360,height=150");
}

async function publishHudPayload() {
  if (!window.desktopStore?.setHudPayload) return;
  const player = floatingHudPlayer();
  const allStats = getPlayerHudStats();
  let recentHand = null;
  const tables = {};
  const payloadPlayers = new Set();
  const liveSnapshots = [...liveHudTableSnapshots.values()]
    .sort((left, right) => compareHudHandIds(right.handId, left.handId));
  for (const snapshot of liveSnapshots) {
    const tablePayload = {
      players: snapshot.players,
      hero: snapshot.hero,
      maxSeats: snapshot.maxSeats,
      tableName: snapshot.tableName,
      handId: snapshot.handId,
    };
    snapshot.aliases.forEach((id) => { if (!tables[id]) tables[id] = tablePayload; });
    snapshot.players.forEach((seat) => payloadPlayers.add(seat.name));
  }
  for (let index = state.hands.length - 1; index >= 0; index -= 1) {
    const hand = state.hands[index];
    if (!hand?.raw) continue;
    if (!recentHand) recentHand = hand;
    const tableId = String(hand.tournamentId || "").replace(/\D/g, "");
    const roster = parseSeats(String(hand.raw).replace(/\r/g, "").split("\n")).map((seat) => ({ name: seat.name, seat: seat.seat })).filter((seat) => seat.name);
    if (!roster.length) continue;
    const declaredMaxSeats = Number(String(hand.raw).match(/^Table\s+.+?\s+(\d+)-max\b/im)?.[1]);
    const maxSeats = declaredMaxSeats || Math.max(roster.length, ...roster.map((seat) => Number(seat.seat) || 0));
    const tableLine = String(hand.raw).match(/^Table\s+['\"]?(.+?)['\"]?\s+\d+-max\b/im)?.[1] || "";
    const tableIds = [...new Set([
      tableId,
      ...[...tableLine.matchAll(/\b\d{8,}\b/g)].map((match) => match[0]),
    ].filter(Boolean))];
    if (!tableIds.length || tableIds.every((id) => tables[id])) continue;
    const tablePayload = {
      players: roster,
      hero: hand.hero || "",
      maxSeats,
      tableName: tableLine,
      handId: String(hand.handId || ""),
    };
    tableIds.forEach((id) => { if (!tables[id]) tables[id] = tablePayload; });
    roster.forEach((seat) => payloadPlayers.add(seat.name));
    if (Object.keys(tables).length >= 20) break;
  }
  const players = recentHand
    ? parseSeats(String(recentHand.raw).replace(/\r/g, "").split("\n")).map((seat) => seat.name).filter(Boolean)
    : (player ? [player] : []);
  players.forEach((name) => payloadPlayers.add(name));
  const statsByPlayer = Object.fromEntries([...payloadPlayers].map((name) => [name, { ...emptyPlayerHudStats(), ...(allStats.get(name) || {}) }]));
  const statKeys = [
    ...sanitizeHudStats(state.hudStats),
    ...(state.hudSecondRowEnabled ? sanitizeHudStats(state.hudStatsSecondRow, true) : []),
  ];
  const rowsByPlayer = Object.fromEntries([...payloadPlayers].map((name) => [name, statKeys.map((key) => {
    const option = hudStatOption(key);
    return { label: option.label, value: option.value(statsByPlayer[name]) };
  })]));
  await window.desktopStore.setHudPayload({
    enabled: !!state.hudEnabled,
    player,
    players,
    tables,
    statsByPlayer,
    rowsByPlayer,
    hudStats: sanitizeHudStats(state.hudStats),
    secondRowEnabled: !!state.hudSecondRowEnabled,
    hudStatsSecondRow: sanitizeHudStats(state.hudStatsSecondRow, true),
    scale: Number(state.hudFloatingScale) || 1,
  });
}

function applyHudPayload(payload) {
  if (!payload || typeof payload !== "object") return;
  state.hudEnabled = !!payload.enabled;
  if (hudOverlayScaleOverride === null) state.hudFloatingScale = Number(payload.scale) || 1;
  state.hudStats = sanitizeHudStats(payload.hudStats);
  state.hudSecondRowEnabled = !!payload.secondRowEnabled;
  state.hudStatsSecondRow = sanitizeHudStats(payload.hudStatsSecondRow, true);
  state.selectedHero = payload.player || "all";
  state.heroes = new Set(payload.player ? [payload.player] : []);
  const tableId = String(payload.tableTitle || "").match(/\((\d+)\)\s*$/)?.[1] || "";
  const tablePlayers = tableId && payload.tables?.[tableId]?.players;
  state.hudOverlayPlayers = Array.isArray(tablePlayers)
    ? tablePlayers.filter(Boolean)
    : (Array.isArray(payload.players) ? payload.players.filter(Boolean) : []);
  playerHudCache.signature = "overlay-payload";
  playerHudCache.stats = new Map(Object.entries(payload.statsByPlayer || {}).map(([name, stats]) => [name, { ...emptyPlayerHudStats(), ...(stats || {}) }]));
  renderHudFloatingOverlay();
}

function startHudAutoSync() {
  if (hudSyncTimer || !window.desktopStore?.syncHudOverlays || !state.hudEnabled) return;
  hudSyncTimer = window.setInterval(() => {
    if (!state.hudEnabled) {
      stopHudAutoSync();
      return;
    }
    publishHudPayload().then(() => window.desktopStore.syncHudOverlays(true));
  }, 3000);
}

function stopHudAutoSync() {
  if (hudSyncTimer) {
    window.clearInterval(hudSyncTimer);
    hudSyncTimer = null;
  }
}

function changeFloatingHudScale(action) {
  const isOverlay = document.body?.classList.contains("hud-overlay-window-mode");
  const current = Number(isOverlay ? hudOverlayScaleOverride ?? state.hudFloatingScale : state.hudFloatingScale) || 1;
  const next = action === "reset" ? 1 : action === "up" ? Math.min(1.8, current + 0.1) : Math.max(0.7, current - 0.1);
  if (isOverlay) {
    hudOverlayScaleOverride = next;
    const key = new URLSearchParams(window.location?.search || "").get("hudKey") || "manual";
    localStorage.setItem(`hud-scale-${key}`, String(next));
    renderHudFloatingOverlay();
    return;
  }
  state.hudFloatingScale = next;
  renderHudFloatingOverlay();
  publishHudPayload();
  saveState({ silent: true });
}

async function toggleHudEnabled() {
  state.hudEnabled = !state.hudEnabled;
  renderHudPage();
  if (state.activeTab === "replayer") renderReplayStageOnly();
  await saveState({ silent: true });
  if (state.hudEnabled) await ensureHudAutoImport();
  else stopAutoImport();
  await syncFloatingHud();
}

function toggleHudSecondRow() {
  state.hudSecondRowEnabled = !state.hudSecondRowEnabled;
  renderHudPage();
  if (state.activeTab === "replayer") renderReplayStageOnly();
  saveState({ silent: true });
  publishHudPayload();
}

function setHudStat(slot, key) {
  const next = sanitizeHudStats(state.hudStats);
  next[slot] = key;
  state.hudStats = sanitizeHudStats(next);
  renderHudPage();
  if (state.activeTab === "replayer") renderReplayStageOnly();
  saveState({ silent: true });
  publishHudPayload();
}

function setHudSecondRowStat(slot, key) {
  const next = sanitizeHudStats(state.hudStatsSecondRow, true);
  next[slot] = key;
  state.hudStatsSecondRow = sanitizeHudStats(next, true);
  renderHudPage();
  if (state.activeTab === "replayer") renderReplayStageOnly();
  saveState({ silent: true });
  publishHudPayload();
}

function renderReplayTable(replay, hand, actionIndex, target = els.replayTable, unitOverride = "") {
  if (!target) return;
  const hero = hand.hero;
  const seatPoints = seatLayout(replay.seats.length);
  const currentAction = replay.actions[actionIndex] || null;
  const activePlayer = currentAction?.player || "";
  const currentPot = currentAction?.pot || replay.startingPot || 0;
  const heroCards = parseCardList(hand.heroCards);
  const visibleShownHands = visibleReplayShownHands(replay, actionIndex);
  const stackUnit = unitOverride || state.replayStackUnit || "bb";
  const currentStacks = replayStacksAtAction(replay, actionIndex);
  const showBounties = hasVisibleBounties(hand, replay.seats);
  const roleInfo = replayRoleInfo(replay, actionIndex);
  const seats = replay.seats.map((seat, index) => {
    const point = seatPoints[index] || { left: 50, top: 50 };
    const lastAction = lastReplayActionForPlayer(replay.actions, seat.name, actionIndex);
    const isHero = seat.name === hero;
    const isActive = seat.name === activePlayer;
    const isButton = seat.seat === replay.buttonSeat;
    const isPfa = seat.name === roleInfo.pfa;
    const isCbetter = seat.name === roleInfo.cbetter;
    const isInPosition = seat.name === roleInfo.inPositionPlayer;
    const shown = visibleShownHands.get(seat.name);
    const seatCards = isHero ? heroCards : shown || [];
    const posClass = positionClass(seat.position);
    const bountyValue = seatBountyValue(hand, seat);
    const badges = [
      isPfa ? "PFA" : "",
      isCbetter ? "CB" : "",
      isInPosition ? "IP" : "",
    ].filter(Boolean);
    return `<div class="seat ${isHero ? "hero-seat" : ""} ${isActive ? "active-seat" : ""} ${isPfa ? "pfa-seat" : ""} ${isCbetter ? "cbetter-seat" : ""} ${isInPosition ? "ip-seat" : ""}" style="left:${point.left}%;top:${point.top}%">
      <div class="seat-top">
        <span>${escapeHtml(seat.name)}</span>
        <strong>${escapeHtml(formatReplayStack(currentStacks.get(seat.name) ?? seat.chips, hand.bigBlind, stackUnit))}</strong>
      </div>
      ${badges.length ? `<div class="seat-badges">${badges.map((badge) => `<i>${escapeHtml(badge)}</i>`).join("")}</div>` : ""}
      <div class="seat-position ${posClass}">${escapeHtml(positionName(seat.position))}</div>
      ${showBounties && Number(bountyValue) > 0 ? `<div class="seat-bounty">Bounty ${escapeHtml(formatBounty(bountyValue))}</div>` : ""}
      <div class="seat-cards">${seatCards.map((card) => cardHtml(card, isHero ? "hero-seat-card" : "shown-seat-card")).join("")}</div>
      ${playerHudHtml(seat.name)}
      ${isButton ? `<i class="dealer-button">BTN</i>` : `<small></small>`}
      ${lastAction ? `<em class="seat-action ${lastAction.verb || ""} ${lastAction.allIn ? "all-in" : ""}">${escapeHtml(actionLabel(lastAction, hand.bigBlind, stackUnit))}</em>` : ""}
    </div>`;
  }).join("");
  const boardCards = replay.boards[currentAction?.street || "Preflop"] || [];
  const board = boardCards.length
    ? boardCards.map((card) => cardHtml(card)).join("")
    : `<span class="board-empty">Board vazio</span>`;
  const betMarker = streetBetMarkersHtml(replay.actions, actionIndex, seatPoints, replay.seats, hand.bigBlind, replay.blinds, stackUnit);
  const blindMarkers = currentAction?.street === "Preflop" ? blindMarkersHtml(replay.blinds, seatPoints, replay.seats, hand.bigBlind, stackUnit) : "";
  target.innerHTML = `
    <div class="table-felt">
      <div class="pot-display">Pot ${escapeHtml(formatReplayStack(currentPot, hand.bigBlind, stackUnit))}</div>
      ${replayCenterInfoHtml(currentAction, hand.bigBlind, stackUnit)}
      ${replayTableStatsHtml(replay, hand, actionIndex, stackUnit)}
      <div class="board">${board}</div>
      ${blindMarkers}
      ${betMarker}
      ${seats}
    </div>`;
}

function positionName(position) {
  const names = {
    BTN: "Button",
    SB: "Small Blind",
    BB: "Big Blind",
    CO: "Cutoff",
    HJ: "Hijack",
    LJ: "Lojack",
    MP: "Middle Position",
    UTG: "Under the Gun",
    "UTG+1": "Under the Gun +1",
  };
  return names[position] || position || "-";
}

function positionClass(position) {
  if (position === "BTN") return "pos-btn";
  if (position === "SB" || position === "BB") return "pos-blind";
  if (position === "CO" || position === "HJ") return "pos-late";
  if (position === "MP" || position === "LJ") return "pos-mid";
  return "pos-early";
}

function formatBounty(value) {
  const number = Number(value);
  if (!Number.isFinite(number) || number <= 0) return "-";
  return number >= 100 ? `$${Math.round(number)}` : `$${number.toFixed(2).replace(/\.00$/, "")}`;
}

function replayCenterInfoHtml(action, bigBlind, unit = state.replayStackUnit || "bb") {
  const street = action?.street || "Preflop";
  const amount = Number(action?.amount);
  const potAfter = Number(action?.pot);
  const potBefore = Number.isFinite(potAfter) && Number.isFinite(amount) ? Math.max(0, potAfter - amount) : null;
  const parts = [`<span>${escapeHtml(street)}</span>`];
  if (Number.isFinite(amount) && amount > 0 && Number.isFinite(potBefore) && potBefore > 0) {
    const pctPot = Math.round((amount / potBefore) * 100);
    parts.push(`<span>${escapeHtml(formatReplayStack(amount, bigBlind, unit))}</span>`);
    parts.push(`<span>${pctPot}% pot</span>`);
  }
  return `<div class="replay-center-info">${parts.join("")}</div>`;
}

function replayTableStatsHtml(replay, hand, actionIndex, unit = state.replayStackUnit || "bb") {
  const metrics = replayActionMetrics(replay, hand, actionIndex, unit);
  const items = [
    metrics.potOdds ? `Pot odds ${metrics.potOdds}` : "",
    metrics.spr ? `SPR ${metrics.spr}` : "",
  ].filter(Boolean);
  return items.length ? `<div class="table-quick-stats">${items.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}</div>` : "";
}

function replayActionMetrics(replay, hand, actionIndex, unit = state.replayStackUnit || "bb") {
  const action = replay.actions[actionIndex] || null;
  const amount = Number(action?.amount);
  const potAfter = Number(action?.pot);
  const potBefore = Number.isFinite(potAfter) && Number.isFinite(amount) ? Math.max(0, potAfter - amount) : null;
  const size = Number.isFinite(amount) && amount > 0 ? formatReplayStack(amount, hand.bigBlind, unit) : "";
  const pctPot = Number.isFinite(amount) && amount > 0 && Number.isFinite(potBefore) && potBefore > 0
    ? `${Math.round((amount / potBefore) * 100)}%`
    : "";
  const potOdds = Number.isFinite(amount) && amount > 0 && Number.isFinite(potBefore)
    ? `${(potBefore / amount).toFixed(1)}:1`
    : "";
  const stacks = replayStacksAtAction(replay, Math.max(0, actionIndex - 1));
  const activeStack = action?.player ? Number(stacks.get(action.player)) : null;
  const spr = Number.isFinite(activeStack) && Number.isFinite(potBefore) && potBefore > 0
    ? (activeStack / potBefore).toFixed(1)
    : "";
  return { size, pctPot, potOdds, spr };
}

function replayRoleInfo(replay, actionIndex) {
  const visibleActions = replay.actions.slice(0, Math.max(0, actionIndex) + 1);
  const pfa = [...replay.actions].reverse().find((action) => (action.street || "Preflop") === "Preflop" && action.verb === "raises")?.player || "";
  const street = replay.actions[actionIndex]?.street || "Preflop";
  const streetActions = visibleActions.filter((action) => action.street === street);
  const cbetter = street !== "Preflop"
    ? streetActions.find((action) => action.player === pfa && action.verb === "bets")?.player || ""
    : "";
  const activePlayers = activeReplayPlayers(replay, actionIndex);
  const byPosition = [...activePlayers].sort((a, b) => positionActionRank(a.position) - positionActionRank(b.position));
  const inPositionPlayer = activePlayers.length > 1 ? byPosition[byPosition.length - 1]?.name || "" : "";
  return { pfa, cbetter, inPositionPlayer };
}

function activeReplayPlayers(replay, actionIndex) {
  const folded = new Set();
  for (let index = 0; index <= actionIndex; index += 1) {
    const action = replay.actions[index];
    if (action?.verb === "folds" && action.player) folded.add(action.player);
  }
  return replay.seats.filter((seat) => !folded.has(seat.name));
}

function positionActionRank(position) {
  const ranks = {
    SB: 1,
    BB: 2,
    UTG: 3,
    "UTG+1": 4,
    MP: 5,
    LJ: 6,
    HJ: 7,
    CO: 8,
    BTN: 9,
  };
  return ranks[position] || 0;
}

function replayAllInEquity(hand) {
  const lines = String(hand.raw || "").split("\n").map((line) => line.trim()).filter(Boolean);
  if (!lines.some((line) => /all-in/i.test(line))) return null;
  const heroCards = parseCardList(hand.heroCards);
  if (heroCards.length !== 2) return null;
  const shownHands = parseShownHands(lines);
  const villains = [...shownHands.entries()].filter(([player, cards]) => player !== hand.hero && cards.length === 2);
  if (villains.length !== 1) return null;
  const [villain, villainCards] = villains[0];
  const equityPoint = findAllInEquityPoint(lines, hand.hero);
  if (!equityPoint || !equityPoint.activePlayers.includes(villain)) return null;
  const equity = calculateHoldemEquity(heroCards, villainCards, equityPoint.board, hand.handId);
  return Number.isFinite(equity) ? { equity, villain } : null;
}

const REPLAY_NOTE_OPTIONS = [
  { key: "erro", label: "Erro" },
  { key: "estudar", label: "Estudar" },
  { key: "icm", label: "ICM" },
  { key: "bounty", label: "Bounty spot" },
];

function handNoteSet(handId) {
  const notes = state.handNotes?.[handId];
  return new Set(Array.isArray(notes) ? notes : []);
}

function handNoteLabels(handId) {
  const selected = handNoteSet(handId);
  return REPLAY_NOTE_OPTIONS.filter((note) => selected.has(note.key)).map((note) => note.label);
}

function renderReplayNotes(hand) {
  if (!els.replayNotes) return;
  const selected = handNoteSet(hand.handId);
  els.replayNotes.innerHTML = `
    <span>Notas</span>
    ${REPLAY_NOTE_OPTIONS.map((note) => `
      <button class="${selected.has(note.key) ? "is-active" : ""}" data-replay-note="${escapeAttr(note.key)}" type="button">
        ${escapeHtml(note.label)}
      </button>
    `).join("")}
  `;
}

async function toggleReplayNote(handId, noteKey) {
  if (!handId || !REPLAY_NOTE_OPTIONS.some((note) => note.key === noteKey)) return;
  const selected = handNoteSet(handId);
  if (selected.has(noteKey)) selected.delete(noteKey);
  else selected.add(noteKey);
  state.handNotes[handId] = [...selected];
  if (!state.handNotes[handId].length) delete state.handNotes[handId];
  renderReplayStageOnly();
  await saveState({ silent: true });
}

function cardHtml(card, extraClass = "") {
  const rank = card?.[0] || "";
  const suit = card?.[1] || "";
  const suitSymbol = { c: "&clubs;", d: "&diams;", h: "&hearts;", s: "&spades;" }[suit] || escapeHtml(suit);
  const color = { c: "club-card", d: "diamond-card", h: "heart-card", s: "spade-card" }[suit] || "black-card";
  return `<span class="card ${color} ${extraClass}">
    <b>${escapeHtml(rank)}</b>
    <i>${suitSymbol}</i>
  </span>`;
}

function lastReplayActionForPlayer(actions, player, actionIndex) {
  for (let index = actionIndex; index >= 0; index -= 1) {
    if (actions[index]?.player === player) return actions[index];
  }
  return null;
}

function visibleReplayShownHands(replay, actionIndex) {
  const visible = new Map();
  for (let index = 0; index <= actionIndex; index += 1) {
    const action = replay.actions[index];
    if (action?.type === "show" && action.player && action.cards?.length) {
      visible.set(action.player, action.cards);
    }
    if (action?.type === "showdown") {
      for (const [player, cards] of replay.shownHands.entries()) {
        visible.set(player, cards);
      }
    }
  }
  return visible;
}

function actionLabel(action, bigBlind, unit = state.replayStackUnit || "bb") {
  if (!action) return "";
  if (action.type === "show") return "Shows";
  if (action.verb === "folds") return "Fold";
  if (action.verb === "checks") return "Check";
  if (action.allIn && action.verb === "calls") return `All-in call ${formatReplayStack(action.amount, bigBlind, unit)}`;
  if (action.allIn && ["bets", "raises"].includes(action.verb)) return `All-in ${formatReplayStack(action.amount, bigBlind, unit)}`;
  if (action.verb === "calls") return `Call ${formatReplayStack(action.amount, bigBlind, unit)}`;
  if (action.verb === "bets") return `Bet ${formatReplayStack(action.amount, bigBlind, unit)}`;
  if (action.verb === "raises") return `Raise ${formatReplayStack(action.amount, bigBlind, unit)}`;
  if (/collected/.test(action.text || "")) return "Ganhou";
  return "";
}

function formatReplayBb(action, bigBlind, value = action?.amount) {
  if (!Number.isFinite(value) || !Number.isFinite(bigBlind) || bigBlind <= 0) {
    return Number.isFinite(value) ? fmt(value) : "";
  }
  return `${(value / bigBlind).toFixed(1)}bb`;
}

function betMarkerHtml(action, seatPoints, seats, bigBlind, unit = state.replayStackUnit || "bb") {
  const displayAmount = Number.isFinite(action.displayAmount) ? action.displayAmount : action.amount;
  if (!["calls", "bets", "raises"].includes(action.verb) || !Number.isFinite(displayAmount)) return "";
  const seatIndex = seats.findIndex((seat) => seat.name === action.player);
  const point = seatPoints[seatIndex];
  if (!point) return "";
  const left = 50 + (point.left - 50) * 0.55;
  const top = 50 + (point.top - 50) * 0.55;
  const label = action.allIn
    ? `All-in ${formatReplayStack(displayAmount, bigBlind, unit)}`
    : formatReplayStack(displayAmount, bigBlind, unit);
  return `<div class="bet-marker ${action.allIn ? "all-in" : ""}" style="left:${left}%;top:${top}%">${escapeHtml(label)}</div>`;
}

function streetBetMarkersHtml(actions, actionIndex, seatPoints, seats, bigBlind, blinds = [], unit = state.replayStackUnit || "bb") {
  const current = actions[actionIndex];
  if (!current) return "";
  const currentStreet = current.street || "Preflop";
  const streetStart = replayStreetStartIndex(actions, actionIndex, currentStreet);
  const committed = new Map();
  const byPlayer = new Map();
  if (currentStreet === "Preflop") {
    for (const blind of blinds || []) {
      if (blind.player && Number.isFinite(blind.amount)) committed.set(blind.player, blind.amount);
    }
  }
  for (let index = streetStart; index <= actionIndex; index += 1) {
    const action = actions[index];
    if (!action) continue;
    if ((action.street || "Preflop") !== currentStreet) continue;
    if (action.type === "street" || action.type === "showdown") continue;
    const returned = parseReturnedBetLine(action.text || "");
    if (returned) {
      const amount = returned.amount || 0;
      const player = returned.player;
      committed.set(player, Math.max(0, (committed.get(player) || 0) - amount));
      continue;
    }
    if (!action.player || !Number.isFinite(action.amount)) continue;
    if (action.verb === "calls" || action.verb === "bets") {
      committed.set(action.player, (committed.get(action.player) || 0) + action.amount);
    }
    else if (action.verb === "raises") {
      committed.set(action.player, action.amount);
    }
    if (["calls", "bets", "raises"].includes(action.verb)) {
      byPlayer.set(action.player, { ...action, displayAmount: committed.get(action.player) || action.amount });
    }
  }
  return [...byPlayer.values()]
    .map((action) => betMarkerHtml(action, seatPoints, seats, bigBlind, unit))
    .join("");
}

function replayStreetStartIndex(actions, actionIndex, currentStreet) {
  for (let index = actionIndex; index >= 0; index -= 1) {
    const action = actions[index];
    if (!action) continue;
    if ((action.street || "Preflop") !== currentStreet) return index + 1;
    if (index !== actionIndex && (action.type === "street" || action.type === "showdown")) return index + 1;
  }
  return 0;
}

function blindMarkersHtml(blinds, seatPoints, seats, bigBlind, unit = state.replayStackUnit || "bb") {
  return (blinds || []).map((blind) => {
    const seatIndex = seats.findIndex((seat) => seat.name === blind.player);
    const point = seatPoints[seatIndex];
    if (!point) return "";
    const left = 50 + (point.left - 50) * 0.7;
    const top = 50 + (point.top - 50) * 0.7;
    const bbText = formatReplayStack(blind.amount, bigBlind, unit);
    return `<div class="blind-marker ${blind.label.toLowerCase()}" style="left:${left}%;top:${top}%">
      <strong>${escapeHtml(blind.label)}</strong>
      <span>${escapeHtml(bbText)}</span>
    </div>`;
  }).join("");
}

function seatLayout(count) {
  const layouts = {
    2: [{ left: 50, top: 14 }, { left: 50, top: 86 }],
    3: [{ left: 50, top: 13 }, { left: 84, top: 67 }, { left: 16, top: 67 }],
    4: [{ left: 50, top: 13 }, { left: 86, top: 50 }, { left: 50, top: 87 }, { left: 14, top: 50 }],
    5: [{ left: 50, top: 13 }, { left: 86, top: 40 }, { left: 73, top: 86 }, { left: 27, top: 86 }, { left: 14, top: 40 }],
    6: [{ left: 50, top: 13 }, { left: 84, top: 32 }, { left: 84, top: 68 }, { left: 50, top: 87 }, { left: 16, top: 68 }, { left: 16, top: 32 }],
    7: [{ left: 50, top: 13 }, { left: 80, top: 26 }, { left: 87, top: 58 }, { left: 68, top: 86 }, { left: 32, top: 86 }, { left: 13, top: 58 }, { left: 20, top: 26 }],
    8: [{ left: 50, top: 13 }, { left: 78, top: 23 }, { left: 88, top: 50 }, { left: 78, top: 77 }, { left: 50, top: 87 }, { left: 22, top: 77 }, { left: 12, top: 50 }, { left: 22, top: 23 }],
    9: [{ left: 50, top: 13 }, { left: 75, top: 20 }, { left: 88, top: 43 }, { left: 84, top: 70 }, { left: 63, top: 87 }, { left: 37, top: 87 }, { left: 16, top: 70 }, { left: 12, top: 43 }, { left: 25, top: 20 }],
  };
  return layouts[count] || layouts[9];
}

function renderReplayActions(replay) {
  if (!els.replayActionList) return;
  const key = `${state.selectedReplayHandId}|${replay.actions.length}`;
  if (replayActionListKey !== key) {
    let lastStreet = "";
    els.replayActionList.innerHTML = replay.actions.map((action, index) => {
      const kind = action.type || action.verb || (/collected/.test(action.text) ? "collect" : /posts/.test(action.text) ? "post" : "");
      const street = action.street || "Preflop";
      const divider = street !== lastStreet ? `<span class="timeline-street">${escapeHtml(street)}</span>` : "";
      lastStreet = street;
      return `${divider}<button class="action-row-item ${kind}" data-action-index="${index}" type="button" title="${escapeAttr(action.text)}">
        <span>${fmt(index + 1)}</span>
        <strong>${escapeHtml(action.player || action.type || street)}</strong>
        <p>${escapeHtml(shortReplayActionText(action))}</p>
      </button>`;
    }).join("");
    replayActionListKey = key;
  }
  updateReplayActionActive();
}

function shortReplayActionText(action) {
  if (!action) return "";
  if (action.type === "street") return action.text || action.street || "";
  if (action.type === "showdown") return "Showdown";
  if (action.type === "show") return `Mostra ${action.cards?.join(" ") || ""}`;
  if (action.verb) return actionLabel(action, currentReplayContext?.selectedHand?.bigBlind || 0);
  if (/collected/.test(action.text || "")) return "Ganhou pote";
  if (/returned/.test(action.text || "")) return "Devolvido";
  return action.text || "";
}

function updateReplayActionActive() {
  els.replayActionList?.querySelectorAll?.("[data-action-index]").forEach((button) => {
    button.classList.toggle("is-active", Number(button.dataset.actionIndex) === state.selectedReplayActionIndex);
  });
}

function updateReplayHandActive() {
  if (!els.replayHandList) return;
  let activeButton = null;
  els.replayHandList.querySelectorAll("[data-hand-id]").forEach((button) => {
    const isActive = button.dataset.handId === state.selectedReplayHandId;
    button.classList.toggle("is-active", isActive);
    if (isActive) activeButton = button;
  });
  activeButton?.scrollIntoView?.({ block: "nearest" });
}

function updateReplayControls(replay, tournamentHands, hand) {
  const handIndex = hand ? tournamentHands.findIndex((item) => item.handId === hand.handId) : -1;
  const actionCount = replay?.actions?.length || 0;
  const actionNumber = actionCount ? state.selectedReplayActionIndex + 1 : 0;
  if (els.replayStepLabel) els.replayStepLabel.textContent = `${actionNumber} / ${actionCount}`;
  if (els.prevReplayHand) els.prevReplayHand.disabled = handIndex <= 0;
  if (els.nextReplayHand) els.nextReplayHand.disabled = handIndex < 0 || handIndex >= tournamentHands.length - 1;
  if (els.prevReplayAction) els.prevReplayAction.disabled = state.selectedReplayActionIndex <= 0;
  if (els.nextReplayAction) els.nextReplayAction.disabled = !actionCount || state.selectedReplayActionIndex >= actionCount - 1;
}

function replayStreetIndex(actions, street) {
  if (!Array.isArray(actions) || !actions.length) return 0;
  if (street === "Preflop") {
    const firstPreflop = actions.findIndex((action) => (action.street || "Preflop") === "Preflop");
    return Math.max(0, firstPreflop);
  }
  const exact = actions.findIndex((action) => action.street === street || (action.type === "street" && action.text?.startsWith(street)));
  return exact >= 0 ? exact : Math.max(0, actions.length - 1);
}

function clamp(value, min, max) {
  return Math.min(max, Math.max(min, value));
}

function selectAdjacentReplayHand(direction) {
  const tournaments = buildReplayTournaments(filterReplayHandsByDate(filteredHands()));
  const selected = tournaments.find((tournament) => tournament.id === state.selectedTournamentId);
  if (!selected) return;
  const hands = replayFilteredHands(selected.hands);
  const index = hands.findIndex((hand) => hand.handId === state.selectedReplayHandId);
  const next = hands[index + direction];
  if (!next) return;
  state.selectedReplayHandId = next.handId;
  state.selectedReplayActionIndex = 0;
  currentReplayContext = buildReplayContext(filteredHands());
  renderReplayStageOnly();
}

function isReplayKeyboardTargetEditable(target) {
  if (!(target instanceof Element)) return false;
  return Boolean(target.closest("input, textarea, select, [contenteditable='true'], [role='textbox']"));
}

function handleReplayArrowKey(event) {
  if (event.defaultPrevented) return;
  if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
  if (isReplayKeyboardTargetEditable(event.target)) return;
  if (els.statPopup && !els.statPopup.hidden) {
    if (!statPopupUi.selectedHandId) return;
    if (event.key === "ArrowUp" || event.key === "ArrowDown") {
      event.preventDefault();
      changeStatReplayHand(event.key === "ArrowUp" ? -1 : 1);
    } else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
      event.preventDefault();
      changeStatReplayAction(event.key === "ArrowLeft" ? -1 : 1);
    }
    return;
  }
  if (state.activeTab !== "replayer") return;

  if (event.key === "ArrowUp" || event.key === "ArrowDown") {
    event.preventDefault();
    selectAdjacentReplayHand(event.key === "ArrowUp" ? -1 : 1);
    return;
  }

  if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
  const hand = currentReplayContext?.selectedHand;
  if (!hand) return;
  const actionCount = getReplayHand(hand).actions.length;
  if (!actionCount) return;
  const direction = event.key === "ArrowLeft" ? -1 : 1;
  event.preventDefault();
  state.selectedReplayActionIndex = clamp(
    state.selectedReplayActionIndex + direction,
    0,
    actionCount - 1,
  );
  renderReplayStageOnly();
}

function escapeHtml(value) {
  return String(value ?? "").replace(/[&<>"']/g, (char) => ({
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#039;",
  }[char]));
}

function escapeAttr(value) {
  return escapeHtml(value);
}

function formatChips(value) {
  const prefix = value < 0 ? "-Chip" : "Chip";
  return `${prefix}${fmt(Math.abs(Math.round(value)))}`;
}

function renderTournamentTable() {
  if (!els.tournamentCount || !els.tournamentsTable) return;
  const sorted = [...state.tournaments].sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
  els.tournamentCount.textContent = `${fmt(sorted.length)} torneios`;
  els.tournamentsTable.innerHTML = sorted
    .slice(0, 300)
    .map((tournament) => {
      const profitClass = tournament.profit > 0 ? "positive" : tournament.profit < 0 ? "negative" : "";
      const format = displayTournamentFormat(tournament.format, tournament.name);
      return `<tr>
        <td>${tournament.startedAt || "-"}</td>
        <td>${tournament.name}</td>
        <td><span class="pill">${format}</span></td>
        <td>$${tournament.totalCost.toFixed(2)}</td>
        <td>${fmt(tournament.players)}</td>
        <td>${tournament.place ? fmt(tournament.place) : "-"}</td>
        <td>${fmt(tournament.reentries)}</td>
        <td>$${tournament.prize.toFixed(2)}</td>
        <td class="${profitClass}">$${tournament.profit.toFixed(2)}</td>
      </tr>`;
    })
    .join("");
}

function renderStackBars(hands) {
  if (!els.stackBars || !els.chartCaption) return;
  const stackHands = hands.filter((hand) => validHeroStackBb(hand) !== null);
  const buckets = [
    ["0-8", 0, 8],
    ["8-15", 8, 15],
    ["15-25", 15, 25],
    ["25-40", 25, 40],
    ["40+", 40, 999],
  ].map(([label, min, max]) => ({
    label,
    count: stackHands.filter((hand) => validHeroStackBb(hand) >= min && validHeroStackBb(hand) < max).length,
  }));
  const max = Math.max(1, ...buckets.map((bucket) => bucket.count));
  els.chartCaption.textContent = `${fmt(stackHands.length)} maos com stack do Hero`;
  els.stackBars.innerHTML = buckets
    .map(
      (bucket) => `
      <div class="bar-row">
        <span>${bucket.label} bb</span>
        <div class="bar-track"><div class="bar-fill" style="width:${(bucket.count / max) * 100}%"></div></div>
        <strong>${fmt(bucket.count)}</strong>
      </div>`
    )
    .join("");
}

function renderPositionTable(hands) {
  if (!els.positionTable) return;
  const profitMap = buildHandProfitMap(state.hands);
  const positions = ["UTG", "UTG1", "MP", "LJ", "HJ", "CO", "BTN", "SB", "BB"];
  const rows = positions.map((position) => {
    const rowHands = hands.filter((hand) => reportPosition(hand) === position);
    return buildPositionReportRow(position, rowHands, profitMap);
  }).filter((row) => row.hands > 0);
  els.positionTable.innerHTML = rows
    .map((row) => {
      return `<tr>
        <td><span class="pill">${row.position}</span></td>
        <td>${fmt(row.hands)}</td>
        <td class="${row.netWon >= 0 ? "positive" : "negative"}">${formatChips(row.netWon)}</td>
        <td class="${row.bb100 >= 0 ? "positive" : "negative"}">${row.bb100.toFixed(1)}</td>
        <td class="${row.evBb100 >= 0 ? "positive" : "negative"}">${row.evBb100.toFixed(1)}</td>
      </tr>`;
    })
    .join("");
}

function renderHandsTable(hands) {
  if (!els.handCount || !els.handsTable) return;
  els.handCount.textContent = `${fmt(hands.length)} maos`;
  els.handsTable.innerHTML = hands
    .slice(0, 500)
    .map((hand) => {
      const resultClass = hand.result > 0 ? "positive" : hand.result === 0 ? "negative" : "";
      return `<tr>
        <td>${hand.handId}</td>
        <td>${hand.date}</td>
        <td>${hand.tournamentId}</td>
        <td>${hand.hero}</td>
        <td><span class="pill">${hand.position}</span></td>
        <td>${validHeroStackBb(hand) ?? "-"} bb</td>
        <td>${hand.heroCards}</td>
        <td>${hand.preflopSummary}</td>
        <td class="${resultClass}">${hand.result === null ? "-" : fmt(hand.result)}</td>
      </tr>`;
    })
    .join("");
}

els.fileInput?.addEventListener("change", (event) => safeImportFiles(event.target.files));
els.detectAutoImportFolders?.addEventListener("click", detectAutoImportFolders);
els.chooseAutoImportFolder?.addEventListener("click", chooseAutoImportFolder);
els.toggleAutoImport?.addEventListener("click", toggleAutoImport);
els.graphTimeFilter?.addEventListener("change", () => {
  state.graphTimeFilter = els.graphTimeFilter.value || "all";
  safeRender();
  saveState({ silent: true });
});
els.graphDateFrom?.addEventListener("change", () => {
  state.graphDateFrom = els.graphDateFrom.value || "";
  safeRender();
  saveState({ silent: true });
});
els.graphDateTo?.addEventListener("change", () => {
  state.graphDateTo = els.graphDateTo.value || "";
  safeRender();
  saveState({ silent: true });
});
els.statsTimeFilter?.addEventListener("change", () => {
  state.statsTimeFilter = els.statsTimeFilter.value || "all";
  safeRender();
  saveState({ silent: true });
});
els.statsDateFrom?.addEventListener("change", () => {
  state.statsDateFrom = els.statsDateFrom.value || "";
  safeRender();
  saveState({ silent: true });
});
els.statsDateTo?.addEventListener("change", () => {
  state.statsDateTo = els.statsDateTo.value || "";
  safeRender();
  saveState({ silent: true });
});
els.dropzone?.addEventListener("dragover", (event) => {
  event.preventDefault();
  els.dropzone.classList.add("is-over");
});
els.dropzone?.addEventListener("dragleave", () => els.dropzone.classList.remove("is-over"));
els.dropzone?.addEventListener("drop", (event) => {
  event.preventDefault();
  els.dropzone.classList.remove("is-over");
  safeImportFiles(event.dataTransfer.files);
});
els.heroSelect?.addEventListener("change", () => {
  state.selectedHero = els.heroSelect.value;
  safeRender();
});
els.positionFilter?.addEventListener("change", safeRender);
els.stackFilter?.addEventListener("change", safeRender);
els.blindLevelFilter?.addEventListener("change", safeRender);
els.searchInput?.addEventListener("input", safeRender);
els.saveButton?.addEventListener("click", saveState);
els.resetButton?.addEventListener("click", resetState);
els.dashboardTab?.addEventListener("click", () => switchTab("dashboard"));
els.graphTab?.addEventListener("click", () => switchTab("graph"));
els.replayerTab?.addEventListener("click", () => switchTab("replayer"));
els.hudTab?.addEventListener("click", () => switchTab("hud"));
els.statsTab?.addEventListener("click", () => switchTab("stats"));
els.hudToggle?.addEventListener("click", toggleHudEnabled);
els.openFloatingHudButton?.addEventListener("click", syncFloatingHud);
els.hudSecondRowToggle?.addEventListener("change", toggleHudSecondRow);
els.hudPlayerSelect?.addEventListener("change", renderHudPreview);
els.hudFloatingOverlay?.addEventListener("click", (event) => {
  const button = event.target.closest("[data-hud-scale]");
  if (!button) return;
  changeFloatingHudScale(button.dataset.hudScale || "reset");
});
els.hudConfigGrid?.addEventListener("change", (event) => {
  const select = event.target.closest("[data-hud-slot]");
  if (!select) return;
  setHudStat(Number(select.dataset.hudSlot), select.value);
});
els.hudSecondConfigGrid?.addEventListener("change", (event) => {
  const select = event.target.closest("[data-hud-second-slot]");
  if (!select) return;
  setHudSecondRowStat(Number(select.dataset.hudSecondSlot), select.value);
});
document.querySelectorAll(".stats-nav-button").forEach((button) => {
  button.addEventListener("click", () => switchStatsSection(button.dataset.statsSection || "overview"));
});
document.querySelectorAll("[data-srp-scenario]").forEach((button) => {
  button.addEventListener("click", () => {
    state.srpScenario = button.dataset.srpScenario || "defender-oop";
    srpSelectedPath = "all";
    safeRender();
  });
});
document.querySelectorAll("[data-srp-street]").forEach((button) => {
  button.addEventListener("click", () => {
    state.srpStreet = button.dataset.srpStreet || "flop";
    srpSelectedPath = "all";
    safeRender();
  });
});
els.srpPathSelect?.addEventListener("change", () => {
  srpSelectedPath = els.srpPathSelect.value || "all";
  safeRender();
});
document.querySelectorAll("[data-three-bet-pot-scenario]").forEach((button) => {
  button.addEventListener("click", () => {
    state.threeBetPotScenario = button.dataset.threeBetPotScenario || "defender-oop";
    threeBetPotSelectedPath = "all";
    safeRender();
  });
});
document.querySelectorAll("[data-three-bet-pot-street]").forEach((button) => {
  button.addEventListener("click", () => {
    state.threeBetPotStreet = button.dataset.threeBetPotStreet || "flop";
    threeBetPotSelectedPath = "all";
    safeRender();
  });
});
els.threeBetPotPathSelect?.addEventListener("change", () => {
  threeBetPotSelectedPath = els.threeBetPotPathSelect.value || "all";
  safeRender();
});
document.querySelectorAll("[data-blind-war-side]").forEach((button) => {
  button.addEventListener("click", () => {
    state.blindWarSide = button.dataset.blindWarSide || "bb";
    safeRender();
  });
});
els.statsView?.addEventListener("click", (event) => {
  const row = event.target.closest("[data-stat-popup]");
  if (!row) return;
  showStatPopup(row.dataset.statPopup || "");
});
els.statsView?.addEventListener("keydown", (event) => {
  if (event.key !== "Enter" && event.key !== " ") return;
  const row = event.target.closest("[data-stat-popup]");
  if (!row) return;
  event.preventDefault();
  showStatPopup(row.dataset.statPopup || "");
});
els.statPopupClose?.addEventListener("click", closeStatPopup);
els.statPopup?.addEventListener("click", (event) => {
  if (event.target === els.statPopup) {
    closeStatPopup();
    return;
  }
  const tab = event.target.closest("[data-stat-popup-tab]");
  if (tab) {
    if (els.statPopupHandsPanel && !els.statPopupHandsPanel.hidden) closeStatHands();
    statPopupUi.tab = tab.dataset.statPopupTab || "range";
    renderStatPopupContent();
    return;
  }
  if (event.target.closest("[data-stat-open-hands]")) {
    openStatHands();
    return;
  }
  const betSize = event.target.closest("[data-stat-bet-bucket]");
  if (betSize) {
    openStatHands("bet-size", betSize.dataset.statBetBucket || "");
    return;
  }
  const range = event.target.closest("[data-stat-range]");
  if (range) {
    openStatHands("combo", range.dataset.statRange || "");
    return;
  }
  const distribution = event.target.closest("[data-stat-filter-type]");
  if (distribution) {
    openStatHands(distribution.dataset.statFilterType || "", distribution.dataset.statFilterValue || "");
    return;
  }
  const hand = event.target.closest("[data-popup-hand-id]");
  if (hand) {
    openStatReplay(hand.dataset.popupHandId || "");
    return;
  }
  if (event.target.closest("[data-stat-load-more]")) {
    statPopupUi.listLimit += 250;
    renderStatHandList();
    return;
  }
  if (event.target.closest("[data-stat-close-replay]")) {
    closeStatReplay();
    return;
  }
  if (event.target.closest("[data-stat-close-hands]")) {
    closeStatHands();
    return;
  }
  const replayControl = event.target.closest("[data-stat-replay]");
  if (replayControl) {
    const action = replayControl.dataset.statReplay;
    if (action === "prev-hand") changeStatReplayHand(-1);
    if (action === "next-hand") changeStatReplayHand(1);
    if (action === "prev-action") changeStatReplayAction(-1);
    if (action === "next-action") changeStatReplayAction(1);
    return;
  }
  const street = event.target.closest("[data-stat-replay-street]");
  if (street) {
    const selected = statPopupUi.filteredHands.find((item) => item.handId === statPopupUi.selectedHandId);
    if (!selected) return;
    statPopupUi.actionIndex = replayStreetIndex(getReplayHand(selected).actions, street.dataset.statReplayStreet || "Preflop");
    renderStatReplay();
  }
});
document.addEventListener?.("keydown", (event) => {
  if (event.key === "Escape" && els.statPopup && !els.statPopup.hidden) closeStatPopup();
  handleReplayArrowKey(event);
});
window.addEventListener?.("resize", () => {
  if (els.graphView?.classList.contains("is-active")) renderResultsGraph(graphFilteredHands(filteredHands()));
  if (document.body?.classList.contains("hud-overlay-window-mode")) renderHudFloatingOverlay();
});
els.tournamentSearch?.addEventListener("input", renderReplayOnly);
function resetReplaySelectionAndRender() {
  state.selectedTournamentId = "";
  state.selectedReplayHandId = "";
  state.selectedReplayActionIndex = 0;
  renderReplayOnly();
}
els.replayDateFrom?.addEventListener("change", resetReplaySelectionAndRender);
els.replayDateTo?.addEventListener("change", resetReplaySelectionAndRender);
els.tournamentTypeFilter?.addEventListener("change", () => {
  state.selectedTournamentId = "";
  state.selectedReplayHandId = "";
  state.selectedReplayActionIndex = 0;
  renderReplayOnly();
});
els.replayHandFilter?.addEventListener("change", () => {
  state.selectedReplayHandId = "";
  state.selectedReplayActionIndex = 0;
  renderReplayOnly();
});
els.tournamentList?.addEventListener("click", (event) => {
  const button = event.target.closest("[data-tournament-id]");
  if (!button) return;
  state.selectedTournamentId = button.dataset.tournamentId || "";
  state.selectedReplayHandId = "";
  state.selectedReplayActionIndex = 0;
  renderReplayOnly();
});
els.replayHandList?.addEventListener("click", (event) => {
  const button = event.target.closest("[data-hand-id]");
  if (!button) return;
  state.selectedReplayHandId = button.dataset.handId || "";
  state.selectedReplayActionIndex = 0;
  currentReplayContext = buildReplayContext(filteredHands());
  renderReplayStageOnly();
});
els.replayActionList?.addEventListener("click", (event) => {
  const button = event.target.closest("[data-action-index]");
  if (!button) return;
  state.selectedReplayActionIndex = Number(button.dataset.actionIndex) || 0;
  renderReplayStageOnly();
});
els.replayNotes?.addEventListener("click", (event) => {
  const button = event.target.closest("[data-replay-note]");
  if (!button || !currentReplayContext?.selectedHand) return;
  toggleReplayNote(currentReplayContext.selectedHand.handId, button.dataset.replayNote || "");
});
els.prevReplayAction?.addEventListener("click", () => {
  state.selectedReplayActionIndex = Math.max(0, state.selectedReplayActionIndex - 1);
  renderReplayStageOnly();
});
els.nextReplayAction?.addEventListener("click", () => {
  state.selectedReplayActionIndex += 1;
  renderReplayStageOnly();
});
document.querySelector(".replay-controls")?.addEventListener("click", (event) => {
  const button = event.target.closest("[data-replay-street]");
  if (!button || !currentReplayContext?.selectedHand) return;
  const replay = getReplayHand(currentReplayContext.selectedHand);
  const street = button.dataset.replayStreet || "Preflop";
  state.selectedReplayActionIndex = replayStreetIndex(replay.actions, street);
  renderReplayStageOnly();
});
els.replayStackUnit?.addEventListener("change", () => {
  state.replayStackUnit = els.replayStackUnit.value || "bb";
  renderReplayStageOnly();
});

els.statPopupReplayUnit?.addEventListener("change", () => {
  statPopupUi.stackUnit = els.statPopupReplayUnit.value || "bb";
  renderStatReplay();
});
els.openReplayWindowButton?.addEventListener("click", async () => {
  if (window.desktopStore?.openReplayer) {
    const saved = await saveState({ silent: true });
    if (!saved) return;
    await window.desktopStore.openReplayer();
    setImportStatus("Replayer aberto em uma janela separada.", "success");
    return;
  }
  window.open(`${window.location.pathname}?mode=replayer`, "mtt-edge-replayer", "width=1180,height=820");
});
els.prevReplayHand?.addEventListener("click", () => selectAdjacentReplayHand(-1));
els.nextReplayHand?.addEventListener("click", () => selectAdjacentReplayHand(1));

async function bootApp() {
  const isHudOverlay = /(?:^\?|&)mode=hud-overlay(?:&|$)/.test(window.location?.search || "");
  if (isHudOverlay) {
    applyWindowMode();
    const key = new URLSearchParams(window.location?.search || "").get("hudKey") || "manual";
    const savedScale = Number(localStorage.getItem(`hud-scale-${key}`));
    if (Number.isFinite(savedScale) && savedScale > 0) hudOverlayScaleOverride = clampHudScale(savedScale);
    const payload = await window.desktopStore?.getHudPayload?.();
    applyHudPayload(payload);
    window.desktopStore?.onHudPayload?.(applyHudPayload);
    hideBootScreen();
    return;
  }
  setBootStatus("Carregando dados salvos...");
  await nextFrame();
  await loadState();
  startOpponentHudIndex();
  setBootStatus(`Preparando ${fmt(state.hands.length)} maos...`);
  await nextFrame();
  safeRender();
  setBootStatus("Montando interface...");
  await nextFrame();
  updateAutoImportUi();
  applyWindowMode();
  if (state.hudEnabled) window.setTimeout(() => {
    ensureHudAutoImport().then(syncFloatingHud);
  }, 350);
  setBootStatus("Pronto.");
  window.setTimeout(hideBootScreen, 120);
}

bootApp();

