const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const { execFile } = require("child_process");
const fs = require("fs/promises");
const fsSync = require("fs");
const path = require("path");
const { pathToFileURL } = require("url");
const { TextDecoder } = require("util");

const STORE_FILE = "mtt-edge-lab-state.json";
const BETA_TRIAL_FILE = "mtt-edge-lab-beta-trial.json";
const BETA_RELEASE_ID = "beta-15d-0.2.153-20260716-r1";
const BETA_TRIAL_DAYS = 15;
const BETA_TRIAL_MS = BETA_TRIAL_DAYS * 24 * 60 * 60 * 1000;
const HAND_HISTORY_EXTENSIONS = new Set([".txt", ".log", ".xml", ".hh", ".hhs"]);
const HUD_BOUNDS_FILE = "mtt-edge-lab-hud-window.json";
const HUD_LAYOUTS_FILE = "mtt-edge-lab-hud-layouts.json";
const HUD_LAYOUT_VERSION = 5;
const HUD_SITE_POLICIES = [
  { site: "GG", status: "blocked", pattern: /\b(ggpoker|gg poker|natural8|natural 8|ggnetwork)\b/i },
  { site: "PartyPoker", status: "blocked", pattern: /\bparty\s*poker\b|\bpartypoker\b/i },
  { site: "PokerStars", status: "allowed", pattern: /\bpokerstars\b/i },
  { site: "iPoker", status: "allowed", pattern: /\b(champion\s*poker|championpoker|ipoker|bet365|betfair\s*poker|william\s*hill|paddy\s*power|netbet|boyle\s*sports|boylesports|coral\s*poker|grosvenor\s*poker|betfred|redstar\s*poker|titan\s*poker|winner\s*poker|mansion\s*poker)\b/i },
  { site: "WPN", status: "allowed", pattern: /\bpokerking\b|\bwinning\s*poker\b|\bwpn\b|\bacr(?:\s*poker)?\b|\bacrpoker\b|\bblack\s*chip\b|\bamerica'?s\s*cardroom\b|\bamericascardroom\b/i },
  { site: "Chico", status: "allowed", pattern: /\b(tiger\s*gaming|tigergaming|bet\s*online|betonline|sports\s*betting|sportsbetting|chico)\b/i },
];
const DETECT_FOLDER_NAMES = [
  "HandHistory",
  "Hand Histories",
  "HandHistories",
  "History",
  "TournSummary",
  "TournSummaries",
  "TournamentSummary",
  "Tournament Summary",
  "TournamentSummaries",
  "Tournament Summaries",
  "TournamentResults",
  "Tournament Results",
  "Summaries",
  "Resumos",
];

function decodeHandHistoryBytes(bytes) {
  const data = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || []);
  try {
    return new TextDecoder("utf-8", { fatal: true }).decode(data);
  } catch {}
  try {
    return new TextDecoder("windows-1252").decode(data);
  } catch {}
  return data.toString("utf8");
}
let mainWindow = null;
const replayWindows = new Set();
const hudWindows = new Map();
const hudWindowInstances = new Set();
let latestHudPayload = null;
let hudLayouts = {};
let hudGlobalScales = {};
let hudOpacities = {};
let hudLayoutsLoaded = false;
let hudLayoutsSaveTimer = null;
let hudSyncInFlight = null;
let betaTrialStatus = null;
let betaTrialTimer = null;

function hudPayloadForWindow(window) {
  if (!latestHudPayload || typeof latestHudPayload !== "object") return latestHudPayload;
  const siteKey = hudSiteFamilyKey(window?.hudSiteKey || "default");
  const activeTable = hudTablePayloadForTitle(window?.hudTableTitle || "");
  return {
    ...latestHudPayload,
    activeTable,
    tableTitle: window?.hudTableTitle || "",
    playerName: window?.hudPlayerName || "",
    slotKey: window?.hudSlotKey || "manual",
    slotScale: Number(hudLayouts[window?.hudSlotKey]?.scale) || 1,
    layouts: hudLayouts,
    siteKey,
    globalScale: Number(hudGlobalScales[siteKey]) || 1,
    opacity: Math.max(0.35, Math.min(1, Number(hudOpacities[siteKey]) || 1)),
    stableCompositor: Boolean(window?.hudStableCompositor),
  };
}
const hudBoundsSaveTimers = new Map();
const appDataPath = path.join(app.getPath("appData"), "MTT Edge Lab");
const appIconPath = path.join(__dirname, "assets", "logo-mtt-edge-lab-1024.png");

app.setPath("userData", appDataPath);
app.commandLine.appendSwitch("disk-cache-dir", path.join(appDataPath, "Cache"));
app.commandLine.appendSwitch("disable-gpu-shader-disk-cache");

const hasSingleInstanceLock = app.requestSingleInstanceLock();
if (!hasSingleInstanceLock) app.quit();

function diagnosticLogPath() {
  return path.join(app.getPath("userData"), "mtt-edge-lab-diagnostic.log");
}

function logDiagnostic(event, details = "") {
  try {
    fsSync.mkdirSync(app.getPath("userData"), { recursive: true });
    const text = typeof details === "string" ? details : JSON.stringify(details);
    fsSync.appendFileSync(diagnosticLogPath(), `[${new Date().toISOString()}] ${event}${text ? `: ${text}` : ""}\r\n`, "utf8");
  } catch {}
}

process.on("uncaughtException", (error) => {
  logDiagnostic("uncaughtException", error?.stack || error?.message || String(error));
});
process.on("unhandledRejection", (error) => {
  logDiagnostic("unhandledRejection", error?.stack || error?.message || String(error));
});

function storePath() {
  return path.join(app.getPath("userData"), STORE_FILE);
}

function betaTrialPath() {
  return path.join(app.getPath("userData"), BETA_TRIAL_FILE);
}

async function ensureBetaTrial(now = Date.now()) {
  let saved = {};
  try {
    saved = JSON.parse(await fs.readFile(betaTrialPath(), "utf8"));
  } catch (error) {
    if (error.code !== "ENOENT") logDiagnostic("beta-trial-read-error", error?.message || String(error));
  }

  const savedStartedAt = Date.parse(saved.startedAt || "");
  const isCurrentRelease = saved.releaseId === BETA_RELEASE_ID && Number.isFinite(savedStartedAt);
  const startedAt = isCurrentRelease ? savedStartedAt : now;
  const lastSeenAt = isCurrentRelease ? Date.parse(saved.lastSeenAt || "") : now;
  const effectiveNow = Math.max(now, Number.isFinite(lastSeenAt) ? lastSeenAt : now);
  const expiresAt = startedAt + BETA_TRIAL_MS;
  const remainingMs = expiresAt - effectiveNow;
  const status = {
    releaseId: BETA_RELEASE_ID,
    days: BETA_TRIAL_DAYS,
    valid: remainingMs > 0,
    daysLeft: Math.max(0, Math.ceil(remainingMs / (24 * 60 * 60 * 1000))),
    startedAt: new Date(startedAt).toISOString(),
    expiresAt: new Date(expiresAt).toISOString(),
  };

  await fs.mkdir(path.dirname(betaTrialPath()), { recursive: true });
  await fs.writeFile(betaTrialPath(), JSON.stringify({
    releaseId: BETA_RELEASE_ID,
    startedAt: status.startedAt,
    expiresAt: status.expiresAt,
    lastSeenAt: new Date(effectiveNow).toISOString(),
  }, null, 2), "utf8");
  return status;
}

