import fs from "node:fs/promises";
import http, { type Server } from "node:http";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import type {
  CaptureOptions,
  CaptureResult,
  CaptureSourceInfo,
  ClickResult,
  ReviewSampleListResult,
  NativeScannerCatalogStatus,
  NativeScannerDataStatus,
  NativeScannerImageLoadStatus,
  NativeScannerPreflightStatus,
  NativeScannerProcessStatus,
  NativeScannerResultsLoadStatus,
  NativeScannerRunStatus,
  ScannerCommand,
  ScannerStatusPayload,
  AppRuntimeInfo,
} from "../src/types/global.js";
import { validateLookupPackage } from "../src/lib/genshinLookup.js";

const execFileAsync = promisify(execFile);

interface DevControlServerDependencies {
  registeredHotkeys: Record<string, boolean>;
  appBuild: AppRuntimeInfo;
  hasMainWindow: () => boolean;
  sendScannerCommand: (command: ScannerCommand | "probe-click") => void;
  clickScreen: (x: number, y: number) => Promise<ClickResult>;
  scannerStatus: () => ScannerStatusPayload;
  nativeScannerDataStatus: () => Promise<NativeScannerDataStatus>;
  nativeScannerCatalog: () => Promise<NativeScannerCatalogStatus>;
  nativeScannerPreflight: (options?: { category?: string }) => Promise<NativeScannerPreflightStatus>;
  nativeScannerStart: (options?: { limit?: number; category?: string }) => Promise<NativeScannerRunStatus>;
  nativeScannerStop: () => Promise<NativeScannerRunStatus>;
  nativeScannerStatus: () => Promise<NativeScannerRunStatus>;
  nativeScannerProcessRun: (options?: { runDir?: string; persist?: boolean; limit?: number }) => Promise<NativeScannerProcessStatus>;
  nativeScannerLoadResults: (options?: { runDir?: string; limit?: number }) => Promise<NativeScannerResultsLoadStatus>;
  nativeScannerLoadImage: (options: { runDir?: string; imagePath: string }) => Promise<NativeScannerImageLoadStatus>;
  warmOcr: (engine: "current") => Promise<unknown>;
  loadReviewSamples: (limit?: number) => Promise<ReviewSampleListResult>;
  listCaptureSources: () => Promise<CaptureSourceInfo[]>;
  captureSource: (
    id: string,
    delayMs?: number,
    focusGenshin?: boolean,
    options?: CaptureOptions,
  ) => Promise<CaptureResult>;
  requestShutdown?: (reason: string) => void;
}

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 dataUrlBase64(dataUrl: string) {
  return dataUrl.replace(/^data:image\/png;base64,/, "");
}

function devCaptureOutputDir() {
  return path.join(process.cwd(), "outputs", "live-capture");
}

function safeDebugFilePart(value: string) {
  return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
}

