import { createHash, randomUUID } from "node:crypto";
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { FIXTURES } from "./ats-fixtures";
import { scoreExtractedText } from "./ats-fidelity/metrics";
import { ANALYTICS_TEMPLATES } from "../src/lib/analytics-taxonomy";
import { CLIENT_TIMEOUT_MS, MAX_RESULT_BYTES, renderResultSchema } from "../src/renderer/protocol";

/** Collect synthetic evidence from an isolated release candidate, never customer PDFs. */
async function main() {
  const endpoint = process.env.RENDERER_URL;
  const secret = process.env.RENDERER_SECRET;
  const imageId = process.env.CANDIDATE_IMAGE_ID;
  if (
    !endpoint ||
    !secret ||
    !imageId ||
    !/^sha256:[a-f0-9]{64}$/.test(imageId) ||
    process.env.NEXT_PUBLIC_APP_URL !== "https://smoke.invalid"
  ) {
    throw new Error(
      "Use an isolated CI renderer with RENDERER_URL, RENDERER_SECRET, CANDIDATE_IMAGE_ID and NEXT_PUBLIC_APP_URL=https://smoke.invalid.",
    );
  }
  const origin = new URL(endpoint);
  if (
    !["http:", "https:"].includes(origin.protocol) ||
    origin.username ||
    origin.password ||
    origin.pathname !== "/" ||
    origin.search ||
    origin.hash
  )
    throw new Error("Renderer URL must be an HTTP(S) origin.");
  const headers = { Authorization: `Bearer ${secret}`, "Content-Type": "application/json" };
  const health = await fetch(new URL("/health", origin), {
    headers,
    redirect: "error",
    signal: AbortSignal.timeout(3_000),
  });
  if (!health.ok || (await health.json()).environment !== "ci") {
    throw new Error("Research capture requires a ready renderer marked as CI.");
  }
  const output = resolve(process.argv[2] ?? `.render-bench/pdf-research-${Date.now()}`);
  await mkdir(dirname(output), { recursive: true });
  await mkdir(output); // Refuse to overwrite a previous evidence collection.
  const requirePdf = createRequire(import.meta.url);
  const assets = dirname(requirePdf.resolve("pdfjs-dist/package.json"));
  const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
  const hash = (data: string | Uint8Array) => createHash("sha256").update(data).digest("hex");
  const files: Record<string, string> = {};
  const cases: Array<{
    fixture: string;
    template: string;
    pages: number;
    extractedCharacters: number;
    checks: { id: string; score: number; detail: string }[];
  }> = [];
  async function save(name: string, data: string | Uint8Array) {
    await writeFile(join(output, name), data, { flag: "wx" });
    files[name] = hash(data);
  }
  await save("fixtures-source.ts.txt", await readFile("scripts/ats-fixtures.ts"));
  await save("capture-source.ts.txt", await readFile("scripts/capture-pdf-research.ts"));
  await save("metrics-source.ts.txt", await readFile("scripts/ats-fidelity/metrics.ts"));
  await save("package-lock.json", await readFile("package-lock.json"));
  for (const [fixture, data] of Object.entries(FIXTURES)) {
    for (const template of ANALYTICS_TEMPLATES) {
      const name = `${fixture}-${template}`;
      const snapshot = JSON.stringify({ ...data, templateId: template });
      const response = await fetch(new URL("/render", origin), {
        method: "POST",
        headers,
        redirect: "error",
        signal: AbortSignal.timeout(CLIENT_TIMEOUT_MS),
        body: JSON.stringify({
          actorId: `research-${randomUUID()}`,
          kind: "final",
          snapshot,
          preview: true,
          watermarked: false,
        }),
      });
      if (!response.ok) {
        await response.body?.cancel();
        throw new Error(`${name}: renderer returned HTTP ${response.status}`);
      }
      const reader = response.body?.getReader();
      if (!reader) throw new Error(`${name}: renderer returned no body`);
      const chunks: Uint8Array[] = [];
      let bytes = 0;
      try {
        for (;;) {
          const { done, value } = await reader.read();
          if (done) break;
          bytes += value.byteLength;
          if (bytes > MAX_RESULT_BYTES) throw new Error(`${name}: response exceeds artifact limit`);
          chunks.push(value);
        }
      } finally {
        await reader.cancel().catch(() => {});
      }
      const result = renderResultSchema.parse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
      if (result.kind !== "final" || result.pages.length !== result.pageCount) {
        throw new Error(`${name}: final PDF and page images are required`);
      }
      const pdf = Buffer.from(result.pdf, "base64");
      await save(`${name}.pdf`, pdf);
      await save(`${name}.snapshot.json`, snapshot);
      const task = pdfjs.getDocument({
        data: new Uint8Array(pdf),
        enableXfa: false,
        cMapUrl: `${join(assets, "cmaps")}/`,
        cMapPacked: true,
        standardFontDataUrl: `${join(assets, "standard_fonts")}/`,
        wasmUrl: `${join(assets, "wasm")}/`,
        useSystemFonts: false,
        stopAtErrors: true,
      });
      const deadline = setTimeout(() => {
        void task.destroy().catch(() => {});
      }, 30_000);
      const text: string[] = [];
      try {
        const document = await task.promise;
        if (document.numPages !== result.pageCount) throw new Error(`${name}: page count mismatch`);
        for (let number = 1; number <= document.numPages; number++) {
          const page = await document.getPage(number);
          const content = await page.getTextContent();
          text.push(
            content.items
              .map((item) => ("str" in item ? `${item.str}${item.hasEOL ? "\n" : " "}` : ""))
              .join(""),
          );
          page.cleanup();
        }
      } finally {
        clearTimeout(deadline);
        await task.destroy();
      }
      await save(`${name}.txt`, text.join("\n\f\n"));
      for (const [index, page] of result.pages.entries()) {
        await save(`${name}.page-${index + 1}.png`, Buffer.from(page.png, "base64"));
      }
      cases.push({
        fixture,
        template,
        pages: result.pageCount,
        extractedCharacters: text.join("").length,
        checks: scoreExtractedText(text.join("\n\f\n"), fixture, data).map(
          ({ id, score, detail }) => ({ id, score, detail }),
        ),
      });
      console.log(`Collected ${name}`);
    }
  }
  await save(
    "report.json",
    JSON.stringify(
      {
        capturedAt: new Date().toISOString(),
        candidateImageId: imageId,
        sourceRevision: process.env.APP_VERSION,
        provenance:
          "Image ID supplied by operator; retain docker inspect evidence with this collection.",
        extractor: `PDF.js ${pdfjs.version}`,
        node: process.version,
        cases,
        scope:
          "Synthetic candidate-renderer artifacts. Selected extraction checks are local string measurements, not ATS acceptance rates. Source revision identifies the base checkout; the candidate includes working changes. No ATS vendor, hiring outcome, independent visual review or production deployment is established. PDF.js and Node versions below describe the collector; retain separate renderer runtime evidence.",
      },
      null,
      2,
    ),
  );
  await save(
    "review.md",
    "# Candidate PDF evidence\n\nPending human review. Compare snapshots with each PDF, extracted text and page image. Record omissions, reading order, clipping and glyph problems per case. Retain the app and renderer image digests and runtime versions. Publish only after provenance and findings have been checked; do not replace the historical July dataset.\n",
  );
  await writeFile(
    join(output, "checksums.sha256"),
    Object.entries(files)
      .map(([name, digest]) => `${digest}  ${name}`)
      .join("\n") + "\n",
    { flag: "wx" },
  );
  console.log(`Evidence saved to ${output}; human review and publication remain separate.`);
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : "Research capture failed");
  process.exitCode = 1;
});
