import { app, BrowserWindow, Menu, desktopCapturer, globalShortcut, nativeImage, screen, type NativeImage } from "electron";
import fs from "node:fs/promises";
import http, { type Server } from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createWorker } from "tesseract.js";
import { createInputHelperService, type InputHelperService } from "./services/inputHelper.js";
import { createRepositoryContext, type RepositoryContext } from "./bootstrap/repositoryContext.js";
import { registerIpcHandlers } from "./bootstrap/ipcBootstrap.js";
import type { AppSnapshot } from "../src/types/domain.js";
import type {
  CaptureOptions,
  CaptureResult,
  GoodDatabase,
  SaveResultWithPath,
  ScannerLearningRulePayload,
  ScannerStatusPayload,
} from "../src/types/global.js";
import type {
  ArtifactStoreRepositoryPort,
  ReviewSamplesRepositoryPort,
  ScannerLearningRepositoryPort,
} from "./repositories/index.js";

// Chromium's renderer sandbox can refuse to fully initialize (or silently
// crash the GPU/renderer process) when the hosting process runs with a full
// Administrator token - a well-known Electron-on-Windows-elevation quirk.
// This app already requires elevation for input automation and never loads
// untrusted remote content, so the renderer sandbox has little security
// value here; disabling it avoids "works normally, fails only when run as
// admin" failures with no visible error. Must run before app is ready.
app.commandLine.appendSwitch("no-sandbox");
app.commandLine.appendSwitch("disable-gpu-sandbox");

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);

let mainWindow: BrowserWindow | null = null;
let overlayWindow: BrowserWindow | null = null;
let registeredHotkeys: Record<string, boolean> = {};
let devControlServer: Server | null = null;
let scannerDevStatus: ScannerStatusPayload = {
  running: false,
  reviewStatus: "",
  captureStatus: "",
  selectedSource: null,
  stats: {},
  summary: null,
  snapshotArtifacts: 0,
  snapshotCharacters: 0,
  snapshotRecommendations: 0,
  snapshotBuilds: 0,
  grid: null,
  automationLog: [],
  runtimeInfo: null,
  storedTotal: null,
  learningRuleCount: 0,
  updatedAt: null,
};
let repositoryContext: RepositoryContext | null = null;
let artifactStoreRepository: ArtifactStoreRepositoryPort | null = null;
let reviewSamplesRepository: ReviewSamplesRepositoryPort | null = null;
let scannerLearningRepository: ScannerLearningRepositoryPort | null = null;
let inputHelperService: InputHelperService | null = null;

function getInputHelperService() {
  if (!inputHelperService) {
    throw new Error("Input-helper service has not been initialized.");
  }
  return inputHelperService;
}

function getRepositoryContext() {
  if (!repositoryContext) {
    throw new Error("Repository context has not been initialized.");
  }
  return repositoryContext;
}

function artifactStorePath() {
  return getRepositoryContext().artifactStorePath;
}

function reviewSamplesPath() {
  return getRepositoryContext().reviewSamplesPath;
}

function scannerLearningPath() {
  return getRepositoryContext().scannerLearningPath;
}

function getReviewSamplesRepository() {
  if (!reviewSamplesRepository) {
    throw new Error("Review-sample repository is not initialized.");
  }
  return reviewSamplesRepository;
}

async function loadReviewSamples(limit = 50) {
  try {
    const repository = getReviewSamplesRepository();
    return repository.list(Math.max(1, Math.min(200, Number(limit) || 50)));
  } catch {
    return { ok: true, samples: [], total: 0, path: reviewSamplesPath() };
  }
}

async function loadScannerLearningRules() {
  try {
    return await getScannerLearningRepository().load();
  } catch {
    return { ok: true, path: scannerLearningPath(), rules: { textReplacements: {} } };
  }
}

function getScannerLearningRepository() {
  if (!scannerLearningRepository) {
    throw new Error("Scanner-learning repository is not initialized.");
  }
  return scannerLearningRepository;
}

async function writeScannerLearningRules(rules: ScannerLearningRulePayload) {
  const safeRules = rules && typeof rules === "object" ? rules : {};
  try {
    return await getScannerLearningRepository().save(safeRules as { textReplacements?: Record<string, string> });
  } catch {
    return { ok: true, path: scannerLearningPath(), rules: { textReplacements: {} }, total: 0 };
  }
}

function getArtifactStoreRepository() {
  if (!artifactStoreRepository) {
    throw new Error("Artifact store repository is not initialized.");
  }
  return artifactStoreRepository;
}

function getSnapshotRepository() {
  const context = getRepositoryContext();
  if (!context.snapshotRepository) {
    throw new Error("Snapshot repository is not initialized.");
  }
  return context.snapshotRepository;
}

