/**
 * The before/after pair, at every authored chapter of all three packs.
 *
 * `TERA_RECONCILE` exists so that four data changes can be *looked at* rather
 * than argued about, and the acceptance for every one of them is a photograph:
 * a board that is worse in the picture means that rule reverts. Twenty-six
 * chapters times two flag states is fifty-two frames, which is more than anyone
 * will take one `look.mjs` invocation at a time — so this is one command, one
 * preview and one browser, and it names its output after the chapter's identity
 * rather than after its position in a list.
 *
 *   node scripts/reconcile-shots.mjs [--dist <dir>] [--out <dir>]
 *                                    [--rules <a,b>] [--only <board>]
 *                                    [--at <iso>] [--wait <ms>]
 *
 * `--rules` is passed through as `?reconcile=<rules>`, so a single rule can be
 * photographed alone — which is the point of the four being independently
 * switchable in the first place.
 *
 * The port and build checks are `look.mjs`'s, for `look.mjs`'s reason: this box
 * once accumulated a hundred and forty-seven abandoned previews in an afternoon
 * and photographed somebody else's dist through one of them.
 */

import { spawn } from "node:child_process";
import { mkdirSync } from "node:fs";
import { createServer } from "node:net";
import { chromium } from "playwright";
import CALIFORNIA from "../src/cities/california.ts";
import SF from "../src/cities/sf.ts";
import SOCAL from "../src/cities/socal.ts";

const args = process.argv.slice(2);
const flag = (name, fallback) => {
  const i = args.indexOf(name);
  return i === -1 ? fallback : args[i + 1];
};

const DIST = flag("--dist", "dist");
const OUT = flag("--out", "/tmp/tera-look/reconcile");
const RULES = flag("--rules", "1");
const ONLY = flag("--only", null);
const AT = flag("--at", "2026-08-21T20:00:00Z");
const WAIT = Number(flag("--wait", "6000"));
const FIRST_WAIT = Number(flag("--first-wait", "11000"));
mkdirSync(OUT, { recursive: true });

/**
 * The two California chapters that are doors, not poses.
 *
 * `main.ts` matches them against `CALIFORNIA_DESTINATIONS` and calls
 * `switchCity()` rather than `flyTo()`, so clicking one leaves the board — which
 * is what made `look.mjs`'s `glyph-la` and `glyph-sf` presets photographs of the
 * wrong board for as long as they existed. They are shot anyway, because a
 * before/after pair at *every* authored chapter is what was asked for and
 * because the frame they produce is a real frame of the product; the page is
 * reloaded afterwards so the next chapter is aimed from California again.
 */
const DOORS = new Set(["los-angeles", "san-francisco"]);

const BOARDS = [CALIFORNIA, SF, SOCAL].filter((city) => ONLY === null || city.id === ONLY);

const PORT = await new Promise((resolve, reject) => {
  const probe = createServer();
  probe.once("error", reject);
  probe.listen(0, "127.0.0.1", () => {
    const { port } = probe.address();
    probe.close(() => resolve(port));
  });
});

const server = spawn(
  new URL("../node_modules/.bin/vite", import.meta.url).pathname,
  ["preview", "--outDir", DIST, "--port", String(PORT), "--strictPort"],
  { detached: true, stdio: ["ignore", "pipe", "pipe"] },
);
const shutdown = () => {
  try {
    process.kill(-server.pid, "SIGTERM");
  } catch {
    /* already gone */
  }
};
process.on("exit", shutdown);
const bound = await new Promise((resolve) => {
  let seen = "";
  const settle = setTimeout(() => resolve(null), 30000);
  const read = (chunk) => {
    seen += String(chunk);
    const match = /http:\/\/(?:localhost|127\.0\.0\.1):(\d+)/.exec(seen);
    if (match) {
      clearTimeout(settle);
      resolve(Number(match[1]));
    }
  };
  server.stdout.on("data", read);
  server.stderr.on("data", read);
});
if (bound !== PORT) {
  console.error(`reconcile-shots: preview bound ${bound}, not ${PORT} — refusing to photograph it`);
  shutdown();
  process.exit(1);
}

const browser = await chromium.launch({
  channel: "chrome",
  args: ["--use-gl=angle", "--use-angle=vulkan", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist"],
});
const context = await browser.newContext({
  viewport: { width: 1600, height: 1000 },
  deviceScaleFactor: 1,
  timezoneId: "America/Los_Angeles",
  // The opening arrival collapses to a cut under this, which is the only way
  // fifty-two frames taken over half an hour are the same fifty-two frames when
  // the box is busy.
  reducedMotion: "reduce",
});
await context.clock.setFixedTime(new Date(AT));
const page = await context.newPage();
const errors = [];
page.on("console", (m) => {
  if (m.type() === "error") errors.push(m.text());
});

async function land(url) {
  await page.goto(`http://localhost:${PORT}${url}`, { waitUntil: "networkidle", timeout: 60000 });
  await page.waitForTimeout(FIRST_WAIT);
  try {
    await page.getByText(/^Skip$/).first().click({ timeout: 2000 });
    await page.waitForTimeout(1200);
  } catch {
    /* already dismissed, or not shown */
  }
}

let taken = 0;
for (const city of BOARDS) {
  for (const state of ["off", "on"]) {
    const url = `/?city=${city.id}${state === "on" ? `&reconcile=${RULES}` : ""}`;
    await land(url);
    for (const chapter of city.chapters) {
      // Aim by identity. `data-view` is the chapter's own id, so a pack that is
      // reordered under this script still photographs the chapter it names —
      // the failure `look.mjs`'s two broken presets were an instance of.
      const button = page.locator(`.chapter[data-view="${chapter.id}"]`).first();
      try {
        await button.click({ timeout: 5000 });
      } catch {
        console.log(`reconcile-shots: could not click ${city.id}/${chapter.id}`);
        await land(url);
        continue;
      }
      await page.waitForTimeout(WAIT);
      const path = `${OUT}/${city.id}-${chapter.id}-${state}.png`;
      await page.screenshot({ path });
      taken += 1;
      console.log(`reconcile-shots: ${path}`);
      if (DOORS.has(chapter.id)) await land(url);
    }
  }
}

const real = errors.filter((e) => !/404|Failed to load resource/.test(e));
console.log(`reconcile-shots: ${taken} frames into ${OUT}`);
console.log(real.length === 0 ? "reconcile-shots: no console errors" : `reconcile-shots: ERRORS ${JSON.stringify([...new Set(real)].slice(0, 5))}`);

await browser.close();
shutdown();
process.exit(0);
