import {
  app,
  BrowserWindow,
  dialog,
  ipcMain,
  nativeImage,
  type OpenDialogOptions
} from "electron";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  readFileSync,
  renameSync,
  statSync,
  writeFileSync
} from "node:fs";
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { basename, dirname, extname, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import type {
  AiGenerateRequest,
  AiPlayerGradeEvidence,
  AiSettingsPatch,
  AppConfig,
  ConfigPatch,
  ImportProbeResult,
  ScreenshotCropTemplate,
  ScreenshotCropZone,
  ScreenshotExtractionArtifact,
  ScreenshotExtractionPayload,
  ScreenshotExtractionRecord,
  ScreenshotGameScoreCandidate,
  ScreenshotOcrZoneResult,
  ScreenshotPlayerNameCandidate,
  ScreenshotRecord,
  ScreenshotType
} from "../../packages/shared/src";
import {
  getScreenshotCropTemplate,
  parseScreenshotExtractionText
} from "../../packages/shared/src";
import {
  normalizeAiBaseUrl,
  outputContractFor,
  renderAiPrompt,
  requestAiCompletion,
  testAiProvider,
  validateAiArtifactContent
} from "../../packages/ai/src";
import {
  approveScreenshotHighlights,
  approveScreenshotStats,
  createScreenshotExtraction,
  findSnapshotByHash,
  findScreenshotByHash,
  getDatabaseCounts,
  loadAiGeneratedArtifacts,
  loadAiGeneratedArtifactById,
  loadAiPromptPacks,
  loadAiPromptTemplates,
  loadLatestSnapshot,
  loadScreenshotExtractionById,
  loadScreenshotExtractions,
  loadScreenshotById,
  loadPlayHighlights,
  loadScreenshotStats,
  loadSnapshotById,
  loadScreenshots,
  openDynastyDatabase,
  resolveAiPromptTemplate,
  saveAiGeneratedArtifact,
  saveAiPromptTemplate,
  saveParsedSnapshot,
  saveScreenshotRecord,
  DynastyDatabase,
  updateAiGeneratedArtifact,
  updateScreenshotExtractionResult,
  updateScreenshotExtractionRows,
  updateScreenshotExtractionText,
  updateScreenshotType
} from "../../packages/database/src";
import { parseSaveProbe } from "../../packages/parser/src";
import { readConfig, writeConfig } from "./configStore";
import {
  readAiApiKey,
  readAiSettings,
  writeAiSettings
} from "./aiConfigStore";

const currentDir = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const Tesseract = require("tesseract.js") as typeof import("tesseract.js");
const englishOcrData = require("@tesseract.js-data/eng") as {
  code: string;
  gzip: boolean;
  langPath: string;
};

let mainWindow: BrowserWindow | null = null;
let databasePromise: Promise<DynastyDatabase> | null = null;
let configuredSaveParserContextCache: {
  sourcePath: string;
  sourceHash: string;
  gameScoreCandidates: ScreenshotGameScoreCandidate[];
  playerCandidates: ScreenshotPlayerNameCandidate[];
} | null = null;

function createWindow(): void {
  mainWindow = new BrowserWindow({
    width: 1280,
    height: 820,
    minWidth: 720,
    minHeight: 560,
    show: false,
    backgroundColor: "#f7f4ee",
    webPreferences: {
      preload: join(currentDir, "../preload/index.mjs"),
      contextIsolation: true,
      nodeIntegration: false,
      sandbox: false
    }
  });

  mainWindow.once("ready-to-show", () => {
    mainWindow?.show();
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    void mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
  } else {
    void mainWindow.loadFile(join(currentDir, "../renderer/index.html"));
  }
}

function registerIpc(): void {
  ipcMain.handle("config:get", () => readConfig());

  ipcMain.handle("config:update", (_event, patch: ConfigPatch) => {
    return writeConfig(patch);
  });

  ipcMain.handle("dialog:selectSaveFolder", async () => {
    const config = readConfig();
    const dialogOptions: OpenDialogOptions = {
      title: "Select Save Folder",
      defaultPath: config.saveFolder ?? undefined,
      properties: ["openDirectory"]
    };
    const result = mainWindow
      ? await dialog.showOpenDialog(mainWindow, dialogOptions)
      : await dialog.showOpenDialog(dialogOptions);

    if (result.canceled || result.filePaths.length === 0) return config;
    return writeConfig({ saveFolder: result.filePaths[0] });
  });

  ipcMain.handle("dialog:selectSaveFile", async () => {
    const config = readConfig();
    const dialogOptions: OpenDialogOptions = {
      title: "Select Primary Dynasty Save",
      defaultPath: config.saveFolder ?? undefined,
      properties: ["openFile"]
    };
    const result = mainWindow
      ? await dialog.showOpenDialog(mainWindow, dialogOptions)
      : await dialog.showOpenDialog(dialogOptions);

    if (result.canceled || result.filePaths.length === 0) return config;

    const savePath = result.filePaths[0];
    return writeConfig({
      saveFolder: dirname(savePath),
      primarySaveFile: basename(savePath)
    });
  });

  ipcMain.handle("import:latest", async () => {
    const db = await getDatabase();
    const config = readConfig();
    const sourcePath = resolveConfiguredSavePath(config.saveFolder, config.primarySaveFile);
    const sourceHash = hashFile(sourcePath);

    const duplicate = findSnapshotByHash(db, sourceHash);
    if (duplicate) {
      const stored = loadSnapshotById(db, duplicate.snapshotId);
      if (!stored) {
        throw new Error(`Duplicate snapshot ${duplicate.snapshotId} could not be loaded.`);
      }

      stored.importStatus = {
        snapshotId: duplicate.snapshotId,
        imported: false,
        duplicate: true,
        databasePath: getDatabasePath()
      };
      if (stored.dynastyId) writeConfig({ activeDynastyId: stored.dynastyId });
      return stored;
    }

    const archivePath = archiveSavePending(sourcePath, sourceHash);
    const openedAt = new Date().toISOString();
    const result = await parseSaveProbe(archivePath, {
      sourceHash,
      archivePath,
      openedAt
    });

    result.archivePath = finalizeArchiveSave(
      archivePath,
      result.dynastyId,
      sourcePath,
      sourceHash
    );

    const stored = saveParsedSnapshot(db, result);
    result.importStatus = {
      snapshotId: stored.snapshotId,
      imported: true,
      duplicate: false,
      databasePath: getDatabasePath()
    };

    if (result.dynastyId) writeConfig({ activeDynastyId: result.dynastyId });

    console.info(
      "[Dynasty Live import]",
      JSON.stringify(
        {
          database: getDatabasePath(),
          counts: getDatabaseCounts(db),
          calendar: result.calendar,
          userTeam: result.userTeam,
          games: result.games.slice(0, 12)
        },
        null,
        2
      )
    );

    return result;
  });

  ipcMain.handle("snapshot:latest", async () => {
    const db = await getDatabase();
    const config = readConfig();
    const result = loadLatestSnapshot(db, config.activeDynastyId);
    if (result?.dynastyId && result.dynastyId !== config.activeDynastyId) {
      writeConfig({ activeDynastyId: result.dynastyId });
    }
    return result;
  });

  ipcMain.handle("screenshots:import", async () => {
    const db = await getDatabase();
    const config = readConfig();
    const latestSnapshot = loadLatestSnapshot(db, config.activeDynastyId);
    const dynastyId = latestSnapshot?.dynastyId ?? config.activeDynastyId;
    const snapshotId = latestSnapshot?.importStatus?.snapshotId ?? null;
    const dialogOptions: OpenDialogOptions = {
      title: "Import Screenshots",
      properties: ["openFile", "multiSelections"],
      filters: [
        {
          name: "Images",
          extensions: ["png", "jpg", "jpeg", "webp", "bmp"]
        }
      ]
    };
    const result = mainWindow
      ? await dialog.showOpenDialog(mainWindow, dialogOptions)
      : await dialog.showOpenDialog(dialogOptions);

    if (result.canceled || result.filePaths.length === 0) {
      return { imported: [], duplicates: [] };
    }

    const imported: ScreenshotRecord[] = [];
    const duplicates: ScreenshotRecord[] = [];
    for (const sourcePath of result.filePaths) {
      const fileHash = hashFile(sourcePath);
      const existing = findScreenshotByHash(db, fileHash);
      if (existing) {
        duplicates.push(existing);
        continue;
      }

      const storedPath = archiveScreenshot(sourcePath, dynastyId, fileHash);
      imported.push(
        saveScreenshotRecord(db, {
          dynastyId,
          snapshotId,
          originalFileName: basename(sourcePath),
          storedPath,
          fileHash,
          screenshotType: inferScreenshotType(sourcePath)
        })
      );
    }

    return { imported, duplicates };
  });

  ipcMain.handle("screenshots:list", async () => {
    const db = await getDatabase();
    const config = readConfig();
    return loadScreenshots(db, config.activeDynastyId);
  });

  ipcMain.handle(
    "screenshots:updateType",
    async (_event, id: number, screenshotType: ScreenshotType) => {
      const db = await getDatabase();
      return updateScreenshotType(db, id, screenshotType);
    }
  );

  ipcMain.handle("screenshots:extractions:list", async () => {
    const db = await getDatabase();
    const config = readConfig();
    return loadScreenshotExtractions(db, config.activeDynastyId);
  });

  ipcMain.handle("screenshots:extractions:create", async (_event, screenshotId: number) => {
    const db = await getDatabase();
    const screenshot = loadScreenshotById(db, screenshotId);
    if (!screenshot) throw new Error(`Screenshot ${screenshotId} was not found.`);

    const template = getScreenshotCropTemplate(screenshot.screenshotType);
    if (!template) {
      throw new Error(
        `Screenshot ${screenshotId} needs a known type before extraction.`
      );
    }

    return createScreenshotExtraction(db, screenshotId, {
      extractedJson: prepareScreenshotExtractionArtifacts(screenshot, template)
    });
  });

  ipcMain.handle(
    "screenshots:extractions:updateText",
    async (_event, id: number, text: string) => {
      const db = await getDatabase();
      return updateScreenshotExtractionText(db, id, text);
    }
  );

  ipcMain.handle(
    "screenshots:extractions:updateRows",
    async (_event, id: number, rows: unknown) => {
      if (!Array.isArray(rows)) {
        throw new Error("Corrected screenshot rows must be an array.");
      }
      const db = await getDatabase();
      return updateScreenshotExtractionRows(db, id, rows);
    }
  );

  ipcMain.handle("screenshots:extractions:runOcr", async (_event, id: number) => {
    const db = await getDatabase();
    const extraction = loadScreenshotExtractionById(db, id);
    if (!extraction) throw new Error(`Screenshot extraction ${id} was not found.`);

    try {
      const completedAt = new Date().toISOString();
      return updateScreenshotExtractionResult(db, id, {
        status: "completed",
        extractedJson: await runOcrForExtraction(extraction, completedAt),
        completedAt
      });
    } catch (caught) {
      return updateScreenshotExtractionResult(db, id, {
        status: "failed",
        errorMessage: errorMessage(caught),
        completedAt: new Date().toISOString()
      });
    }
  });

  ipcMain.handle(
    "screenshots:extractions:parseRows",
    async (_event, id: number, targetGameRow?: number | null) => {
      const db = await getDatabase();
      const config = readConfig();
      const extraction = loadScreenshotExtractionById(db, id);
      if (!extraction) throw new Error(`Screenshot extraction ${id} was not found.`);
      const screenshot = loadScreenshotById(db, extraction.screenshotId);
      const snapshot = screenshot?.snapshotId
        ? loadSnapshotById(db, screenshot.snapshotId)
        : null;
      const latestSnapshot = loadLatestSnapshot(
        db,
        screenshot?.dynastyId ?? config.activeDynastyId
      );
      const storedSnapshotHashes = new Set(
        [snapshot?.sourceHash, latestSnapshot?.sourceHash].filter(
          (hash): hash is string => Boolean(hash)
        )
      );
      const configuredSaveContext = await configuredSaveParserContext(
        config,
        storedSnapshotHashes
      );
      const gameScoreCandidates = prioritizeTargetGameScoreCandidates(
        uniqueGameScoreCandidates([
          ...screenshotGameScoreCandidates(snapshot),
          ...screenshotGameScoreCandidates(latestSnapshot),
          ...configuredSaveContext.gameScoreCandidates
        ]),
        normalizeTargetGameRow(targetGameRow)
      );
      const playerCandidates = uniquePlayerNameCandidates([
        ...screenshotPlayerNameCandidates(snapshot),
        ...screenshotPlayerNameCandidates(latestSnapshot),
        ...configuredSaveContext.playerCandidates
      ]);

      const rows = parseScreenshotExtractionText(
        extraction.screenshotType,
        extraction.extractedJson.text ?? "",
        {
          gameScoreCandidates,
          playerCandidates
        }
      );
      return updateScreenshotExtractionResult(db, id, {
        status: extraction.status,
        extractedJson: {
          rawRows: rows,
          rows,
          approval: null
        }
      });
    }
  );

  ipcMain.handle(
    "screenshots:highlights:approve",
    async (_event, extractionId: number, gameRow: number, snapshotId: number | null) => {
      const db = await getDatabase();
      return approveScreenshotHighlights(db, extractionId, gameRow, snapshotId);
    }
  );

  ipcMain.handle(
    "screenshots:stats:approve",
    async (_event, extractionId: number, gameRow: number, snapshotId: number | null) => {
      const db = await getDatabase();
      return approveScreenshotStats(db, extractionId, gameRow, snapshotId);
    }
  );

  ipcMain.handle(
    "highlights:list",
    async (_event, snapshotId: number | null, gameRow: number | null) => {
      const db = await getDatabase();
      return loadPlayHighlights(db, snapshotId, gameRow);
    }
  );

  ipcMain.handle(
    "screenshots:stats:list",
    async (_event, snapshotId: number | null, gameRow: number | null) => {
      const db = await getDatabase();
      return loadScreenshotStats(db, snapshotId, gameRow);
    }
  );

  ipcMain.handle("ai:settings:get", () => readAiSettings());

  ipcMain.handle("ai:settings:update", (_event, patch: AiSettingsPatch) => {
    if (patch.baseUrl !== undefined) {
      patch = {
        ...patch,
        baseUrl: normalizeAiBaseUrl(patch.baseUrl)
      };
    }
    return writeAiSettings(patch);
  });

  ipcMain.handle("ai:connection:test", async () => {
    const settings = readAiSettings();
    return testAiProvider({
      baseUrl: settings.baseUrl,
      apiKey: readAiApiKey(),
      model: settings.model
    });
  });

  ipcMain.handle("ai:promptPacks:list", async () => {
    return loadAiPromptPacks(await getDatabase());
  });

  ipcMain.handle("ai:promptTemplates:list", async () => {
    const db = await getDatabase();
    return loadAiPromptTemplates(db, readConfig().activeDynastyId);
  });

  ipcMain.handle(
    "ai:promptTemplates:save",
    async (_event, id: number, template: string) => {
      const dynastyId = readConfig().activeDynastyId;
      if (!dynastyId) throw new Error("Import a dynasty before editing prompts.");
      return saveAiPromptTemplate(await getDatabase(), id, dynastyId, template);
    }
  );

  ipcMain.handle(
    "ai:artifacts:generate",
    async (_event, request: AiGenerateRequest) => {
      return generateAiArtifact(request);
    }
  );

  ipcMain.handle(
    "ai:artifacts:list",
    async (_event, snapshotId: number, gameRow: number) => {
      return loadAiGeneratedArtifacts(await getDatabase(), snapshotId, gameRow);
    }
  );

  ipcMain.handle(
    "ai:artifacts:update",
    async (_event, id: number, content: Record<string, unknown>) => {
      if (!content || typeof content !== "object" || Array.isArray(content)) {
        throw new Error("Artifact content must be a JSON object.");
      }
      const db = await getDatabase();
      const existing = loadAiGeneratedArtifactById(db, id);
      if (!existing) throw new Error(`Generated artifact ${id} was not found.`);
      return updateAiGeneratedArtifact(
        db,
        id,
        validateAiArtifactContent(existing.contentType, content)
      );
    }
  );
}

async function generateAiArtifact(request: AiGenerateRequest) {
  const db = await getDatabase();
  const snapshot = loadSnapshotById(db, request.snapshotId);
  if (!snapshot) throw new Error(`Snapshot ${request.snapshotId} was not found.`);
  if (!snapshot.dynastyId) {
    throw new Error(`Snapshot ${request.snapshotId} has no dynasty identity.`);
  }
  const game = snapshot.games.find((candidate) => candidate.row === request.gameRow);
  if (!game) {
    throw new Error(`Game ${request.gameRow} was not found in snapshot ${request.snapshotId}.`);
  }

  const settings = readAiSettings();
  const packs = loadAiPromptPacks(db);
  const pack =
    packs.find((candidate) => candidate.id === settings.promptPackId) ??
    packs.find((candidate) => candidate.id === "balanced");
  if (!pack) throw new Error("No AI prompt pack is available.");

  const [highlights, screenshotStats] = [
    loadPlayHighlights(db, request.snapshotId, request.gameRow),
    loadScreenshotStats(db, request.snapshotId, request.gameRow)
  ];
  const mode =
    highlights.length || screenshotStats.length
      ? ("screenshot_enriched" as const)
      : ("save_only" as const);
  const promptTemplate = resolveAiPromptTemplate(db, {
    dynastyId: snapshot.dynastyId,
    packId: pack.id,
    contentType: request.contentType,
    mode,
    requestedId: request.promptTemplateId
  });
  const gradeEvidence = validatedGradeEvidence(
    request.contentType,
    request.gradeEvidence ?? [],
    snapshot
  );
  const evidence = buildAiEvidence({
    snapshot,
    gameRow: request.gameRow,
    mode,
    highlights,
    screenshotStats,
    gradeEvidence
  });
  const renderedPrompt = renderAiPrompt(promptTemplate.template, {
    factsJson: JSON.stringify(evidence),
    modeGuidance:
      mode === "screenshot_enriched"
        ? `${highlights.length} approved highlights and ${screenshotStats.length} approved screenshot stat rows are included.`
        : "Use schedule, score, team stats, player stats, and deterministic grades only.",
    personalitiesJson: JSON.stringify(
      pack.personalities.filter(
        (personality) =>
          personality.enabled &&
          !settings.disabledPersonalityIds.includes(personality.id)
      )
    ),
    outputContract: outputContractFor(request.contentType)
  });
  const model =
    settings.contentModels[request.contentType]?.trim() || settings.model.trim();
  const completion = await requestAiCompletion(
    {
      baseUrl: settings.baseUrl,
      apiKey: readAiApiKey(),
      model,
      contentType: request.contentType
    },
    renderedPrompt
  );
  const content = validateAiArtifactContent(request.contentType, completion.content);

  return saveAiGeneratedArtifact(db, {
    dynastyId: snapshot.dynastyId,
    snapshotId: request.snapshotId,
    gameRow: request.gameRow,
    contentType: request.contentType,
    mode,
    promptTemplateId: promptTemplate.id,
    providerBaseUrl: normalizeAiBaseUrl(settings.baseUrl),
    model: completion.model,
    renderedPrompt,
    evidence,
    content
  });
}

function buildAiEvidence({
  snapshot,
  gameRow,
  mode,
  highlights,
  screenshotStats,
  gradeEvidence
}: {
  snapshot: ImportProbeResult;
  gameRow: number;
  mode: "save_only" | "screenshot_enriched";
  highlights: ReturnType<typeof loadPlayHighlights>;
  screenshotStats: ReturnType<typeof loadScreenshotStats>;
  gradeEvidence: AiPlayerGradeEvidence[];
}): Record<string, unknown> {
  const game = snapshot.games.find((candidate) => candidate.row === gameRow);
  if (!game) throw new Error(`Game ${gameRow} was not found.`);
  const gameTeamRows = new Set(
    [game.awayTeam?.row, game.homeTeam?.row].filter(
      (value): value is number => value !== null && value !== undefined
    )
  );
  return {
    evidenceVersion: 1,
    mode,
    calendar: snapshot.calendar,
    userTeam: snapshot.userTeam?.team
      ? {
          row: snapshot.userTeam.team.row,
          name: snapshot.userTeam.team.displayName,
          shortName: snapshot.userTeam.team.shortName,
          rank: snapshot.userTeam.team.rank,
          conference: snapshot.userTeam.team.conferenceName
        }
      : null,
    coach: snapshot.userTeam?.coach
      ? {
          row: snapshot.userTeam.coach.row,
          name: snapshot.userTeam.coach.name,
          role: snapshot.userTeam.coach.role
        }
      : null,
    relationshipState: {
      lockerRoom: 50,
      fans: 50,
      media: 50,
      school: 50,
      boosters: 50,
      source: "Phase 6 not initialized; neutral defaults"
    },
    game: {
      row: game.row,
      seasonYear: game.seasonYear,
      week: game.week,
      weekType: game.weekType,
      status: game.status,
      awayTeam: game.awayTeam?.displayName ?? null,
      homeTeam: game.homeTeam?.displayName ?? null,
      awayScore: game.awayScore,
      homeScore: game.homeScore,
      quarterScores: game.quarterScores,
      isSimmed: game.isSimmed,
      isMajorGame: game.isMajorGame,
      majorGameReasons: game.majorGameReasons,
      attendance: game.attendance,
      weather: game.weather
    },
    teamStats: snapshot.teamGameStats
      .filter((row) => row.gameRow === gameRow)
      .map((row) => ({
        teamRow: row.teamRow,
        side: row.side,
        stats: row.stats
      })),
    playerStats: snapshot.playerGameStats
      .filter((row) => row.gameRow === gameRow)
      .filter((row) => row.teamRow === null || gameTeamRows.has(row.teamRow))
      .map((row) => ({
        playerRow: row.playerRow,
        playerName: row.playerName,
        teamRow: row.teamRow,
        statGroup: row.statGroup,
        stats: row.stats
      })),
    deterministicGrades: gradeEvidence,
    approvedHighlights: highlights.map((highlight) => ({
      sequence: highlight.sequence,
      period: highlight.period,
      clock: highlight.clock,
      team: highlight.team,
      summary: highlight.summary,
      score: highlight.score
    })),
    approvedScreenshotStats: screenshotStats.map((row) => row.correctedRow)
  };
}

function validatedGradeEvidence(
  contentType: AiGenerateRequest["contentType"],
  grades: AiPlayerGradeEvidence[],
  snapshot: ImportProbeResult
): AiPlayerGradeEvidence[] {
  if (contentType !== "grade_explanation") return [];
  if (!grades.length) {
    throw new Error("Player grade generation requires deterministic grade evidence.");
  }
  const playersByRow = new Map(snapshot.players.map((player) => [player.row, player]));
  return grades.slice(0, 12).map((grade) => {
    const player = playersByRow.get(grade.playerRow);
    if (!player || player.displayName !== grade.playerName) {
      throw new Error(`Player grade evidence for ${grade.playerName} is invalid.`);
    }
    return {
      ...grade,
      score: Math.max(0, Math.min(100, Math.round(grade.score))),
      factors: grade.factors.slice(0, 12)
    };
  });
}

async function configuredSaveParserContext(
  config: AppConfig,
  storedSnapshotHashes: Set<string>
): Promise<{
  gameScoreCandidates: ScreenshotGameScoreCandidate[];
  playerCandidates: ScreenshotPlayerNameCandidate[];
}> {
  const emptyContext = {
    gameScoreCandidates: [],
    playerCandidates: []
  };
  if (!config.saveFolder || !config.primarySaveFile) return emptyContext;

  try {
    const sourcePath = resolveConfiguredSavePath(
      config.saveFolder,
      config.primarySaveFile
    );
    const sourceHash = hashFile(sourcePath);
    if (storedSnapshotHashes.has(sourceHash)) return emptyContext;
    if (
      configuredSaveParserContextCache?.sourcePath === sourcePath &&
      configuredSaveParserContextCache.sourceHash === sourceHash
    ) {
      return {
        gameScoreCandidates: configuredSaveParserContextCache.gameScoreCandidates,
        playerCandidates: configuredSaveParserContextCache.playerCandidates
      };
    }

    const result = await parseSaveProbe(sourcePath, {
      sourceHash,
      archivePath: sourcePath,
      openedAt: new Date().toISOString()
    });
    const gameScoreCandidates = screenshotGameScoreCandidates(result);
    const playerCandidates = screenshotPlayerNameCandidates(result);
    configuredSaveParserContextCache = {
      sourcePath,
      sourceHash,
      gameScoreCandidates,
      playerCandidates
    };
    return {
      gameScoreCandidates,
      playerCandidates
    };
  } catch (caught) {
    console.warn(
      "[Dynasty Live OCR] Could not parse configured save for OCR matching:",
      errorMessage(caught)
    );
    return emptyContext;
  }
}

function screenshotGameScoreCandidates(
  snapshot: ImportProbeResult | null
): ScreenshotGameScoreCandidate[] {
  if (!snapshot) return [];

  return snapshot.games.map((game) => ({
    gameRow: game.row,
    awayTeamShortName: game.awayTeam?.shortName ?? null,
    awayTeamDisplayName: game.awayTeam?.displayName ?? null,
    awayScore: game.awayScore,
    homeTeamShortName: game.homeTeam?.shortName ?? null,
    homeTeamDisplayName: game.homeTeam?.displayName ?? null,
    homeScore: game.homeScore
  }));
}

function normalizeTargetGameRow(value: unknown): number | null {
  return typeof value === "number" && Number.isInteger(value) ? value : null;
}

function prioritizeTargetGameScoreCandidates(
  gameScoreCandidates: ScreenshotGameScoreCandidate[],
  targetGameRow: number | null
): ScreenshotGameScoreCandidate[] {
  if (targetGameRow === null) return gameScoreCandidates;

  const targetCandidates = gameScoreCandidates.filter(
    (candidate) => candidate.gameRow === targetGameRow
  );
  return targetCandidates.length ? targetCandidates : gameScoreCandidates;
}

function screenshotPlayerNameCandidates(
  snapshot: ImportProbeResult | null
): ScreenshotPlayerNameCandidate[] {
  if (!snapshot) return [];

  const teamsByRow = new Map(snapshot.teams.map((team) => [team.row, team]));
  return snapshot.players.map((player) => {
    const team =
      player.teamRow !== null ? teamsByRow.get(player.teamRow) ?? null : null;
    return {
      displayName: player.displayName,
      firstName: player.firstName,
      lastName: player.lastName,
      position: player.position,
      teamShortName: team?.shortName ?? null,
      teamDisplayName: team?.displayName ?? null
    };
  });
}

function uniquePlayerNameCandidates(
  playerCandidates: ScreenshotPlayerNameCandidate[]
): ScreenshotPlayerNameCandidate[] {
  const byKey = new Map<string, ScreenshotPlayerNameCandidate>();
  for (const candidate of playerCandidates) {
    const key = [
      candidate.displayName.toLowerCase(),
      candidate.position?.toLowerCase() ?? "",
      candidate.teamShortName?.toLowerCase() ?? "",
      candidate.teamDisplayName?.toLowerCase() ?? ""
    ].join("|");
    if (!byKey.has(key)) byKey.set(key, candidate);
  }
  return Array.from(byKey.values());
}

function uniqueGameScoreCandidates(
  gameScoreCandidates: ScreenshotGameScoreCandidate[]
): ScreenshotGameScoreCandidate[] {
  const byKey = new Map<string, ScreenshotGameScoreCandidate>();
  for (const candidate of gameScoreCandidates) {
    const key = [
      candidate.gameRow ?? "",
      candidate.awayTeamShortName?.toLowerCase() ?? "",
      candidate.awayTeamDisplayName?.toLowerCase() ?? "",
      candidate.awayScore ?? "",
      candidate.homeTeamShortName?.toLowerCase() ?? "",
      candidate.homeTeamDisplayName?.toLowerCase() ?? "",
      candidate.homeScore ?? ""
    ].join("|");
    if (!byKey.has(key)) byKey.set(key, candidate);
  }
  return Array.from(byKey.values());
}

function resolveConfiguredSavePath(
  saveFolder: string | null,
  primarySaveFile: string | null
): string {
  if (!saveFolder || !primarySaveFile) {
    throw new Error("Select a save folder and primary save file before importing.");
  }

  const sourcePath = isAbsolute(primarySaveFile)
    ? primarySaveFile
    : join(saveFolder, primarySaveFile);
  if (!existsSync(sourcePath)) {
    throw new Error(`Configured save file does not exist: ${sourcePath}`);
  }

  const stat = statSync(sourcePath);
  if (!stat.isFile()) {
    throw new Error(`Configured save path is not a file: ${sourcePath}`);
  }

  return sourcePath;
}

function hashFile(filePath: string): string {
  return createHash("sha256").update(readFileSync(filePath)).digest("hex");
}

function getDatabase(): Promise<DynastyDatabase> {
  databasePromise ??= openDynastyDatabase(getDatabasePath());
  return databasePromise;
}

function getDatabasePath(): string {
  return join(app.getPath("userData"), "dynasty-live.sqlite");
}

function archiveSavePending(sourcePath: string, sourceHash: string): string {
  const archiveDir = join(app.getPath("userData"), "archive", "_pending");
  mkdirSync(archiveDir, { recursive: true });

  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const archivedName = `${timestamp}-${sourceHash.slice(0, 12)}-${basename(sourcePath)}`;
  const archivePath = join(archiveDir, archivedName);
  copyFileSync(sourcePath, archivePath);
  return archivePath;
}

function finalizeArchiveSave(
  pendingPath: string,
  dynastyId: string | null,
  sourcePath: string,
  sourceHash: string
): string {
  const archiveDir = join(
    app.getPath("userData"),
    "archive",
    sanitizeArchiveSegment(dynastyId ?? "unidentified")
  );
  mkdirSync(archiveDir, { recursive: true });

  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const archivePath = join(
    archiveDir,
    `${timestamp}-${sourceHash.slice(0, 12)}-${basename(sourcePath)}`
  );
  renameSync(pendingPath, archivePath);
  return archivePath;
}

function archiveScreenshot(
  sourcePath: string,
  dynastyId: string | null,
  fileHash: string
): string {
  const archiveDir = join(
    app.getPath("userData"),
    "screenshots",
    sanitizeArchiveSegment(dynastyId ?? "unassigned")
  );
  mkdirSync(archiveDir, { recursive: true });

  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const extension = extname(sourcePath) || ".png";
  const archivePath = join(
    archiveDir,
    `${timestamp}-${fileHash.slice(0, 12)}${extension}`
  );
  copyFileSync(sourcePath, archivePath);
  return archivePath;
}

function inferScreenshotType(sourcePath: string): ScreenshotType {
  const image = nativeImage.createFromPath(sourcePath);
  const size = image.getSize();
  if (size.width >= 2200 && size.height >= 900) return "highlight_list";
  if (size.width >= 1600 && size.height <= 500) return "passing_stats";
  if (size.width >= 1750 && size.height >= 640) return "rushing_stats";
  if (size.width >= 1750 && size.height >= 560) return "receiving_stats";
  return "unknown";
}

function prepareScreenshotExtractionArtifacts(
  screenshot: ScreenshotRecord,
  template: ScreenshotCropTemplate
): ScreenshotExtractionPayload {
  if (!existsSync(screenshot.storedPath)) {
    throw new Error(`Screenshot file does not exist: ${screenshot.storedPath}`);
  }

  const image = nativeImage.createFromPath(screenshot.storedPath);
  if (image.isEmpty()) {
    throw new Error(`Screenshot image could not be opened: ${screenshot.storedPath}`);
  }

  const size = image.getSize();
  if (size.width <= 0 || size.height <= 0) {
    throw new Error(`Screenshot image has invalid dimensions: ${screenshot.storedPath}`);
  }

  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const artifactDir = join(
    app.getPath("userData"),
    "extractions",
    String(screenshot.id),
    timestamp
  );
  mkdirSync(artifactDir, { recursive: true });

  return {
    image: {
      width: size.width,
      height: size.height
    },
    artifacts: template.zones.map((zone) => {
      const rect = cropZoneRect(zone, size);
      const artifactPath = join(artifactDir, `${zone.key}.png`);
      writeFileSync(artifactPath, image.crop(rect).toPNG());
      return {
        zoneKey: zone.key,
        label: zone.label,
        purpose: zone.purpose,
        path: artifactPath,
        ...rect
      };
    })
  };
}

async function runOcrForExtraction(
  extraction: ScreenshotExtractionRecord,
  completedAt: string
): Promise<ScreenshotExtractionPayload> {
  const artifacts = extraction.extractedJson.artifacts ?? [];
  if (!artifacts.length) {
    throw new Error(`Screenshot extraction ${extraction.id} has no crop artifacts.`);
  }

  const worker = await Tesseract.createWorker(englishOcrData.code, Tesseract.OEM.LSTM_ONLY, {
    cachePath: join(app.getPath("userData"), "ocr-cache"),
    corePath: dirname(require.resolve("tesseract.js-core")),
    gzip: englishOcrData.gzip,
    langPath: englishOcrData.langPath,
    workerPath: require.resolve("tesseract.js/src/worker-script/node/index.js")
  });

  try {
    await worker.setParameters({
      preserve_interword_spaces: "1",
      user_defined_dpi: "300"
    });

    const zones: ScreenshotOcrZoneResult[] = [];
    for (const artifact of artifacts) {
      zones.push(await recognizeArtifact(worker, artifact));
    }

    return {
      ...extraction.extractedJson,
      rawRows: undefined,
      rows: undefined,
      approval: null,
      ocr: {
        engine: "tesseract.js",
        language: englishOcrData.code,
        completedAt,
        zones
      },
      text: zonesToOcrText(zones)
    };
  } finally {
    await worker.terminate();
  }
}

async function recognizeArtifact(
  worker: Awaited<ReturnType<typeof Tesseract.createWorker>>,
  artifact: ScreenshotExtractionArtifact
): Promise<ScreenshotOcrZoneResult> {
  if (!existsSync(artifact.path)) {
    throw new Error(`Crop artifact does not exist: ${artifact.path}`);
  }

  await worker.setParameters({
    tessedit_pageseg_mode:
      artifact.purpose === "rows"
        ? Tesseract.PSM.SINGLE_BLOCK
        : Tesseract.PSM.SPARSE_TEXT
  });

  const result = await worker.recognize(artifact.path);
  return {
    zoneKey: artifact.zoneKey,
    label: artifact.label,
    purpose: artifact.purpose,
    text: normalizeOcrText(result.data.text),
    confidence:
      typeof result.data.confidence === "number" && Number.isFinite(result.data.confidence)
        ? result.data.confidence
        : null
  };
}

function zonesToOcrText(zones: ScreenshotOcrZoneResult[]): string {
  return zones
    .map((zone) => {
      const text = zone.text.trim();
      return text ? `[${zone.label}]\n${text}` : `[${zone.label}]`;
    })
    .join("\n\n")
    .trim();
}

function normalizeOcrText(value: string): string {
  return value
    .replace(/\r\n/g, "\n")
    .replace(/[ \t]+\n/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function cropZoneRect(
  zone: ScreenshotCropZone,
  size: { width: number; height: number }
): { x: number; y: number; width: number; height: number } {
  const x = clampInt(Math.round((zone.xPct / 100) * size.width), 0, size.width - 1);
  const y = clampInt(Math.round((zone.yPct / 100) * size.height), 0, size.height - 1);
  const maxWidth = size.width - x;
  const maxHeight = size.height - y;
  return {
    x,
    y,
    width: clampInt(Math.round((zone.widthPct / 100) * size.width), 1, maxWidth),
    height: clampInt(Math.round((zone.heightPct / 100) * size.height), 1, maxHeight)
  };
}

function clampInt(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}

function sanitizeArchiveSegment(value: string): string {
  return value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 96) || "unidentified";
}

function errorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

app.whenReady().then(() => {
  registerIpc();
  createWindow();

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

app.on("window-all-closed", () => {
  void databasePromise?.then((db) => db.close());
  databasePromise = null;
  if (process.platform !== "darwin") app.quit();
});