function exportPath(fileName: string) {
  return path.join(app.getPath("userData"), "exports", fileName);
}

async function loadSnapshotFromDisk() {
  try {
    return await getSnapshotRepository().load();
  } catch {
    return null;
  }
}

async function saveSnapshotToDisk(snapshot: AppSnapshot) {
  try {
    return await getSnapshotRepository().save(snapshot);
  } catch {
    return { ok: false, path: "" };
  }
}

async function publishScannerStatus(status: ScannerStatusPayload) {
  scannerDevStatus = { ...status, updatedAt: new Date().toISOString() };
  return { ok: true };
}

async function readRuntimeInfo() {
  try {
    const result = await getInputHelperService().getRuntimeInfo();
    return {
      ok: true,
      isElevated: result.isElevated,
      platform: result.platform,
      hotkeys: registeredHotkeys,
      genshinFound: result.genshinFound,
      genshinHwnd: result.genshinHwnd ?? undefined,
      targetProcess: result.targetProcess,
      foregroundProcess: result.foregroundProcess,
      foregroundHwnd: result.foregroundHwnd ?? undefined,
      helperPid: result.helperPid,
    };
  } catch {
    return { ok: false, isElevated: false, platform: process.platform, hotkeys: registeredHotkeys };
  }
}

async function focusGenshinWindow() {
  try {
    const result = await getInputHelperService().focusGenshinWindow();
    return result;
  } catch {
    return { focused: false, alreadyForeground: false, genshinFound: false };
  }
}

async function focusGenshinForScanStart() {
  try {
    return await getInputHelperService().focusGenshinForScanStart();
  } catch {
    return { focused: false, alreadyForeground: false, genshinFound: false };
  }
}

async function getGenshinWindowBounds() {
  try {
    return await getInputHelperService().getGenshinWindowBounds();
  } catch {
    return null;
  }
}

// Our own dashboard window defaulted to Electron's normal placement, which
// (on this exact reported bug) ended up sitting directly on top of Genshin's
// fullscreen window on the same monitor - so every simulated click that
// looked correct (focused, on-target, injected) was actually landing on our
// own window, not the game, since mouse hit-testing goes by which window is
// topmost at that screen pixel, not by which window has keyboard focus. If a
// second display exists and isn't the one Genshin occupies, move the
// dashboard there so it can never cover the grid we're about to click.
async function moveMainWindowOffGenshin() {
  if (!mainWindow || mainWindow.isDestroyed()) return;
  const genshinBounds = await getGenshinWindowBounds();
  const displays = screen.getAllDisplays();
  if (displays.length < 2) return;

  const genshinCenter = genshinBounds
    ? { x: genshinBounds.x + genshinBounds.width / 2, y: genshinBounds.y + genshinBounds.height / 2 }
    : null;
  const genshinDisplay = genshinCenter
    ? screen.getDisplayNearestPoint(genshinCenter)
    : screen.getPrimaryDisplay();

  const otherDisplay = displays.find((d) => d.id !== genshinDisplay.id);
  if (!otherDisplay) return;

  const currentBounds = mainWindow.getBounds();
  const alreadyOnOtherDisplay = screen.getDisplayMatching(currentBounds).id === otherDisplay.id;
  if (alreadyOnOtherDisplay) return;

  const area = otherDisplay.workArea;
  const width = Math.min(currentBounds.width, area.width - 40);
  const height = Math.min(currentBounds.height, area.height - 40);
  mainWindow.setBounds({
    x: Math.round(area.x + (area.width - width) / 2),
    y: Math.round(area.y + (area.height - height) / 2),
    width: Math.round(width),
    height: Math.round(height),
  });
}

async function runMockScan() {
  return null;
}

async function showOverlayWindow() {
  createOverlayWindow();
  return { ok: true };
}

async function hideOverlayWindow() {
  overlayWindow?.close();
  return { ok: true };
}

async function listCaptureSources() {
  const displays = screen.getAllDisplays();
  const maxSize = displays.reduce(
    (size, display) => ({
      width: Math.max(size.width, display.size.width),
      height: Math.max(size.height, display.size.height),
    }),
    { width: 1920, height: 1080 },
  );

  const sources = await desktopCapturer.getSources({
    types: ["window", "screen"],
    thumbnailSize: maxSize,
    fetchWindowIcons: true,
  });

  return sources.map((source) => ({
    id: source.id,
    name: source.name,
    isGenshinCandidate: isLikelyGenshinSourceName(source.name),
    thumbnailDataUrl: source.thumbnail.resize({ width: 420 }).toDataURL(),
  }));
}

async function clickScreenCommand(x: number, y: number) {
  return getInputHelperService().clickScreen(Math.round(x), Math.round(y));
}