async function enforceBetaTrial() {
  betaTrialStatus = await ensureBetaTrial();
  if (betaTrialStatus.valid) return true;
  closeAuxiliaryWindows();
  if (mainWindow && !mainWindow.isDestroyed()) mainWindow.close();
  await dialog.showMessageBox({
    type: "info",
    title: "MTT Edge Lab - Beta encerrada",
    message: "Seu teste de 15 dias terminou.",
    detail: "Para continuar usando ou receber uma nova versao de teste, fale comigo pelo Discord.",
    buttons: ["OK"],
  });
  app.quit();
  return false;
}

function safeHudKey(key = "manual") {
  return String(key || "manual").replace(/[^a-z0-9_-]+/gi, "_").slice(0, 80) || "manual";
}

function hudSiteFamilyKey(site = "default") {
  const value = safeHudKey(site).toLowerCase();
  if (/^(champion|ipoker|bet365|betfair|william_hill|paddy_power|netbet|boyle_sports|boylesports|coral_poker|grosvenor_poker|betfred|redstar_poker|titan_poker|winner_poker|mansion_poker)$/.test(value)) return "ipoker";
  if (/^(pokerking|wpn|acr|acr_poker|americascardroom|black_chip|winning_poker)$/.test(value)) return "wpn";
  return value;
}

function tableIdFromWindowTitle(title) {
  const value = String(title || "");
  return value.match(/\((\d+)\)\s*$/)?.[1]
    || value.match(/\bTournament\s*#?\s*(\d{5,})\b/i)?.[1]
    || value.match(/\b(?:SCHEDULEDID-G|SITGOID-G|T)(\d{6,})\b/i)?.[1]
    || value.match(/\b(\d{8,})\b/)?.[1]
    || "";
}

function normalizeHudTableName(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/\b\d{8,}\b/g, " ")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, " ")
    .trim();
}

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

function hudTablePayloadForTitle(title) {
  const tables = latestHudPayload?.tables || {};
  const tableId = tableIdFromWindowTitle(title);
  if (tableId && tables[tableId]) return tables[tableId];
  const normalizedTitle = normalizeHudTableName(title);
  if (!normalizedTitle) return null;
  const candidates = [...new Set(Object.values(tables))].filter((table) => {
    const name = normalizeHudTableName(table?.tableName);
    return name.length >= 6 && (normalizedTitle.includes(name) || name.includes(normalizedTitle));
  });
  return candidates.sort((left, right) => compareHudTableRecency(right, left))[0] || null;
}

function hudBoundsPath(key = "manual") {
  const suffix = safeHudKey(key);
  return path.join(app.getPath("userData"), suffix === "manual" ? HUD_BOUNDS_FILE : `mtt-edge-lab-hud-window-${suffix}.json`);
}

async function readHudBounds(key = "manual") {
  try {
    const bounds = JSON.parse(await fs.readFile(hudBoundsPath(key), "utf8"));
    if (Number.isFinite(bounds.width) && Number.isFinite(bounds.height)) return bounds;
  } catch {}
  return { width: 360, height: 150 };
}

function scheduleHudBoundsSave(window, key = "manual") {
  if (!window || window.isDestroyed()) return;
  const safeKey = safeHudKey(key);
  const currentTimer = hudBoundsSaveTimers.get(safeKey);
  if (currentTimer) clearTimeout(currentTimer);
  const nextTimer = setTimeout(async () => {
    if (!window || window.isDestroyed()) return;
    await fs.mkdir(path.dirname(hudBoundsPath(safeKey)), { recursive: true });
    await fs.writeFile(hudBoundsPath(safeKey), JSON.stringify(window.getBounds()), "utf8");
  }, 220);
  hudBoundsSaveTimers.set(safeKey, nextTimer);
}

async function listHandHistoryFiles(folderPath, seen = {}) {
  const maxFiles = 20;
  const maxBytes = 16 * 1024 * 1024;
  const results = [];
  const candidates = [];
  async function walk(currentPath) {
    let entries = [];
    try {
      entries = await fs.readdir(currentPath, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      const fullPath = path.join(currentPath, entry.name);
      if (entry.isDirectory()) {
        if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
        await walk(fullPath);
        continue;
      }
      if (!entry.isFile()) continue;
      const ext = path.extname(entry.name).toLowerCase();
      if (!HAND_HISTORY_EXTENSIONS.has(ext)) continue;
      const stat = await fs.stat(fullPath);
      const signature = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
      if (seen[fullPath] === signature) continue;
      candidates.push({ path: fullPath, name: entry.name, signature, size: stat.size, mtimeMs: stat.mtimeMs });
    }
  }
  await walk(folderPath);
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
  let loadedBytes = 0;
  for (const candidate of candidates) {
    if (results.length >= maxFiles || (results.length && loadedBytes + candidate.size > maxBytes)) break;
    if (candidate.size > maxBytes) {
      logDiagnostic("auto-import-file-too-large", { path: candidate.path, size: candidate.size });
      continue;
    }
    const text = decodeHandHistoryBytes(await fs.readFile(candidate.path));
    loadedBytes += candidate.size;
    results.push({ path: candidate.path, name: candidate.name, signature: candidate.signature, text });
  }
  return results;
}

function runArchiveTool(args) {
  const executable = process.platform === "win32" ? "tar.exe" : "tar";
  return new Promise((resolve, reject) => {
    execFile(executable, args, {
      windowsHide: true,
      timeout: 120000,
      maxBuffer: 256 * 1024 * 1024,
      encoding: null,
    }, (error, stdout, stderr) => {
      if (error) {
        const detail = Buffer.isBuffer(stderr) ? stderr.toString("utf8").trim() : String(stderr || "").trim();
        reject(new Error(detail || "Nao foi possivel abrir o arquivo RAR."));
        return;
      }
      resolve(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout || ""));
    });
  });
}

async function readRarTextEntries(archivePath) {
  if (!archivePath || path.extname(archivePath).toLowerCase() !== ".rar") {
    throw new Error("Arquivo RAR invalido.");
  }
  const stat = await fs.stat(archivePath);
  if (!stat.isFile()) throw new Error("Arquivo RAR nao encontrado.");

  const listed = decodeHandHistoryBytes(await runArchiveTool(["-tf", archivePath]));
  const entries = listed
    .split(/\r?\n/)
    .map((name) => name.trim())
    .filter(Boolean)
    .filter((name) => HAND_HISTORY_EXTENSIONS.has(path.extname(name).toLowerCase()));
  if (!entries.length) throw new Error("O RAR nao possui arquivos .txt, .log, .xml, .hh ou .hhs suportados.");

  const result = [];
  for (const name of entries) {
    const bytes = await runArchiveTool(["-xOf", archivePath, name]);
    result.push({ name, text: decodeHandHistoryBytes(bytes) });
  }
  return result;
}

async function pathExists(folderPath) {
  try {
    const stat = await fs.stat(folderPath);
    return stat.isDirectory();
  } catch {
    return false;
  }
}