function dataUrlFingerprint(dataUrl: string | undefined) {
  if (!dataUrl) return "";
  let hash = 2166136261;
  const stride = Math.max(1, Math.floor(dataUrl.length / 4096));
  for (let index = 0; index < dataUrl.length; index += stride) {
    hash ^= dataUrl.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return `${dataUrl.length.toString(16)}:${(hash >>> 0).toString(16)}`;
}

async function writeDevCaptureImage(filePath: string, dataUrl: string | undefined) {
  if (!dataUrl) return null;
  await fs.writeFile(filePath, Buffer.from(dataUrlBase64(dataUrl), "base64"));
  return filePath;
}

async function writeDevCaptureSnapshot(capture: CaptureResult) {
  const outputDir = devCaptureOutputDir();
  await fs.mkdir(outputDir, { recursive: true });
  const stamp = new Date().toISOString().replace(/[\\/:]/g, "-").replace(/\..+?$/, "");
  const prefix = safeDebugFilePart(`${stamp}-${capture.name}`);
  const files = {
    full: await writeDevCaptureImage(path.join(outputDir, `${prefix}-full.png`), capture.dataUrl),
    detail: await writeDevCaptureImage(path.join(outputDir, `${prefix}-detail.png`), capture.detailDataUrl),
    inventory: await writeDevCaptureImage(path.join(outputDir, `${prefix}-inventory.png`), capture.inventoryDataUrl),
    crops: [] as Array<{ id: string; label: string; path: string; rect: { x: number; y: number; width: number; height: number } }>,
  };

  for (const crop of capture.crops ?? []) {
    const cropPath = path.join(outputDir, `${prefix}-${safeDebugFilePart(crop.id)}.png`);
    const written = await writeDevCaptureImage(cropPath, crop.dataUrl);
    if (written) files.crops.push({ id: crop.id, label: crop.label, path: written, rect: crop.rect });
  }

  const summary = {
    id: capture.id,
    name: capture.name,
    width: capture.width,
    height: capture.height,
    capturedAt: capture.capturedAt,
    captureTarget: capture.captureTarget,
    ocrSkipped: capture.ocrSkipped,
    ocrTimedOut: capture.ocrTimedOut,
    layout: capture.layout,
    inventoryGrid: capture.inventoryGrid
      ? {
          rows: capture.inventoryGrid.rows,
          cols: capture.inventoryGrid.cols,
          confidence: capture.inventoryGrid.confidence,
          source: capture.inventoryGrid.source,
          firstCenter: capture.inventoryGrid.centers[0] ?? null,
          lastCenter: capture.inventoryGrid.centers.at(-1) ?? null,
        }
      : null,
    inventoryCount: capture.inventoryCount ?? null,
    locked: capture.locked,
    lockSignal: capture.lockSignal,
    crops: (capture.crops ?? []).map((crop) => ({ id: crop.id, label: crop.label, rect: crop.rect })),
    ocr: capture.ocr ?? [],
    files,
  };
  const summaryPath = path.join(outputDir, `${prefix}-summary.json`);
  await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2), "utf8");
  return { ...summary, summaryPath };
}