async function scrollScreenCommand(notches: number, anchorX?: number, anchorY?: number) {
  return getInputHelperService().scrollScreen(notches, anchorX, anchorY);
}

async function getAutomationGuardCommand() {
  const result = await getInputHelperService().getAutomationGuard();
  return {
    ...result,
    ok: true,
    // Preserve historical automation metadata shape expected by callers.
    isElevated: typeof result.isElevated === "boolean" ? result.isElevated : false,
  };
}

function isLikelyGenshinSourceName(sourceName: string) {
  const lowered = sourceName.toLowerCase();
  return (
    lowered.includes("genshin")
    || lowered.includes("genshinimpact")
    || lowered.includes("yuanshen")
    || sourceName.includes("\u539f\u795e")
  );
}

async function capturePrimaryScreenViaGdi() {
  return getInputHelperService().capturePrimaryScreenViaGdi();
}

async function captureSourceFromGdi(sourceId: string, sourceName: string, options: CaptureOptions = {}) {
  const gdi = await capturePrimaryScreenViaGdi();
  const sourceImage = nativeImage.createFromDataURL(gdi.dataUrl);
  return await buildCaptureResult(sourceImage, sourceId, sourceName, gdi.captureTarget, options);
}

function createMainWindow() {
  Menu.setApplicationMenu(null);

  mainWindow = new BrowserWindow({
    width: 1320,
    height: 860,
    minWidth: 1120,
    minHeight: 720,
    backgroundColor: "#090711",
    title: "Genshin Artifact Assistant",
    show: false,
    autoHideMenuBar: true,
    webPreferences: {
      preload: path.join(__dirname, "preload.cjs"),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  mainWindow.setMenuBarVisibility(false);
  mainWindow.on("closed", () => {
    mainWindow = null;
  });
  mainWindow.once("ready-to-show", () => {
    void moveMainWindowOffGenshin();
    focusMainWindow();
  });
  mainWindow.webContents.once("did-finish-load", () => {
    setTimeout(() => focusMainWindow(), 350);
  });

  if (isDev) {
    mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL!);
  } else {
    mainWindow.loadFile(path.join(__dirname, "../dist/index.html"));
  }
}

function focusMainWindow() {
  if (!mainWindow || mainWindow.isDestroyed()) return { ok: false };

  if (mainWindow.isMinimized()) mainWindow.restore();
  mainWindow.show();
  // Genshin often keeps foreground focus after a scan click. Toggling
  // always-on-top for one tick nudges Windows to surface the dashboard again
  // without leaving it pinned above other apps.
  mainWindow.setAlwaysOnTop(true, "screen-saver");
  mainWindow.focus();
  setTimeout(() => {
    if (!mainWindow || mainWindow.isDestroyed()) return;
    mainWindow.setAlwaysOnTop(false);
    mainWindow.focus();
  }, 250);
  return { ok: true };
}

function sendScannerCommand(command: "start-auto" | "stop" | "probe-click") {
  if (!mainWindow || mainWindow.isDestroyed()) return;
  mainWindow.webContents.send("scanner:command", command);
}

function registerScannerHotkeys() {
  globalShortcut.unregisterAll();
  registeredHotkeys = {
    "Ctrl+Shift+S": globalShortcut.register("CommandOrControl+Shift+S", () => sendScannerCommand("start-auto")),
    "Ctrl+Shift+X": globalShortcut.register("CommandOrControl+Shift+X", () => sendScannerCommand("stop")),
    F8: globalShortcut.register("F8", () => sendScannerCommand("start-auto")),
    F9: globalShortcut.register("F9", () => sendScannerCommand("stop")),
  };
}

function writeDevJson(res: http.ServerResponse, statusCode: number, payload: unknown) {
  res.writeHead(statusCode, {
    "content-type": "application/json; charset=utf-8",
    "cache-control": "no-store",
  });
  res.end(JSON.stringify(payload));
}

function startDevControlServer() {
  if (!isDev || devControlServer) return;

  devControlServer = http.createServer((req, res) => {
    if (req.socket.remoteAddress && !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(req.socket.remoteAddress)) {
      writeDevJson(res, 403, { ok: false, error: "local only" });
      return;
    }

    const url = new URL(req.url ?? "/", "http://127.0.0.1");
    if (url.pathname === "/health") {
      writeDevJson(res, 200, { ok: true, hotkeys: registeredHotkeys, hasWindow: Boolean(mainWindow && !mainWindow.isDestroyed()) });
      return;
    }
    if (url.pathname === "/scanner/start") {
      sendScannerCommand("start-auto");
      writeDevJson(res, 200, { ok: true, command: "start-auto" });
      return;
    }
    if (url.pathname === "/scanner/stop") {
      sendScannerCommand("stop");
      writeDevJson(res, 200, { ok: true, command: "stop" });
      return;
    }
    if (url.pathname === "/scanner/probe") {
      sendScannerCommand("probe-click");
      writeDevJson(res, 200, { ok: true, command: "probe-click" });
      return;
    }
    if (url.pathname === "/automation/click") {
      const x = Number(url.searchParams.get("x"));
      const y = Number(url.searchParams.get("y"));
      if (!Number.isFinite(x) || !Number.isFinite(y)) {
        writeDevJson(res, 400, { ok: false, error: "x and y query params are required" });
        return;
      }
      getInputHelperService()
        .clickScreen(Math.round(x), Math.round(y))
        .then((payload: unknown) => writeDevJson(res, 200, { ok: true, payload }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/status") {
      writeDevJson(res, 200, { ok: true, status: scannerDevStatus });
      return;
    }
    if (url.pathname === "/review/samples") {
      loadReviewSamples(Number(url.searchParams.get("limit") ?? 20))
        .then((payload: unknown) => writeDevJson(res, 200, payload))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }

    writeDevJson(res, 404, { ok: false, error: "unknown endpoint" });
  });

  devControlServer.listen(17317, "127.0.0.1");
}

function createOverlayWindow() {
  if (overlayWindow) {
    overlayWindow.show();
    return;
  }

  const display = screen.getPrimaryDisplay();
  overlayWindow = new BrowserWindow({
    x: display.workArea.x,
    y: display.workArea.y,
    width: display.workArea.width,
    height: display.workArea.height,
    transparent: true,
    frame: false,
    alwaysOnTop: true,
    skipTaskbar: true,
    resizable: false,
    focusable: false,
    webPreferences: {
      preload: path.join(__dirname, "preload.cjs"),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  overlayWindow.setIgnoreMouseEvents(true, { forward: true });

  if (isDev) {
    overlayWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL!}?overlay=1`);
  } else {
    overlayWindow.loadFile(path.join(__dirname, "../dist/index.html"), {
      query: { overlay: "1" },
    });
  }

  overlayWindow.on("closed", () => {
    overlayWindow = null;
  });
}

function dataUrlToBuffer(dataUrl: string) {
  const base64 = dataUrl.replace(/^data:image\/png;base64,/, "");
  return Buffer.from(base64, "base64");
}

// One shared OCR worker. Creating a Tesseract worker per capture added ~1s
// to every artifact during batch scans.
let ocrWorkerPromise: ReturnType<typeof createWorker> | null = null;

function getOcrWorker() {
  if (!ocrWorkerPromise) {
    ocrWorkerPromise = createWorker("eng");
  }
  return ocrWorkerPromise;
}

async function resetOcrWorker() {
  const broken = ocrWorkerPromise;
  ocrWorkerPromise = null;
  if (broken) {
    try {
      const worker = await broken;
      await worker.terminate();
    } catch {
      // Worker never initialized; nothing to clean up.
    }
  }
}

async function runOcrOnCrops(crops: Array<{ id: string; label: string; dataUrl: string }>) {
  try {
    const worker = await getOcrWorker();
    const results = [];
    for (const crop of crops) {
      const recognized = await worker.recognize(dataUrlToBuffer(crop.dataUrl));
      results.push({
        id: crop.id,
        label: crop.label,
        text: cleanOcrText(crop.id, recognized.data.text),
        confidence: Math.round(recognized.data.confidence),
      });
    }
    return results;
  } catch (error) {
    await resetOcrWorker();
    throw error;
  }
}

async function runOcrOnCropsWithTimeout(crops: Array<{ id: string; label: string; dataUrl: string }>, timeoutMs = 6500) {
  let timeout: NodeJS.Timeout | undefined;
  try {
    return await Promise.race([
      runOcrOnCrops(crops).then((ocr) => ({ ocr, timedOut: false })),
      new Promise<{ ocr: Awaited<ReturnType<typeof runOcrOnCrops>>; timedOut: boolean }>((resolve) => {
        timeout = setTimeout(() => resolve({ ocr: [], timedOut: true }), timeoutMs);
      }),
    ]);
  } finally {
    if (timeout) clearTimeout(timeout);
  }
}

function cleanOcrText(cropId: string, text: string) {
  const normalized = text
    .replace(/[â€œâ€]/g, '"')
    .replace(/[â€™]/g, "'")
    .replace(/\r/g, "")
    .split("\n")
    .map((line) => line.replace(/\s+/g, " ").trim())
    .filter(Boolean);

  if (cropId === "artifact-footer") {
    const equipped = normalized.find((line) => /equipped/i.test(line));
    if (!equipped) return "";

    const match = /equipped\s*:?\s*([A-Za-z][A-Za-z'\-\s]{1,32})/i.exec(equipped);
    return match ? `Equipped: ${match[1].replace(/[^A-Za-z'\-\s]/g, "").trim()}` : equipped;
  }

  if (cropId === "artifact-title") {
    return normalized
      .filter((line) => /[A-Za-z]/.test(line))
      .slice(0, 2)
      .join("\n");
  }

  if (cropId === "artifact-main-stat") {
    return normalized
      .filter((line) => /[A-Za-z0-9]/.test(line))
      .slice(0, 3)
      .join("\n");
  }

  if (cropId === "artifact-substats") {
    return normalized
      .filter((line) => /(\+|CRIT|ATK|DEF|HP|Energy|Elemental)/i.test(line))
      .slice(0, 5)
      .join("\n");
  }

  if (cropId === "inventory-count") {
    return normalized
      .map((line) => line.replace(/[^0-9/]/g, ""))
      .find((line) => /[0-9]/.test(line)) ?? "";
  }

  return normalized.join("\n");
}

function parseInventoryCount(ocr: Array<{ id: string; text: string; confidence: number }>) {
  const entry = ocr.find((item) => item.id === "inventory-count");
  if (!entry?.text) return { current: 0, total: 0, confidence: 0, source: "missing" as const, text: "" };
  const cleaned = entry.text.replace(/[^0-9/]/g, "");
  const match = /^(\d{1,4})\/(\d{3,4})$/.exec(cleaned);
  if (match) {
    return {
      current: Number(match[1]),
      total: Number(match[2]),
      confidence: Math.max(0, Math.min(100, entry.confidence)),
      source: "ocr" as const,
      text: cleaned,
    };
  }

  const fallback = cleaned.match(/(\d{1,4})(\d{4})$/);
  if (fallback) {
    return {
      current: Number(fallback[1]),
      total: Number(fallback[2]),
      confidence: Math.max(0, Math.min(84, entry.confidence)),
      source: "ocr" as const,
      text: cleaned,
    };
  }

  return { current: 0, total: 0, confidence: 0, source: "missing" as const, text: cleaned };
}

function isArtifactTitleOrange(bitmap: Buffer, index: number) {
  const blue = bitmap[index];
  const green = bitmap[index + 1];
  const red = bitmap[index + 2];

  return red >= 135 && green >= 70 && green <= 155 && blue <= 95 && red > green + 35 && green > blue + 20;
}

function isEquippedFooterYellow(bitmap: Buffer, index: number) {
  const blue = bitmap[index];
  const green = bitmap[index + 1];
  const red = bitmap[index + 2];

  return red >= 220 && green >= 185 && blue >= 125 && red > blue + 35 && green > blue + 20;
}

function isSetTitleGreen(bitmap: Buffer, index: number) {
  const blue = bitmap[index];
  const green = bitmap[index + 1];
  const red = bitmap[index + 2];

  return green >= 110 && red <= 170 && blue <= 160 && green > red + 12 && green > blue + 10;
}

function isArtifactTextColor(bitmap: Buffer, index: number) {
  const blue = bitmap[index];
  const green = bitmap[index + 1];
  const red = bitmap[index + 2];

  return green >= 180 && red >= 140 && blue <= 95 && green > blue + 35 && red > blue + 10;
}

function waitDelay(ms: number) {
  return new Promise<void>((resolve) => setTimeout(resolve, Math.max(0, Math.floor(ms))));
}

type CropTemplate = { id: string; label: string; rect: Electron.Rectangle };

function getCaptureSourceListOptions() {
  const displays = screen.getAllDisplays();
  const maxSize = displays.reduce(
    (size, display) => ({
      width: Math.max(size.width, display.size.width),
      height: Math.max(size.height, display.size.height),
    }),
    { width: 1920, height: 1080 },
  );

  return { maxSize, fetchWindowIcons: true };
}

async function getAllSources() {
  const { maxSize, fetchWindowIcons } = getCaptureSourceListOptions();
  return desktopCapturer.getSources({ types: ["window", "screen"], thumbnailSize: maxSize, fetchWindowIcons });
}

async function findCaptureSourceById(sourceId: string) {
  const sources = await getAllSources();
  return sources.find((source) => source.id === sourceId) ?? null;
}

function clampCaptureRect(rect: Electron.Rectangle, imageSize: { width: number; height: number }) {
  const clamped = {
    x: Math.max(0, Math.min(imageSize.width - 1, rect.x)),
    y: Math.max(0, Math.min(imageSize.height - 1, rect.y)),
  };
  const maxWidth = Math.max(1, imageSize.width - clamped.x);
  const maxHeight = Math.max(1, imageSize.height - clamped.y);
  return {
    ...clamped,
    width: Math.max(1, Math.min(maxWidth, rect.width)),
    height: Math.max(1, Math.min(maxHeight, rect.height)),
  };
}

function imageCropDataUrl(sourceImage: NativeImage, rect: Electron.Rectangle, imageSize: { width: number; height: number }) {
  const safeRect = clampCaptureRect(rect, imageSize);
  return sourceImage.crop(safeRect).toDataURL();
}

function createCrops(
  sourceImage: NativeImage,
  imageSize: { width: number; height: number },
  detailRect: Electron.Rectangle,
  inventoryRect: Electron.Rectangle,
) {
  const templates: CropTemplate[] = [
    {
      id: "artifact-title",
      label: "Artifact title",
      rect: {
        x: Math.round(detailRect.x + detailRect.width * 0.055),
        y: Math.round(detailRect.y + detailRect.height * 0.05),
        width: Math.round(detailRect.width * 0.82),
        height: Math.round(detailRect.height * 0.16),
      },
    },
    {
      id: "artifact-main-stat",
      label: "Main stat",
      rect: {
        x: Math.round(detailRect.x + detailRect.width * 0.055),
        y: Math.round(detailRect.y + detailRect.height * 0.20),
        width: Math.round(detailRect.width * 0.82),
        height: Math.round(detailRect.height * 0.18),
      },
    },
    {
      id: "artifact-substats",
      label: "Substats",
      rect: {
        x: Math.round(detailRect.x + detailRect.width * 0.055),
        y: Math.round(detailRect.y + detailRect.height * 0.41),
        width: Math.round(detailRect.width * 0.82),
        height: Math.round(detailRect.height * 0.25),
      },
    },
    {
      id: "artifact-footer",
      label: "Footer",
      rect: {
        x: Math.round(detailRect.x + detailRect.width * 0.055),
        y: Math.round(detailRect.y + detailRect.height * 0.78),
        width: Math.round(detailRect.width * 0.82),
        height: Math.round(detailRect.height * 0.16),
      },
    },
  ];

  if (inventoryRect.width > 120 && inventoryRect.height > 80) {
    templates.push({
      id: "inventory-count",
      label: "Inventory count",
      rect: {
        x: Math.round(inventoryRect.x + inventoryRect.width * 0.62),
        y: Math.round(inventoryRect.y + inventoryRect.height * 0.02),
        width: Math.round(inventoryRect.width * 0.34),
        height: Math.round(inventoryRect.height * 0.09),
      },
    });
  }

  return templates
    .map((template) => ({
      ...template,
      rect: clampCaptureRect(template.rect, imageSize),
      dataUrl: imageCropDataUrl(sourceImage, template.rect, imageSize),
    }))
    .filter((crop) => crop.rect.width > 0 && crop.rect.height > 0)
    .map((crop) => ({
      ...crop,
      rect: {
        x: crop.rect.x,
        y: crop.rect.y,
        width: crop.rect.width,
        height: crop.rect.height,
      },
    }));
}

function inferDetailRect(bitmap: Buffer, imageSize: { width: number; height: number }) {
  const { width, height } = imageSize;
  const sampleStrideX = width > 2200 ? 4 : 3;
  const sampleStrideY = height > 1400 ? 4 : 3;
  const candidates: Array<{ x: number; y: number }> = [];

  const search = {
    x0: Math.floor(width * 0.40),
    x1: Math.floor(width * 0.98),
    y0: Math.floor(height * 0.04),
    y1: Math.floor(height * 0.83),
  };

  for (let y = search.y0; y < search.y1; y += sampleStrideY) {
    const rowOffset = y * width * 4;
    for (let x = search.x0; x < search.x1; x += sampleStrideX) {
      const index = rowOffset + x * 4;
      if (isArtifactTitleOrange(bitmap, index) || isSetTitleGreen(bitmap, index) || isArtifactTextColor(bitmap, index)) {
        candidates.push({ x, y });
      }
    }
  }

  if (candidates.length >= 180) {
    const xValues = candidates.map((item) => item.x);
    const yValues = candidates.map((item) => item.y);
    const xMin = Math.min(...xValues);
    const xMax = Math.max(...xValues);
    const yMin = Math.min(...yValues);
    const yMax = Math.max(...yValues);
    const spanX = Math.max(1, xMax - xMin);
    const spanY = Math.max(1, yMax - yMin);
    const widthGuess = Math.max(Math.round(width * 0.30), Math.min(Math.round(width * 0.52), Math.round(spanX * 3.6)));
    const left = Math.max(Math.round(width * 0.42), Math.round((xMin + xMax) / 2 - widthGuess * 0.52));
    const top = Math.max(0, Math.min(height - 1, Math.round(yMin - spanY * 0.4)));
    const heightGuess = Math.max(Math.round(height * 0.58), Math.min(Math.round(height * 0.74), Math.round(spanY * 4.1)));
    return clampCaptureRect({ x: left, y: top, width: widthGuess, height: heightGuess }, imageSize);
  }

  if (width > 0 && height > 0) {
    return clampCaptureRect(
      {
        x: Math.round(width * 0.50),
        y: Math.round(height * 0.08),
        width: Math.round(width * 0.46),
        height: Math.round(height * 0.74),
      },
      imageSize,
    );
  }

  return { x: 0, y: 0, width, height };
}

function inferInventoryRect(imageSize: { width: number; height: number }, detailRect: Electron.Rectangle) {
  const { width, height } = imageSize;
  const preferredWidth = Math.max(140, Math.round(width * 0.48));
  const x = Math.round(width * 0.03);
  const y = Math.round(detailRect.y + detailRect.height * 0.09);
  const availableWidth = Math.max(100, detailRect.x - Math.round(width * 0.04));
  const panelWidth = Math.max(100, Math.min(preferredWidth, availableWidth));
  const safeWidth = panelWidth > width * 0.85 ? Math.round(width * 0.55) : panelWidth;
  return clampCaptureRect(
    {
      x,
      y,
      width: Math.min(safeWidth, Math.max(width - x - Math.round(width * 0.02), 100)),
      height: Math.max(140, Math.round(height * 0.70)),
    },
    imageSize,
  );
}

function inferInventoryGrid(imageSize: { width: number; height: number }, detailRect: Electron.Rectangle) {
  const inventoryRect = inferInventoryRect(imageSize, detailRect);
  const cols = 5;
  if (inventoryRect.width < 160 || inventoryRect.height < 140) {
    return {
      centers: [],
      rows: 0,
      cols: 0,
      confidence: 0,
      source: "missing" as const,
    };
  }

  const cellWidth = Math.max(56, Math.round(inventoryRect.width / cols));
  const stepX = Math.round(cellWidth * 0.96);
  const stepY = Math.round(cellWidth * 1.03);
  const visibleRows = Math.max(2, Math.min(6, Math.round(inventoryRect.height / Math.max(stepY, 1))));

  const startX = inventoryRect.x + Math.max(6, Math.round(stepX * 0.45));
  const startY = inventoryRect.y + Math.max(6, Math.round(stepY * 0.45));
  const centers = [];
  for (let row = 0; row < visibleRows; row++) {
    for (let col = 0; col < cols; col++) {
      const x = startX + col * stepX;
      const y = startY + row * stepY;
      if (x < imageSize.width && y < imageSize.height) {
        centers.push({ x, y, row, col });
      }
    }
  }

  const trimmed = centers.filter((center) => center.x > 0 && center.y > 0);
  return {
    centers: trimmed,
    rows: visibleRows,
    cols,
    confidence: trimmed.length >= cols * 2 ? 76 : trimmed.length >= cols ? 58 : 36,
    source: "detected" as const,
  };
}

async function buildCaptureResult(
  sourceImage: NativeImage,
  sourceId: string,
  sourceName: string,
  captureTarget: CaptureResult["captureTarget"],
  options: CaptureOptions = {},
) {
  const size = sourceImage.getSize();
  if (!size.width || !size.height) {
    throw new Error("Capture produced an empty image.");
  }

  const bitmap = sourceImage.getBitmap();
  const detailRect = inferDetailRect(bitmap, size);
  const inventoryRect = inferInventoryRect(size, detailRect);
  const crops = createCrops(sourceImage, size, detailRect, inventoryRect);
  const croppedPayload = crops.map((crop) => ({
    id: crop.id,
    label: crop.label,
    dataUrl: crop.dataUrl,
  }));
  const recognized = options.skipOcr ? { ocr: [], timedOut: false } : await runOcrOnCropsWithTimeout(croppedPayload);

  const count = parseInventoryCount(recognized.ocr);
  return {
    id: sourceId,
    name: sourceName,
    width: size.width,
    height: size.height,
    dataUrl: sourceImage.toDataURL(),
    capturedAt: new Date().toISOString(),
    captureTarget,
    detailDataUrl: imageCropDataUrl(sourceImage, detailRect, size),
    inventoryDataUrl: imageCropDataUrl(sourceImage, inventoryRect, size),
    ocr: recognized.ocr,
    ocrTimedOut: recognized.timedOut,
    ocrSkipped: Boolean(options.skipOcr),
    crops: crops.map((crop) => ({
      id: crop.id,
      label: crop.label,
      rect: {
        x: crop.rect.x,
        y: crop.rect.y,
        width: crop.rect.width,
        height: crop.rect.height,
      },
      dataUrl: crop.dataUrl,
    })),
    inventoryGrid: inferInventoryGrid(size, detailRect),
    inventoryCount: count,
  };
}

async function captureSource(sourceId: string, delayMs = 0, focusGenshin = false, options?: CaptureOptions): Promise<CaptureResult> {
  if (!Number.isFinite(delayMs) || delayMs < 0) {
    delayMs = 0;
  }

  await waitDelay(Math.floor(delayMs));

  if (focusGenshin) {
    await focusGenshinForScanStart();
  }

  const source = await findCaptureSourceById(sourceId);
  if (!source) {
    throw new Error("Capture source not found.");
  }

  const isGenshinCandidate = isLikelyGenshinSourceName(source.name);
  if (isGenshinCandidate) {
    try {
      return await captureSourceFromGdi(sourceId, source.name, options ?? {});
    } catch {
      // Fall back to desktop thumbnail capture for robustness in low-permission
      // or transient capture failures. OCR will still produce a best-effort result.
    }
  }

  const sourceImage = source.thumbnail;
  if (sourceImage.isEmpty()) {
    return await captureSourceFromGdi(sourceId, source.name, options ?? {});
  }

  return await buildCaptureResult(
    sourceImage,
    sourceId,
    source.name,
    sourceId.startsWith("screen:") ? "desktop-source" : "genshin-client",
    options ?? {},
  );
}

async function exportGood(payload: GoodDatabase): Promise<SaveResultWithPath> {
  const fileNameSafe = `good-export-${new Date().toISOString().replace(/[\\/:]/g, "-").replace(/\..+?$/, "").replace(/\s+/g, "-")}.json`;
  const filePath = exportPath(fileNameSafe);
  try {
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
    return { ok: true, path: filePath };
  } catch {
    return { ok: false, path: filePath };
  }
}

function initializeAppLifecycle() {
  app.whenReady().then(() => {
    const userDataPath = app.getPath("userData");
    repositoryContext = createRepositoryContext(userDataPath);
    artifactStoreRepository = repositoryContext.artifactStoreRepository;
    reviewSamplesRepository = repositoryContext.reviewSamplesRepository;
    scannerLearningRepository = repositoryContext.scannerLearningRepository;
    inputHelperService = createInputHelperService({ userDataPath });

    registerIpcHandlers({
      focusMainWindow: () => focusMainWindow(),
      moveMainWindowOffGenshin: async () => moveMainWindowOffGenshin(),
      focusGenshinForScanStart: () => focusGenshinForScanStart(),
      publishScannerStatus: (status: ScannerStatusPayload) => publishScannerStatus(status),
      readRuntimeInfo: () => readRuntimeInfo(),
      loadSnapshotFromDisk: () => loadSnapshotFromDisk(),
      saveSnapshotToDisk: (snapshot: AppSnapshot) => saveSnapshotToDisk(snapshot),
      runMockScan: () => runMockScan(),
      showOverlayWindow: () => showOverlayWindow(),
      hideOverlayWindow: () => hideOverlayWindow(),
      getArtifactStoreRepository: () => getArtifactStoreRepository(),
      getReviewSamplesRepository: () => getReviewSamplesRepository(),
      artifactStorePath: () => artifactStorePath(),
      reviewSamplesPath: () => reviewSamplesPath(),
      loadReviewSamples: (limit?: number) => loadReviewSamples(limit),
      loadScannerLearningRules: () => loadScannerLearningRules(),
      writeScannerLearningRules: (rules: ScannerLearningRulePayload) => writeScannerLearningRules(rules),
      exportGood: (exportPayload: GoodDatabase) => exportGood(exportPayload),
      listSources: () => listCaptureSources(),
      captureSource: (
        id: string,
        delayMs?: number,
        focus?: boolean,
        captureOptions?: CaptureOptions,
      ) => captureSource(id, delayMs, focus, captureOptions),
      clickScreen: (x: number, y: number) => clickScreenCommand(x, y),
      scrollScreen: (notches: number, anchorX?: number, anchorY?: number) => scrollScreenCommand(notches, anchorX, anchorY),
      getAutomationGuard: () => getAutomationGuardCommand(),
    });

    createMainWindow();
    registerScannerHotkeys();
    startDevControlServer();
  });

  app.on("activate", () => {
    if (!mainWindow || mainWindow.isDestroyed()) {
      createMainWindow();
    }
  });

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

  app.on("will-quit", async () => {
    globalShortcut.unregisterAll();
    if (devControlServer) {
      devControlServer.close();
      devControlServer = null;
    }
    await resetOcrWorker();
    inputHelperService?.dispose();
  });
}

initializeAppLifecycle();