async function countHandHistoryFiles(folderPath, maxDepth = 4) {
  let count = 0;
  async function walk(currentPath, depth) {
    if (depth > maxDepth || count >= 200) return;
    let entries = [];
    try {
      entries = await fs.readdir(currentPath, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      const fullPath = path.join(currentPath, entry.name);
      if (entry.isDirectory()) {
        if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
        await walk(fullPath, depth + 1);
        continue;
      }
      if (entry.isFile() && HAND_HISTORY_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) count += 1;
      if (count >= 200) return;
    }
  }
  await walk(folderPath, 0);
  return count;
}

function siteFromPath(folderPath) {
  const value = folderPath.toLowerCase();
  if (value.includes("pokerstars")) return "PokerStars";
  if (value.includes("pokerking")) return "PokerKing";
  if (value.includes("champion") || value.includes("ipoker") || value.includes("bet365") || value.includes("william hill") || value.includes("williamhill") || value.includes("betfair") || value.includes("paddy power") || value.includes("paddypower")) return "iPoker";
  if (value.includes("partypoker") || value.includes("party poker")) return "PartyPoker";
  if (value.includes("ggpoker") || value.includes("ggnetwork") || value.includes("natural8")) return "GG";
  if (value.includes("winning poker") || value.includes("wpn") || value.includes("americascardroom") || value.includes("acr poker")) return "WPN";
  if (value.includes("tigergaming") || value.includes("tiger gaming") || value.includes("betonline") || value.includes("bet online") || value.includes("sportsbetting") || value.includes("sports betting") || value.includes("chico")) return "Chico";
  return "Poker";
}

function hudPolicyForTitle(title) {
  const policy = HUD_SITE_POLICIES.find((item) => item.pattern.test(title || ""));
  return policy || { site: "Desconhecido", status: "blocked" };
}

function looksLikePokerStarsTableTitle(title = "") {
  const value = String(title || "");
  if (/\b(lobby|caixa|cashier)\b/i.test(value)) return false;
  return (
    /\b(?:tournament|torneio)\s+#?\d+\b.*\b(?:table|mesa)\s+\d+\b/i.test(value)
    || /\b(?:table|mesa)\s+\d+\b.*\b(?:logged\s+in\s+as|logado\s+como)\b/i.test(value)
    || /\b(?:hold'?em|omaha|stud|zoom)\b.*\b\d[\d,.]*\s*\/\s*\d[\d,.]*\b/i.test(value)
  );
}

function runPowerShell(script) {
  return new Promise((resolve) => {
    execFile("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 8000, maxBuffer: 1024 * 512 }, (error, stdout) => {
      if (error) {
        resolve([]);
        return;
      }
      try {
        const parsed = JSON.parse(stdout || "[]");
        resolve(Array.isArray(parsed) ? parsed : [parsed]);
      } catch {
        resolve([]);
      }
    });
  });
}

async function attachHudToPokerTable(window, tableHandle) {
  if (!window || window.isDestroyed() || !tableHandle) return false;
  const nativeHandle = window.getNativeWindowHandle();
  const childHandle = nativeHandle.length >= 8 ? nativeHandle.readBigUInt64LE(0).toString() : String(nativeHandle.readUInt32LE(0));
  const ownerHandle = String(tableHandle).replace(/\D/g, "");
  if (!ownerHandle) return false;
  const script = `
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class HudOwnerWindow {
  [DllImport("user32.dll", EntryPoint="SetWindowLongPtrW")]
  public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr newLong);
  [DllImport("user32.dll")]
  public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
}
"@
[void][HudOwnerWindow]::SetWindowLongPtr([IntPtr]${childHandle}, -8, [IntPtr]${ownerHandle})
$currentOwner = [HudOwnerWindow]::GetWindow([IntPtr]${childHandle}, 4).ToInt64()
@{ ok = ($currentOwner -eq ${ownerHandle}); owner = $currentOwner } | ConvertTo-Json
`;
  const result = await runPowerShell(script);
  const attached = result?.[0]?.ok === true;
  if (!attached) logDiagnostic("hud-owner-attach-failed", { tableHandle: ownerHandle, childHandle, result: result?.[0] || null });
  return attached;
}

function updateHudWindowZOrder(window, tableWindow) {
  if (!window || window.isDestroyed()) return;
  window.setSkipTaskbar(true);
  const isActuallyTopmost = typeof window.isAlwaysOnTop === "function"
    ? window.isAlwaysOnTop()
    : window.hudTopmost === true;
  const now = Date.now();
  const isInteracting = window.hudDragging || now < (window.hudInteractionUntil || 0);
  const isSiteForeground = tableWindow?.isForegroundProcess || tableWindow?.isHudForeground;
  if (isSiteForeground) window.hudLastTableForegroundAt = now;
  const keepVisibleForTableFocusHandoff = now - (window.hudLastTableForegroundAt || 0) < 450;
  const shouldBeVisible = isInteracting || isSiteForeground || keepVisibleForTableFocusHandoff;
  const setTopmost = (enabled) => {
    if (enabled) {
      if (!isActuallyTopmost || window.hudTopmost !== true) {
        window.setAlwaysOnTop(true, window.hudSiteKey === "pokerstars" ? "floating" : "floating");
        window.hudTopmost = true;
      }
      return;
    }
    if (isActuallyTopmost || window.hudTopmost !== false) {
      window.setAlwaysOnTop(false);
      window.hudTopmost = false;
    }
  };
  if (window.hudOwnerAttached) {
    setTopmost(false);
    // When native ownership works, Windows keeps the HUD behind unrelated
    // foreground apps by itself. Hiding/showing these attached overlays on
    // every focus sample makes stable clients like ACR flicker.
    if (!window.isVisible()) window.showInactive();
    return;
  }
  if (shouldBeVisible) {
    if (!window.hudAwaitingInitialShape && !window.isVisible()) window.showInactive();
    setTopmost(Boolean(isSiteForeground || isInteracting));
  } else {
    setTopmost(false);
    if (window.isVisible()) window.hide();
  }
}

function applyHudTableBounds(window, tableWindow) {
  if (!window || window.isDestroyed() || !tableWindow) return false;
  const next = {
    x: Math.round(Number(tableWindow.x) || 0),
    y: Math.round(Number(tableWindow.y) || 0),
    width: Math.max(1, Math.round(Number(tableWindow.width) || 1)),
    height: Math.max(1, Math.round(Number(tableWindow.height) || 1)),
  };
  const current = window.getBounds();
  const changedEnough = Math.abs(current.x - next.x) > 1
    || Math.abs(current.y - next.y) > 1
    || Math.abs(current.width - next.width) > 1
    || Math.abs(current.height - next.height) > 1;
  if (!changedEnough) return false;
  if (
    current.x === next.x
    && current.y === next.y
    && current.width === next.width
    && current.height === next.height
  ) return false;
  window.hudApplyingBounds = true;
  window.setBounds(next, false);
  window.hudApplyingBounds = false;
  return true;
}

async function detectPokerTableWindows() {
  const script = `
Add-Type @"
using System;
using System.Text;
using System.Runtime.InteropServices;
public class Win32 {
  public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
  [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
  [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
  [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
  [DllImport("user32.dll")] public static extern int GetWindowTextLength(IntPtr hWnd);
  [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
  [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
  public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
}
"@
$items = New-Object System.Collections.Generic.List[object]
$foregroundPid = 0
$foregroundParentPid = 0
$foregroundWindow = [Win32]::GetForegroundWindow()
if ($foregroundWindow -ne [IntPtr]::Zero) {
  [void][Win32]::GetWindowThreadProcessId($foregroundWindow, [ref]$foregroundPid)
  try { $foregroundParentPid = (Get-CimInstance Win32_Process -Filter "ProcessId=$foregroundPid" -ErrorAction Stop).ParentProcessId } catch {}
}
[Win32]::EnumWindows({
  param($hWnd, $lParam)
  if (-not [Win32]::IsWindowVisible($hWnd)) { return $true }
  $len = [Win32]::GetWindowTextLength($hWnd)
  if ($len -le 0) { return $true }
  $sb = New-Object System.Text.StringBuilder ($len + 1)
  [void][Win32]::GetWindowText($hWnd, $sb, $sb.Capacity)
  $title = $sb.ToString()
  $isPokerLobby = $title -match '(?i)\b(lobby|caixa|cashier)\b'
  $processId = 0
  [void][Win32]::GetWindowThreadProcessId($hWnd, [ref]$processId)
  $proc = $null
  $procPath = $null
  try {
    $processInfo = Get-Process -Id $processId -ErrorAction SilentlyContinue
    $proc = $processInfo.ProcessName
    $procPath = $processInfo.Path
  } catch {}
  if (("$title $proc $procPath") -notmatch '(PokerStars|PokerKing|Champion\\s*Poker|ChampionPoker|iPoker|Bet365|Betfair\\s*Poker|William\\s*Hill|Paddy\\s*Power|NetBet|Boyle\\s*Sports|Boylesports|Coral\\s*Poker|Grosvenor\\s*Poker|Betfred|Redstar\\s*Poker|Titan\\s*Poker|Winner\\s*Poker|Mansion\\s*Poker|PartyPoker|Party\\s*Poker|GGPoker|GG\\s*Poker|Natural8|GGnet|Winning\\s*Poker|America''s\\s*Cardroom|AmericasCardroom|ACR(?:\\s*Poker)?|ACRPoker|Black\\s*Chip|TigerGaming|Tiger\\s*Gaming|BetOnline|Bet\\s*Online|SportsBetting|Sports\\s*Betting|Chico)') { return $true }
  $rect = New-Object Win32+RECT
  [void][Win32]::GetWindowRect($hWnd, [ref]$rect)
  if (($rect.Right - $rect.Left) -lt 220 -or ($rect.Bottom - $rect.Top) -lt 160) { return $true }
  $items.Add([pscustomobject]@{
    handle = $hWnd.ToInt64()
    title = $title
    process = $proc
    processPath = $procPath
    isPokerLobby = $isPokerLobby
    isForegroundProcess = ($processId -eq $foregroundPid -or $processId -eq $foregroundParentPid)
    foregroundHandle = $foregroundWindow.ToInt64()
    x = $rect.Left
    y = $rect.Top
    width = ($rect.Right - $rect.Left)
    height = ($rect.Bottom - $rect.Top)
  })
  return $true
}, [IntPtr]::Zero) | Out-Null
$items | ConvertTo-Json -Depth 3
`;
  const windows = await runPowerShell(script);
  return windows.map((item) => {
    const policy = hudPolicyForTitle(`${item.title || ""} ${item.process || ""} ${item.processPath || ""}`);
    const isPokerStars = policy.site === "PokerStars";
    const isPokerLobby = Boolean(item.isPokerLobby) || (isPokerStars && !looksLikePokerStarsTableTitle(item.title));
    return {
      ...item,
      site: policy.site,
      policy: isPokerLobby ? "ignored" : policy.status,
      isPokerLobby,
      key: `${policy.site}-${item.handle}`,
    };
  });
}

function uniquePaths(paths) {
  const seen = new Set();
  return paths.filter((folderPath) => {
    const key = folderPath.toLowerCase();
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

async function discoverNamedFolders(basePath, depth = 3) {
  const results = [];
  async function walk(currentPath, currentDepth) {
    if (currentDepth > depth) return;
    let entries = [];
    try {
      entries = await fs.readdir(currentPath, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const fullPath = path.join(currentPath, entry.name);
      if (DETECT_FOLDER_NAMES.some((name) => entry.name.toLowerCase() === name.toLowerCase())) results.push(fullPath);
      if (/poker|stars|king|champion|ipoker|bet365|betfair|william|paddy|netbet|boyle|coral|grosvenor|betfred|redstar|titan|winner|mansion|party|gg|natural|winning|wpn|acr|tiger|betonline|sportsbetting|chico/i.test(entry.name)) await walk(fullPath, currentDepth + 1);
    }
  }
  await walk(basePath, 0);
  return results;
}

async function detectHandHistoryFolders() {
  const home = app.getPath("home");
  const documents = app.getPath("documents");
  const localAppData = path.join(home, "AppData", "Local");
  const roamingAppData = path.join(home, "AppData", "Roaming");
  const rawCandidates = [
    path.join(localAppData, "PokerStars", "HandHistory"),
    path.join(roamingAppData, "PokerStars", "HandHistory"),
    path.join(documents, "PokerStars", "HandHistory"),
    path.join(localAppData, "PokerStars", "TournSummary"),
    path.join(roamingAppData, "PokerStars", "TournSummary"),
    path.join(documents, "PokerStars", "TournSummary"),
    path.join(localAppData, "PokerStars", "TournamentSummary"),
    path.join(roamingAppData, "PokerStars", "TournamentSummary"),
    path.join(documents, "PokerStars", "TournamentSummary"),
    path.join(localAppData, "PokerKing", "HandHistory"),
    path.join(roamingAppData, "PokerKing", "HandHistory"),
    path.join(documents, "PokerKing", "HandHistory"),
    path.join(localAppData, "PokerKing", "TournamentSummary"),
    path.join(roamingAppData, "PokerKing", "TournamentSummary"),
    path.join(documents, "PokerKing", "TournamentSummary"),
    path.join(path.parse(home).root, "PokerKing", "handHistory"),
    path.join(path.parse(home).root, "PokerKing", "HandHistory"),
    path.join(localAppData, "AmericasCardroom", "handHistory"),
    path.join(roamingAppData, "AmericasCardroom", "handHistory"),
    path.join(localAppData, "AmericasCardroom", "TournamentSummary"),
    path.join(roamingAppData, "AmericasCardroom", "TournamentSummary"),
    path.join(localAppData, "ACR Poker", "handHistory"),
    path.join(roamingAppData, "ACR Poker", "handHistory"),
    path.join(localAppData, "ACR Poker", "TournamentSummary"),
    path.join(roamingAppData, "ACR Poker", "TournamentSummary"),
    path.join(localAppData, "partypoker", "HandHistory"),
    path.join(roamingAppData, "partypoker", "HandHistory"),
    path.join(documents, "partypoker", "HandHistory"),
    path.join(documents, "PartyPoker", "HandHistory"),
    path.join(localAppData, "partypoker", "TournamentSummary"),
    path.join(roamingAppData, "partypoker", "TournamentSummary"),
    path.join(documents, "PartyPoker", "TournamentSummary"),
    path.join(localAppData, "ChampionPoker", "data"),
    path.join(roamingAppData, "ChampionPoker", "data"),
    path.join(documents, "ChampionPoker", "History"),
    path.join(localAppData, "ChampionPoker", "TournamentSummary"),
    path.join(roamingAppData, "ChampionPoker", "TournamentSummary"),
    path.join(documents, "ChampionPoker", "TournamentSummary"),
    path.join(localAppData, "iPoker", "data"),
    path.join(roamingAppData, "iPoker", "data"),
    path.join(documents, "iPoker", "History"),
    path.join(localAppData, "Bet365", "data"),
    path.join(roamingAppData, "Bet365", "data"),
    path.join(documents, "Bet365", "History"),
    path.join(localAppData, "Betfair Poker", "data"),
    path.join(roamingAppData, "Betfair Poker", "data"),
    path.join(documents, "Betfair Poker", "History"),
    path.join(localAppData, "WilliamHill", "data"),
    path.join(roamingAppData, "WilliamHill", "data"),
    path.join(documents, "WilliamHill", "History"),
    path.join(localAppData, "PaddyPower", "data"),
    path.join(roamingAppData, "PaddyPower", "data"),
    path.join(documents, "PaddyPower", "History"),
    path.join(localAppData, "GGPOKER"),
    path.join(roamingAppData, "GGPOKER"),
    path.join(localAppData, "GGNetwork"),
    path.join(roamingAppData, "GGNetwork"),
    path.join(documents, "GGPoker"),
    path.join(documents, "GGPOKER"),
    path.join(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "TigerGaming", "history"),
    path.join(process.env.ProgramFiles || "C:\\Program Files", "TigerGaming", "history"),
    path.join(localAppData, "TigerGaming", "history"),
    path.join(roamingAppData, "TigerGaming", "history"),
    path.join(documents, "TigerGaming", "HandHistory"),
    path.join(documents, "BetOnline", "HandHistory"),
    path.join(documents, "SportsBetting", "HandHistory"),
    path.join(documents, "TigerGaming", "TournamentSummary"),
    path.join(documents, "BetOnline", "TournamentSummary"),
    path.join(documents, "SportsBetting", "TournamentSummary"),
    path.join(documents, "HandHistory"),
    path.join(documents, "Hand Histories"),
  ];

  const discovered = [];
  for (const basePath of [localAppData, roamingAppData, documents]) {
    discovered.push(...await discoverNamedFolders(basePath, 3));
  }

  const folders = [];
  for (const folderPath of uniquePaths([...rawCandidates, ...discovered])) {
    if (!await pathExists(folderPath)) continue;
    const fileCount = await countHandHistoryFiles(folderPath);
    const hasImportFolderName = /hand|history|summary|summaries|tourn|result|resumo/i.test(path.basename(folderPath));
    if (!fileCount && !hasImportFolderName) continue;
    folders.push({
      site: siteFromPath(folderPath),
      path: folderPath,
      fileCount,
    });
  }

  return folders.sort((a, b) => {
    if (b.fileCount !== a.fileCount) return b.fileCount - a.fileCount;
    return a.path.localeCompare(b.path);
  });
}

function createWindow({ replay = false } = {}) {
  const window = new BrowserWindow({
    width: replay ? 1180 : 1440,
    height: replay ? 820 : 900,
    minWidth: replay ? 980 : 1180,
    minHeight: replay ? 680 : 720,
    title: replay ? "MTT Edge Lab - Replayer" : "MTT Edge Lab",
    icon: appIconPath,
    backgroundColor: "#101213",
    show: false,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      nodeIntegration: false,
      backgroundThrottling: false,
    },
  });

  const indexUrl = pathToFileURL(path.join(__dirname, "index.html")).toString();
  window.loadURL(replay ? `${indexUrl}?mode=replayer` : indexUrl);
  window.once("ready-to-show", () => window.show());
  window.webContents.on("render-process-gone", (_event, details) => logDiagnostic(replay ? "replayer-render-process-gone" : "main-render-process-gone", details));
  window.on("unresponsive", () => logDiagnostic(replay ? "replayer-unresponsive" : "main-unresponsive"));

  if (replay) {
    replayWindows.add(window);
    window.on("closed", () => replayWindows.delete(window));
  } else {
    mainWindow = window;
    window.on("closed", () => {
      logDiagnostic("main-window-closed");
      if (mainWindow === window) mainWindow = null;
      closeAuxiliaryWindows();
    });
  }

  return window;
}

async function loadHudLayouts() {
  if (hudLayoutsLoaded) return;
  hudLayoutsLoaded = true;
  try {
    const saved = JSON.parse(await fs.readFile(path.join(app.getPath("userData"), HUD_LAYOUTS_FILE), "utf8")) || {};
    if (saved.version === HUD_LAYOUT_VERSION) {
      hudLayouts = saved.slots && typeof saved.slots === "object" ? saved.slots : {};
      hudGlobalScales = saved.globalScales && typeof saved.globalScales === "object" ? saved.globalScales : {};
      hudOpacities = saved.opacities && typeof saved.opacities === "object" ? saved.opacities : {};
    } else if (saved.version === 4) {
      hudLayouts = Object.fromEntries(Object.entries(saved.slots || {}).filter(([key]) => /-8-/.test(key)));
      hudGlobalScales = saved.globalScales && typeof saved.globalScales === "object" ? saved.globalScales : {};
      hudOpacities = saved.opacities && typeof saved.opacities === "object" ? saved.opacities : {};
      scheduleHudLayoutsSave();
    } else if (saved.version === 3) {
      hudLayouts = Object.fromEntries(Object.entries(saved.slots || {}).filter(([key]) => !/-7-/.test(key)));
      hudGlobalScales = saved.globalScales && typeof saved.globalScales === "object" ? saved.globalScales : {};
      hudOpacities = saved.opacities && typeof saved.opacities === "object" ? saved.opacities : {};
      scheduleHudLayoutsSave();
    } else if (saved.version === 2) {
      hudLayouts = Object.fromEntries(Object.entries(saved.slots || {}).map(([key, value]) => [`wpn-${key}`, value]));
      hudGlobalScales = { wpn: Math.max(0.6, Math.min(1.8, Number(saved.globalScale) || 1)) };
      hudOpacities = {};
      scheduleHudLayoutsSave();
    } else {
      hudLayouts = {};
      hudGlobalScales = {};
      hudOpacities = {};
      scheduleHudLayoutsSave();
    }
  } catch {
    hudLayouts = {};
    hudGlobalScales = {};
    hudOpacities = {};
  }
}

function scheduleHudLayoutsSave() {
  if (hudLayoutsSaveTimer) clearTimeout(hudLayoutsSaveTimer);
  hudLayoutsSaveTimer = setTimeout(async () => {
    await fs.mkdir(app.getPath("userData"), { recursive: true });
    await fs.writeFile(path.join(app.getPath("userData"), HUD_LAYOUTS_FILE), JSON.stringify({ version: HUD_LAYOUT_VERSION, globalScales: hudGlobalScales, opacities: hudOpacities, slots: hudLayouts }), "utf8");
  }, 180);
}

function defaultHudLayout(slotIndex, playerCount, width, height) {
  const angle = -Math.PI / 2 + ((Math.PI * 2 * slotIndex) / Math.max(1, playerCount));
  return {
    rx: 0.5 + Math.cos(angle) * 0.37 - (width / 2) / 900,
    ry: 0.5 + Math.sin(angle) * 0.34 - (height / 2) / 640,
    scale: 1,
  };
}

function hudBoundsForSlot(tableWindow, slotKey, slotIndex, playerCount) {
  const secondRow = latestHudPayload?.secondRowEnabled === true;
  const baseWidth = secondRow ? 250 : 230;
  const baseHeight = secondRow ? 108 : 78;
  const layout = hudLayouts[slotKey] || defaultHudLayout(slotIndex, playerCount, baseWidth, baseHeight);
  if (!hudLayouts[slotKey]) hudLayouts[slotKey] = layout;
  const scale = Math.max(0.6, Math.min(1.8, Number(layout.scale) || 1));
  const width = Math.round(baseWidth * scale);
  const height = Math.round(baseHeight * scale);
  return {
    x: Math.round(tableWindow.x + layout.rx * tableWindow.width),
    y: Math.round(tableWindow.y + layout.ry * tableWindow.height),
    width,
    height,
  };
}

function applyHudSlotBounds(window) {
  if (!window || window.isDestroyed() || !window.hudTableWindow) return;
  const bounds = hudBoundsForSlot(window.hudTableWindow, window.hudSlotKey, window.hudSlotIndex, window.hudPlayerCount);
  window.hudApplyingBounds = true;
  window.setBounds(bounds, false);
  setTimeout(() => { if (!window.isDestroyed()) window.hudApplyingBounds = false; }, 80);
}

function saveSharedHudPosition(window) {
  if (!window || window.isDestroyed() || window.hudApplyingBounds || !window.hudTableWindow) return;
  const bounds = window.getBounds();
  const table = window.hudTableWindow;
  const current = hudLayouts[window.hudSlotKey] || {};
  hudLayouts[window.hudSlotKey] = {
    ...current,
    rx: (bounds.x - table.x) / Math.max(1, table.width),
    ry: (bounds.y - table.y) / Math.max(1, table.height),
    scale: Number(current.scale) || 1,
  };
  scheduleHudLayoutsSave();
  for (const other of hudWindows.values()) {
    if (other !== window && other.hudSlotKey === window.hudSlotKey) applyHudSlotBounds(other);
  }
}

function pruneHudWindows() {
  for (const [key, window] of hudWindows.entries()) {
    if (!window || window.isDestroyed()) hudWindows.delete(key);
  }
  for (const window of hudWindowInstances) {
    if (!window || window.isDestroyed()) hudWindowInstances.delete(window);
  }
}

function deleteHudWindowEntries(targetWindow) {
  for (const [entryKey, window] of hudWindows.entries()) {
    if (window === targetWindow) hudWindows.delete(entryKey);
  }
}

function bindHudWindowKey(key, window) {
  if (!window || window.isDestroyed()) return;
  const displaced = hudWindows.get(key);
  if (displaced && displaced !== window && !displaced.isDestroyed()) displaced.close();
  deleteHudWindowEntries(window);
  hudWindows.set(key, window);
  window.hudMapKey = key;
}

function normalizedPokerTableTitle(title = "") {
  return String(title)
    .toLowerCase()
    .replace(/\s+/g, " ")
    .replace(/\b(?:re-?entry|reentrada)\b[^-]*/gi, "")
    .trim();
}

function tableBoundsOverlap(first, second) {
  if (!first || !second) return 0;
  const left = Math.max(Number(first.x) || 0, Number(second.x) || 0);
  const top = Math.max(Number(first.y) || 0, Number(second.y) || 0);
  const right = Math.min((Number(first.x) || 0) + (Number(first.width) || 0), (Number(second.x) || 0) + (Number(second.width) || 0));
  const bottom = Math.min((Number(first.y) || 0) + (Number(first.height) || 0), (Number(second.y) || 0) + (Number(second.height) || 0));
  const intersection = Math.max(0, right - left) * Math.max(0, bottom - top);
  const smallerArea = Math.min(
    Math.max(1, (Number(first.width) || 0) * (Number(first.height) || 0)),
    Math.max(1, (Number(second.width) || 0) * (Number(second.height) || 0)),
  );
  return intersection / smallerArea;
}

function isSameLogicalPokerTable(first, second) {
  if (!first || !second) return false;
  const firstSite = hudSiteFamilyKey(first.site || "default");
  const secondSite = hudSiteFamilyKey(second.site || "default");
  if (firstSite !== secondSite) return false;
  const firstTableId = tableIdFromWindowTitle(first.title);
  const secondTableId = tableIdFromWindowTitle(second.title);
  if (firstTableId && secondTableId && firstTableId === secondTableId) return true;
  if (tableBoundsOverlap(first, second) >= 0.78) return true;
  const firstTitle = normalizedPokerTableTitle(first.title);
  const secondTitle = normalizedPokerTableTitle(second.title);
  return Boolean(firstTitle && firstTitle === secondTitle);
}

function hudWindowForTable(tableWindow, key) {
  pruneHudWindows();
  const existingByKey = hudWindows.get(key);
  if (existingByKey && !existingByKey.isDestroyed()) return existingByKey;
  const tableHandle = tableWindow?.handle ? String(tableWindow.handle) : "";
  if (!tableHandle) return null;
  for (const [entryKey, window] of hudWindows.entries()) {
    if (!window || window.isDestroyed()) {
      hudWindows.delete(entryKey);
      continue;
    }
    if (window.hudOwnerHandle && String(window.hudOwnerHandle) === tableHandle) {
      bindHudWindowKey(key, window);
      return window;
    }
    if (isSameLogicalPokerTable(window.hudTableWindow, tableWindow)) {
      bindHudWindowKey(key, window);
      return window;
    }
  }
  return null;
}

function closeDuplicateHudWindowsForHandle(ownerHandle, keepWindow) {
  if (!ownerHandle || !keepWindow) return;
  const handle = String(ownerHandle);
  for (const window of [...hudWindowInstances]) {
    if (!window || window.isDestroyed()) {
      hudWindowInstances.delete(window);
      continue;
    }
    if (window !== keepWindow && window.hudOwnerHandle && String(window.hudOwnerHandle) === handle) {
      window.close();
    }
  }
}

function closeDuplicateHudWindowsForTable(tableWindow, keepWindow) {
  if (!tableWindow || !keepWindow) return;
  for (const window of [...hudWindowInstances]) {
    if (!window || window.isDestroyed()) {
      hudWindowInstances.delete(window);
      continue;
    }
    if (window !== keepWindow && isSameLogicalPokerTable(window.hudTableWindow, tableWindow)) {
      window.close();
    }
  }
}

async function createHudWindow(options = {}) {
  await loadHudLayouts();
  const key = safeHudKey(options.key || "manual");
  const hudSiteKey = hudSiteFamilyKey(options.tableWindow?.site || "default");
  const useStablePokerStarsCompositor = false;
  const initialBounds = options.tableWindow
    ? { x: options.tableWindow.x, y: options.tableWindow.y, width: options.tableWindow.width, height: options.tableWindow.height }
    : await readHudBounds(key);
  const window = new BrowserWindow({
    width: initialBounds.width || 900,
    height: initialBounds.height || 640,
    x: initialBounds.x,
    y: initialBounds.y,
    minWidth: 320,
    minHeight: 240,
    title: options.title || "MTT Edge Lab - HUD",
    icon: appIconPath,
    backgroundColor: useStablePokerStarsCompositor ? "#060a0c" : "#00000000",
    frame: false,
    transparent: !useStablePokerStarsCompositor,
    hasShadow: false,
    roundedCorners: false,
    alwaysOnTop: false,
    skipTaskbar: true,
    resizable: false,
    movable: false,
    show: false,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      nodeIntegration: false,
      backgroundThrottling: false,
    },
  });

  window.hudTableTitle = options.tableWindow?.title || options.title || "";
  window.setSkipTaskbar(true);
  window.hudTableWindow = options.tableWindow || null;
  window.hudSiteKey = hudSiteKey;
  window.hudStableCompositor = useStablePokerStarsCompositor;
  window.hudAwaitingInitialShape = useStablePokerStarsCompositor;
  if (useStablePokerStarsCompositor) {
    window.setOpacity(Math.max(0.35, Math.min(1, Number(hudOpacities[hudSiteKey]) || 1)));
  }
  window.hudOwnerHandle = options.tableWindow?.handle || null;
  window.hudLastTableSeenAt = options.tableWindow ? Date.now() : 0;
  window.hudManagedAuto = Boolean(options.tableWindow);
  window.hudPlayerName = "";
  window.hudSlotKey = "table";
  const indexUrl = pathToFileURL(path.join(__dirname, "hud-player.html")).toString();
  const url = `${indexUrl}?hudKey=${encodeURIComponent(key)}`;
  // Prefer native ownership for every supported room. When Windows accepts it,
  // the HUD follows the table without topmost z-order oscillation.
  window.hudOwnerAttached = options.tableWindow?.handle
    ? await attachHudToPokerTable(window, options.tableWindow.handle)
    : false;
  window.hudOwnerAttachAttempts = options.tableWindow?.handle ? 1 : 0;
  window.hudOwnerAttachAttemptedAt = Date.now();
  window.loadURL(url).catch((error) => logDiagnostic("hud-load-failed", {
    key,
    message: error?.message || String(error)
  }));
  window.setIgnoreMouseEvents(true, { forward: true });
  if (!window.hudAwaitingInitialShape) window.showInactive();
  updateHudWindowZOrder(window, options.tableWindow);
  window.webContents.once("did-finish-load", () => {
    if (window.isDestroyed()) return;
    if (!window.hudAwaitingInitialShape && !window.isVisible()) window.showInactive();
    window.webContents.send("hud:payload", hudPayloadForWindow(window));
  });
  window.webContents.on("did-fail-load", (_event, errorCode, errorDescription) => logDiagnostic("hud-did-fail-load", {
    key,
    errorCode,
    errorDescription
  }));
  window.webContents.on("render-process-gone", (_event, details) => logDiagnostic("hud-render-process-gone", { key, ...details }));
  window.on("closed", () => {
    deleteHudWindowEntries(window);
    hudWindowInstances.delete(window);
  });
  hudWindowInstances.add(window);
  bindHudWindowKey(key, window);
  return window;
}

function closeHudWindows(keysToKeep = new Set()) {
  for (const window of [...hudWindowInstances]) {
    if (keysToKeep.has(window.hudMapKey)) continue;
    if (!window.isDestroyed()) window.close();
  }
  pruneHudWindows();
}

function closeAuxiliaryWindows() {
  closeHudWindows();
  for (const window of [...replayWindows]) {
    if (window && !window.isDestroyed()) window.close();
  }
}

async function syncHudWindowsNow(enabled) {
  if (!enabled) {
    closeHudWindows();
    return { opened: 0, allowed: 0, blocked: 0, manual: 0 };
  }
  pruneHudWindows();
  const windows = await detectPokerTableWindows();
  const foregroundHandle = Number(windows[0]?.foregroundHandle) || 0;
  const foregroundHudWindow = [...hudWindowInstances].find((window) => {
    if (!window || window.isDestroyed()) return false;
    const handle = window.getNativeWindowHandle();
    const nativeHandle = handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0);
    return nativeHandle === foregroundHandle;
  });
  const allowed = windows.filter((item) => item.policy === "allowed");
  const pokerStarsTables = allowed.filter((item) => item.site === "PokerStars");
  const pokerStarsLobbyVisible = windows.some((item) => item.site === "PokerStars" && item.isPokerLobby);
  const keep = new Set();
  const assignedWindows = new Set();
  for (const tableWindow of allowed) {
    tableWindow.isHudForeground = Boolean(
      foregroundHudWindow
      && (
        isSameLogicalPokerTable(foregroundHudWindow.hudTableWindow, tableWindow)
        || (
          foregroundHudWindow.hudSiteKey === hudSiteFamilyKey(tableWindow.site || "default")
          && foregroundHudWindow.hudTableWindow?.process === tableWindow.process
        )
      )
    );
    const key = safeHudKey(tableWindow.key);
    const existing = hudWindowForTable(tableWindow, key);
    const tablePayload = hudTablePayloadForTitle(tableWindow.title);
    const roster = tablePayload?.players;
    if (!Array.isArray(roster) || !roster.length) {
      if (existing && !existing.isDestroyed()) {
        existing.hudMissingSyncCount = (existing.hudMissingSyncCount || 0) + 1;
        if (existing.hudMissingSyncCount < 3) {
          keep.add(key);
          assignedWindows.add(existing);
          existing.hudLastTableSeenAt = Date.now();
          existing.hudTableWindow = tableWindow;
          existing.hudTableTitle = tableWindow.title;
          applyHudTableBounds(existing, tableWindow);
        } else {
          existing.close();
        }
      }
      continue;
    }
    keep.add(key);
    if (existing && !existing.isDestroyed()) {
      assignedWindows.add(existing);
      existing.hudMissingSyncCount = 0;
      existing.hudLastTableSeenAt = Date.now();
      existing.hudTableWindow = tableWindow;
      existing.hudTableTitle = tableWindow.title;
      existing.hudSiteKey = hudSiteFamilyKey(tableWindow.site || "default");
      if (existing.hudOwnerHandle !== tableWindow.handle) {
        existing.hudOwnerHandle = tableWindow.handle;
        existing.hudOwnerAttached = await attachHudToPokerTable(existing, tableWindow.handle);
        existing.hudOwnerAttachAttempts = 1;
        existing.hudOwnerAttachAttemptedAt = Date.now();
      } else if (
        !existing.hudOwnerAttached
        && (existing.hudOwnerAttachAttempts || 0) < 2
        && Date.now() - (existing.hudOwnerAttachAttemptedAt || 0) >= 30000
      ) {
        existing.hudOwnerAttached = await attachHudToPokerTable(existing, tableWindow.handle);
        existing.hudOwnerAttachAttempts = (existing.hudOwnerAttachAttempts || 0) + 1;
        existing.hudOwnerAttachAttemptedAt = Date.now();
      }
      applyHudTableBounds(existing, tableWindow);
      closeDuplicateHudWindowsForHandle(tableWindow.handle, existing);
      closeDuplicateHudWindowsForTable(tableWindow, existing);
      continue;
    }
    const created = await createHudWindow({ key, title: `${tableWindow.site} HUD`, tableWindow });
    created.hudLastTableSeenAt = Date.now();
    created.hudMissingSyncCount = 0;
    assignedWindows.add(created);
    closeDuplicateHudWindowsForHandle(tableWindow.handle, created);
    closeDuplicateHudWindowsForTable(tableWindow, created);
  }
  // Apply z-order only after every table association is complete. Updating it
  // inside the loop left PokerStars overlays in a partial state while window
  // discovery and native-owner attempts were still running.
  for (const window of assignedWindows) {
    updateHudWindowZOrder(window, window.hudTableWindow);
  }
  const now = Date.now();
  for (const window of [...hudWindowInstances]) {
    if (!window.hudManagedAuto || assignedWindows.has(window) || window.isDestroyed()) continue;
    const isInteracting = window.hudDragging || now < (window.hudInteractionUntil || 0);
    if (window.hudSiteKey === "pokerstars") {
      window.hudMissingSyncCount = (window.hudMissingSyncCount || 0) + 1;
      const lobbyOnly = pokerStarsLobbyVisible && pokerStarsTables.length === 0;
      if (lobbyOnly || window.hudMissingSyncCount >= 2) {
        logDiagnostic("hud-pokerstars-table-closed", {
          title: window.hudTableTitle,
          misses: window.hudMissingSyncCount,
          lobbyOnly,
        });
        window.close();
        continue;
      }
    }
    if (isInteracting) {
      if (window.hudMapKey) keep.add(window.hudMapKey);
      updateHudWindowZOrder(window, { ...window.hudTableWindow, isHudForeground: true });
    } else if (now - (window.hudLastTableSeenAt || 0) > 45000) {
      logDiagnostic("hud-stale-close", {
        site: window.hudSiteKey,
        title: window.hudTableTitle,
        lastSeenMs: now - (window.hudLastTableSeenAt || 0),
      });
      window.close();
    } else if (window.hudMapKey) {
      keep.add(window.hudMapKey);
    }
  }
  closeHudWindows(keep);
  return {
    opened: keep.size,
    allowed: allowed.length,
    blocked: windows.filter((item) => item.policy === "blocked").length,
    manual: windows.filter((item) => item.policy === "manual").length,
  };
}

async function syncHudWindows(enabled) {
  if (hudSyncInFlight) await hudSyncInFlight.catch(() => {});
  const currentSync = syncHudWindowsNow(enabled);
  hudSyncInFlight = currentSync;
  try {
    return await currentSync;
  } finally {
    if (hudSyncInFlight === currentSync) hudSyncInFlight = null;
  }
}

app.whenReady().then(async () => {
  if (!hasSingleInstanceLock) return;
  logDiagnostic("app-ready", { version: app.getVersion(), electron: process.versions.electron });
  if (!(await enforceBetaTrial())) return;
  betaTrialTimer = setInterval(() => {
    enforceBetaTrial().catch((error) => logDiagnostic("beta-trial-check-error", error?.stack || error?.message || String(error)));
  }, 6 * 60 * 60 * 1000);

  ipcMain.handle("beta:get-status", async () => betaTrialStatus || ensureBetaTrial());

  ipcMain.handle("store:load", async () => {
    try {
      return await fs.readFile(storePath(), "utf8");
    } catch (error) {
      if (error.code === "ENOENT") return null;
      throw error;
    }
  });

  ipcMain.handle("store:save", async (_event, payload) => {
    await fs.mkdir(path.dirname(storePath()), { recursive: true });
    await fs.writeFile(storePath(), payload, "utf8");
    return { ok: true, path: storePath() };
  });

  ipcMain.handle("store:reset", async () => {
    try {
      await fs.unlink(storePath());
    } catch (error) {
      if (error.code !== "ENOENT") throw error;
    }
    return { ok: true };
  });

  ipcMain.handle("replayer:open", async () => {
    const existing = [...replayWindows].find((window) => !window.isDestroyed());
    if (existing) {
      existing.focus();
      return { ok: true };
    }
    createWindow({ replay: true });
    return { ok: true };
  });

  ipcMain.handle("hud:open", async () => {
    const key = "manual";
    const existing = hudWindows.get(key);
    if (existing && !existing.isDestroyed()) {
      existing.show();
      existing.focus();
      return { ok: true };
    }
    await createHudWindow({ key, title: "MTT Edge Lab - HUD" });
    return { ok: true };
  });

  ipcMain.handle("hud:sync", async (_event, enabled) => syncHudWindows(!!enabled));

  ipcMain.handle("hud:set-payload", async (_event, payload) => {
    latestHudPayload = payload && typeof payload === "object" ? payload : null;
    for (const window of hudWindows.values()) {
      if (!window.isDestroyed()) window.webContents.send("hud:payload", hudPayloadForWindow(window));
    }
    return { ok: true };
  });

  ipcMain.handle("hud:get-payload", async (event) => hudPayloadForWindow(BrowserWindow.fromWebContents(event.sender)));

  ipcMain.handle("hud:set-slot-scale", async (event, input) => {
    await loadHudLayouts();
    const sourceWindow = BrowserWindow.fromWebContents(event.sender);
    const siteKey = hudSiteFamilyKey(sourceWindow?.hudSiteKey || "default");
    const value = input?.action || "reset";
    const current = Number(hudGlobalScales[siteKey]) || 1;
    hudGlobalScales[siteKey] = value === "reset" ? 1 : Math.max(0.6, Math.min(1.8, current + (value === "up" ? 0.1 : -0.1)));
    scheduleHudLayoutsSave();
    for (const window of hudWindows.values()) {
      if (!window.isDestroyed()) window.webContents.send("hud:payload", hudPayloadForWindow(window));
    }
    return { ok: true, scale: hudGlobalScales[siteKey] };
  });

  ipcMain.handle("hud:set-slot-position", async (_event, input) => {
    await loadHudLayouts();
    const slotKey = safeHudKey(input?.slotKey || "manual");
    const current = hudLayouts[slotKey] || { scale: 1 };
    hudLayouts[slotKey] = {
      ...current,
      rx: Math.max(0, Math.min(1, Number(input?.rx) || 0)),
      ry: Math.max(0, Math.min(1, Number(input?.ry) || 0)),
    };
    scheduleHudLayoutsSave();
    for (const window of hudWindows.values()) {
      if (!window.isDestroyed()) window.webContents.send("hud:payload", hudPayloadForWindow(window));
    }
    return { ok: true };
  });

  ipcMain.handle("hud:set-opacity", async (event) => {
    await loadHudLayouts();
    const sourceWindow = BrowserWindow.fromWebContents(event.sender);
    const siteKey = hudSiteFamilyKey(sourceWindow?.hudSiteKey || "default");
    const levels = [1, 0.85, 0.7, 0.55, 0.4];
    const current = Number(hudOpacities[siteKey]) || 1;
    const currentIndex = levels.findIndex((level) => Math.abs(level - current) < 0.03);
    hudOpacities[siteKey] = levels[(currentIndex + 1 + levels.length) % levels.length];
    scheduleHudLayoutsSave();
    for (const window of hudWindows.values()) {
      if (!window.isDestroyed()) {
        if (window.hudStableCompositor && window.hudSiteKey === siteKey) {
          window.setOpacity(hudOpacities[siteKey]);
        }
        window.webContents.send("hud:payload", hudPayloadForWindow(window));
      }
    }
    return { ok: true, opacity: hudOpacities[siteKey] };
  });

  ipcMain.handle("hud:set-mouse-events", async (event, interactive) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    if (window && !window.isDestroyed()) {
      window.hudInteractionUntil = interactive ? Date.now() + 2500 : Date.now() + 800;
      window.setIgnoreMouseEvents(false);
    }
    return { ok: true };
  });

  ipcMain.handle("hud:set-shape", async (event, rects) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    if (!window || window.isDestroyed() || typeof window.setShape !== "function") return { ok: false };
    const safeRects = (Array.isArray(rects) ? rects : []).slice(0, 12).map((rect) => ({
      x: Math.max(0, Math.round(Number(rect.x) || 0)),
      y: Math.max(0, Math.round(Number(rect.y) || 0)),
      width: Math.max(1, Math.round(Number(rect.width) || 1)),
      height: Math.max(1, Math.round(Number(rect.height) || 1)),
    }));
    window.setShape(safeRects);
    if (window.hudAwaitingInitialShape) {
      window.hudAwaitingInitialShape = false;
      window.showInactive();
      updateHudWindowZOrder(window, window.hudTableWindow);
    }
    // The native window shape contains only HUD panels, so clicks outside
    // these rectangles continue directly to the poker table.
    window.setIgnoreMouseEvents(false);
    return { ok: true };
  });

  ipcMain.handle("hud:set-dragging", async (event, active) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    if (!window || window.isDestroyed() || typeof window.setShape !== "function") return { ok: false };
    if (active) {
      window.hudDragging = true;
      window.hudInteractionUntil = Date.now() + 60000;
      window.hudLastTableSeenAt = Date.now();
      const [width, height] = window.getContentSize();
      window.setShape([{ x: 0, y: 0, width, height }]);
      window.setIgnoreMouseEvents(false);
    } else {
      window.hudDragging = false;
      window.hudInteractionUntil = Date.now() + 1500;
      window.hudLastTableSeenAt = Date.now();
      window.webContents.send("hud:payload", hudPayloadForWindow(window));
      window.setIgnoreMouseEvents(false);
    }
    return { ok: true };
  });

  ipcMain.handle("hud:detect-tables", async () => detectPokerTableWindows());

  ipcMain.handle("hands:choose-folder", async () => {
    const result = await dialog.showOpenDialog(mainWindow, {
      title: "Escolher pasta de maos ou sumarios",
      properties: ["openDirectory"],
    });
    if (result.canceled || !result.filePaths?.[0]) return null;
    return result.filePaths[0];
  });

  ipcMain.handle("hands:scan-folder", async (_event, folderPath, seen) => {
    if (!folderPath) return [];
    return listHandHistoryFiles(folderPath, seen || {});
  });

  ipcMain.handle("hands:detect-folders", async () => detectHandHistoryFolders());

  ipcMain.handle("archive:read-rar", async (_event, archivePath) => readRarTextEntries(archivePath));

  createWindow();

  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on("second-instance", () => {
  if (!mainWindow || mainWindow.isDestroyed()) {
    if (app.isReady()) createWindow();
    return;
  }
  if (mainWindow.isMinimized()) mainWindow.restore();
  mainWindow.show();
  mainWindow.focus();
});

app.on("window-all-closed", () => {
  logDiagnostic("window-all-closed");
  if (process.platform !== "darwin") app.quit();
});

app.on("before-quit", () => {
  if (betaTrialTimer) clearInterval(betaTrialTimer);
  logDiagnostic("before-quit");
  closeAuxiliaryWindows();
});