function wait(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function findGenshinSource(sources: CaptureSourceInfo[], sourceId: string | null) {
  return sourceId
    ? sources.find((entry) => entry.id === sourceId)
    : sources.find((entry) => entry.isGenshinCandidate);
}

function sourceListForError(sources: CaptureSourceInfo[]) {
  return sources.map(({ id, name, isGenshinCandidate }) => ({ id, name, isGenshinCandidate }));
}

export function createDevControlServer(deps: DevControlServerDependencies): Server {
  const server = 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: deps.registeredHotkeys, hasWindow: deps.hasMainWindow(), appBuild: deps.appBuild });
      return;
    }
    if (url.pathname === "/dev/shutdown") {
      if (!deps.requestShutdown) {
        writeDevJson(res, 501, { ok: false, error: "shutdown not supported" });
        return;
      }
      const reason = url.searchParams.get("reason") || "dev-control shutdown requested";
      writeDevJson(res, 200, { ok: true, appBuild: deps.appBuild, reason });
      setTimeout(() => deps.requestShutdown?.(reason), 50);
      return;
    }
    if (url.pathname === "/scanner/start") {
      const limit = Number(url.searchParams.get("limit") ?? Number.NaN);
      const hasLimit = Number.isFinite(limit) && limit > 0;
      const category = url.searchParams.get("category") ?? undefined;
      deps.nativeScannerStart({ ...(hasLimit ? { limit } : {}), ...(category ? { category } : {}) })
        .then((scanner) => writeDevJson(res, 200, { ok: true, scanner }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/stop") {
      deps.nativeScannerStop()
        .then((scanner) => writeDevJson(res, 200, { ok: true, scanner }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/probe") {
      deps.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;
      }
      deps.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") {
      deps.nativeScannerStatus()
        .then((nativeScanner) => writeDevJson(res, 200, { ok: true, status: deps.scannerStatus(), nativeScanner }))
        .catch(() => writeDevJson(res, 200, { ok: true, status: deps.scannerStatus() }));
      return;
    }
    if (url.pathname === "/scanner/native/data") {
      deps.nativeScannerDataStatus()
        .then((status) => writeDevJson(res, status.valid ? 200 : 409, { ok: status.valid, status }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/native/catalog") {
      deps.nativeScannerCatalog()
        .then((catalog) => writeDevJson(res, catalog.data.valid ? 200 : 409, { ok: catalog.data.valid, catalog }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/native/preflight") {
      const category = url.searchParams.get("category") ?? undefined;
      deps.nativeScannerPreflight(category ? { category } : undefined)
        .then((status) => writeDevJson(res, status.ready ? 200 : 409, { ok: status.ready, status }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/native/process") {
      const runDir = url.searchParams.get("runDir") ?? undefined;
      const persist = url.searchParams.get("persist") === "1";
      const limit = Number(url.searchParams.get("limit") ?? Number.NaN);
      deps.nativeScannerProcessRun({
        runDir,
        persist,
        limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
      })
        .then((status) => writeDevJson(res, status.ok ? 200 : 409, { ok: status.ok, status }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/native/results") {
      const runDir = url.searchParams.get("runDir") ?? undefined;
      const limit = Number(url.searchParams.get("limit") ?? Number.NaN);
      deps.nativeScannerLoadResults({
        runDir,
        limit: Number.isFinite(limit) && limit > 0 ? limit : undefined,
      })
        .then((status) => writeDevJson(res, status.ok ? 200 : 409, { ok: status.ok, status }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/native/image") {
      const runDir = url.searchParams.get("runDir") ?? undefined;
      const imagePath = url.searchParams.get("imagePath");
      if (!imagePath) {
        writeDevJson(res, 400, { ok: false, error: "imagePath query param is required" });
        return;
      }
      deps.nativeScannerLoadImage({ runDir, imagePath })
        .then((status) => writeDevJson(res, status.ok ? 200 : 409, { ok: status.ok, status }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/ocr/warmup") {
      deps.warmOcr("current")
        .then((status) => writeDevJson(res, 200, { ok: true, status }))
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/lookup/status") {
      const status = validateLookupPackage();
      writeDevJson(res, status.valid ? 200 : 409, { ok: status.valid, status });
      return;
    }
    if (url.pathname === "/scanner/lookup/regenerate") {
      execFileAsync("node", ["scripts/generate-genshin-data.cjs"], { cwd: process.cwd(), windowsHide: true, timeout: 120000 })
        .then(({ stdout, stderr }) => {
          const status = validateLookupPackage();
          writeDevJson(res, status.valid ? 200 : 409, { ok: status.valid, status, stdout, stderr });
        })
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/scanner/benchmark-ocr") {
      const limit = Math.max(1, Math.min(100, Number(url.searchParams.get("limit") ?? 1) || 1));
      const sourceId = url.searchParams.get("sourceId");
      const profileParam = url.searchParams.get("profile");
      const ocrProfile: "full" | "fast" = profileParam === "full" ? "full" : "fast";
      deps.listCaptureSources()
        .then(async (sources) => {
          const source = findGenshinSource(sources, sourceId);
          if (!source) {
            writeDevJson(res, 404, { ok: false, error: "No Genshin capture source found.", sources: sourceListForError(sources) });
            return;
          }
          const benchmarkSource = source;

          async function runCurrentBenchmark() {
            const startedAt = Date.now();
            const captures: Array<{
              index: number;
              elapsedMs: number;
              ocrFields: number;
              timedOut: boolean;
              ocrSkipped: boolean;
              artifactDetailConfidence: number;
              sanctified: boolean;
              prepareMs: number;
              ocrMs: number;
              totalMs: number;
              ocrProfile?: "full" | "fast";
              ocrWorkerPoolSize?: number;
              ocrFieldMs?: Record<string, number>;
            }> = [];
            for (let index = 0; index < limit; index += 1) {
              const captureStartedAt = Date.now();
              const capture = await deps.captureSource(benchmarkSource.id, index === 0 ? 150 : 0, true, {
                ocrMode: "artifact",
                ocrProfile,
                ocrEngine: "current",
                omitFullFrame: true,
                omitInventoryPreview: true,
                skipOcrUnlessArtifactDetail: true,
              });
              captures.push({
                index,
                elapsedMs: Date.now() - captureStartedAt,
                ocrFields: capture.ocr?.length ?? 0,
                timedOut: Boolean(capture.ocrTimedOut),
                ocrSkipped: Boolean(capture.ocrSkipped),
                artifactDetailConfidence: capture.artifactDetail?.confidence ?? 0,
                sanctified: Boolean(capture.sanctified),
                prepareMs: capture.timings?.prepareMs ?? 0,
                ocrMs: capture.timings?.ocrMs ?? 0,
                totalMs: capture.timings?.totalMs ?? 0,
                ocrProfile: capture.timings?.ocrProfile,
                ocrWorkerPoolSize: capture.timings?.ocrWorkerPoolSize,
                ocrFieldMs: capture.timings?.ocrFieldMs,
              });
            }
            const elapsedMs = Date.now() - startedAt;
            const timings = captures.map((capture) => capture.elapsedMs).sort((left, right) => left - right);
            const ocrTimings = captures.map((capture) => capture.ocrMs).filter((value) => value > 0).sort((left, right) => left - right);
            const averageMs = Math.round(elapsedMs / limit);
            const averageOcrMs = ocrTimings.length > 0
              ? Math.round(ocrTimings.reduce((total, value) => total + value, 0) / ocrTimings.length)
              : 0;
            const percentile = (ratio: number) => timings[Math.min(timings.length - 1, Math.max(0, Math.ceil(timings.length * ratio) - 1))] ?? 0;
            const ocrPercentile = (ratio: number) => ocrTimings[Math.min(ocrTimings.length - 1, Math.max(0, Math.ceil(ocrTimings.length * ratio) - 1))] ?? 0;
            const ocrFieldTotals = captures.reduce<Record<string, { totalMs: number; count: number; maxMs: number }>>((fields, capture) => {
              for (const [field, elapsed] of Object.entries(capture.ocrFieldMs ?? {})) {
                const current = fields[field] ?? { totalMs: 0, count: 0, maxMs: 0 };
                current.totalMs += elapsed;
                current.count += 1;
                current.maxMs = Math.max(current.maxMs, elapsed);
                fields[field] = current;
              }
              return fields;
            }, {});
            const ocrFieldAverages = Object.fromEntries(
              Object.entries(ocrFieldTotals).map(([field, timing]) => [
                field,
                {
                  averageMs: Math.round(timing.totalMs / Math.max(1, timing.count)),
                  maxMs: timing.maxMs,
                  count: timing.count,
                },
              ]),
            );
            return {
              engine: "current",
              nativeTesseract: "not-enabled",
              workerPoolSize: captures.find((capture) => capture.ocrWorkerPoolSize)?.ocrWorkerPoolSize ?? null,
              ocrProfile,
              limit,
              elapsedMs,
              averageMs,
              averageOcrMs,
              minMs: timings[0] ?? 0,
              p50Ms: percentile(0.5),
              p90Ms: percentile(0.9),
              maxMs: timings[timings.length - 1] ?? 0,
              ocrP50Ms: ocrPercentile(0.5),
              ocrP90Ms: ocrPercentile(0.9),
              ocrFieldAverages,
              projectedMs: {
                artifacts20: averageMs * 20,
                artifacts45: averageMs * 45,
                artifacts100: averageMs * 100,
              },
              skippedOcrCaptures: captures.filter((capture) => capture.ocrSkipped).length,
              captures,
            };
          }
          const summary = await runCurrentBenchmark();
          writeDevJson(res, 200, {
            ok: true,
            summary,
          });
        })
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/review/samples") {
      deps.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;
    }
    if (url.pathname === "/capture/smart") {
      const sourceId = url.searchParams.get("sourceId");
      const focus = url.searchParams.get("focus") !== "0";
      const skipOcr = url.searchParams.get("skipOcr") === "1";
      deps.listCaptureSources()
        .then(async (sources) => {
          const source = findGenshinSource(sources, sourceId);
          if (!source) {
            writeDevJson(res, 404, { ok: false, error: "No Genshin capture source found.", sources: sourceListForError(sources) });
            return;
          }
          const capture = await deps.captureSource(source.id, 250, focus, { skipOcr });
          const summary = await writeDevCaptureSnapshot(capture);
          writeDevJson(res, 200, { ok: true, source: { id: source.id, name: source.name }, summary });
        })
        .catch((error: unknown) => writeDevJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }));
      return;
    }
    if (url.pathname === "/automation/probe-click") {
      const sourceId = url.searchParams.get("sourceId");
      const requestedIndex = Number(url.searchParams.get("index") ?? "1");
      const requestedRow = Number(url.searchParams.get("row") ?? Number.NaN);
      const requestedCol = Number(url.searchParams.get("col") ?? Number.NaN);
      deps.listCaptureSources()
        .then(async (sources) => {
          const source = findGenshinSource(sources, sourceId);
          if (!source) {
            writeDevJson(res, 404, { ok: false, error: "No Genshin capture source found.", sources: sourceListForError(sources) });
            return;
          }

          const before = await deps.captureSource(source.id, 150, true, { skipOcr: true });
          const grid = before.inventoryGrid;
          const detail = before.artifactDetail;
          const gridReady = before.captureTarget === "genshin-client"
            && grid?.source === "detected"
            && (grid.confidence ?? 0) >= 60;
          const detailReady = Boolean(detail?.present && detail.confidence >= 45);
          if (!gridReady || !detailReady) {
            writeDevJson(res, 409, {
              ok: false,
              error: "Artifact inventory detail view is not ready for probe-click.",
              captureTarget: before.captureTarget,
              grid: grid ? { rows: grid.rows, cols: grid.cols, source: grid.source, confidence: grid.confidence } : null,
              artifactDetail: detail ?? null,
            });
            return;
          }
          const centers = before.inventoryGrid?.centers ?? [];
          const target = Number.isFinite(requestedRow) && Number.isFinite(requestedCol)
            ? centers.find((center) => center.row === requestedRow && center.col === requestedCol)
            : centers[Math.max(0, Math.min(centers.length - 1, Number.isFinite(requestedIndex) ? requestedIndex : 1))];
          if (!target) {
            writeDevJson(res, 409, { ok: false, error: "No inventory grid target available.", grid: before.inventoryGrid ?? null });
            return;
          }

          const beforeFingerprint = dataUrlFingerprint(before.detailDataUrl);
          const click = await deps.clickScreen(target.x, target.y);
          await wait(650);
          const after = await deps.captureSource(source.id, 0, true, { skipOcr: true });
          const afterFingerprint = dataUrlFingerprint(after.detailDataUrl);
          const changed = Boolean(beforeFingerprint && afterFingerprint && beforeFingerprint !== afterFingerprint);
          writeDevJson(res, 200, {
            ok: Boolean(click.ok && click.clicked && changed),
            changed,
            target,
            click,
            before: {
              captureTarget: before.captureTarget,
              grid: before.inventoryGrid ? { rows: before.inventoryGrid.rows, cols: before.inventoryGrid.cols, source: before.inventoryGrid.source, confidence: before.inventoryGrid.confidence } : null,
              detailFingerprint: beforeFingerprint,
            },
            after: {
              captureTarget: after.captureTarget,
              grid: after.inventoryGrid ? { rows: after.inventoryGrid.rows, cols: after.inventoryGrid.cols, source: after.inventoryGrid.source, confidence: after.inventoryGrid.confidence } : null,
              detailFingerprint: afterFingerprint,
            },
          });
        })
        .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" });
  });

  server.listen(17317, "127.0.0.1");
  return server;
}
