// Capture marketing/console screenshots from the built Storybook.
//
// Mirrors the iOS harness (clients/apple/tools/screenshots.sh): one "scene" per
// story, a mock-populated REAL view, captured by the platform's own renderer —
// here headless Chromium over `storybook-static`. No display, GPU, login, or live
// mgmt backend: the page stories render entirely from fixtures (src/stories/lib).
//
//   bun run build-storybook            # produce ./storybook-static
//   node tools/screenshots.mjs         # → ./screenshots/<story-id>.png
//
// Env knobs: OUT (output dir), STORYBOOK_STATIC (input dir), SETTLE (ms after the
// page looks ready, default 600), WIDTH/HEIGHT/SCALE (viewport, default 1440x900@2x),
// ONLY (comma-separated story-id substring filter).

import { existsSync } from "node:fs";
import { mkdir, readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { extname, join, normalize, resolve } from "node:path";
import { chromium } from "playwright";

const ROOT = resolve(process.env.STORYBOOK_STATIC ?? "storybook-static");
const OUT = resolve(process.env.OUT ?? "screenshots");
const SETTLE = Number(process.env.SETTLE ?? 600);
const WIDTH = Number(process.env.WIDTH ?? 1440);
const HEIGHT = Number(process.env.HEIGHT ?? 900);
const SCALE = Number(process.env.SCALE ?? 2);
const ONLY = (process.env.ONLY ?? "")
	.split(",")
	.map((s) => s.trim())
	.filter(Boolean);

// Only the page-level + shell stories make sense as console screenshots — skip the
// component-library stories (Button, Badge, …).
const TITLE_PREFIXES = ["Pages/", "Shell/"];

const MIME = {
	".html": "text/html",
	".js": "text/javascript",
	".mjs": "text/javascript",
	".css": "text/css",
	".json": "application/json",
	".svg": "image/svg+xml",
	".png": "image/png",
	".jpg": "image/jpeg",
	".woff": "font/woff",
	".woff2": "font/woff2",
	".ttf": "font/ttf",
	".map": "application/json",
	".ico": "image/x-icon",
};

function staticServer(rootDir) {
	return createServer(async (req, res) => {
		try {
			const url = new URL(req.url, "http://localhost");
			let path = decodeURIComponent(url.pathname);
			if (path.endsWith("/")) path += "index.html";
			// Contain the path to rootDir (no traversal).
			const filePath = normalize(join(rootDir, path));
			if (!filePath.startsWith(rootDir)) {
				res.writeHead(403).end();
				return;
			}
			const body = await readFile(filePath);
			res.writeHead(200, {
				"content-type": MIME[extname(filePath)] ?? "application/octet-stream",
			});
			res.end(body);
		} catch {
			res.writeHead(404).end();
		}
	});
}

async function listStories(rootDir) {
	const indexPath = join(rootDir, "index.json");
	if (!existsSync(indexPath)) {
		throw new Error(
			`${indexPath} not found — run \`bun run build-storybook\` first`,
		);
	}
	const index = JSON.parse(await readFile(indexPath, "utf8"));
	const entries = Object.values(index.entries ?? index.stories ?? {});
	return entries
		.filter((e) => e.type === "story" || e.type === undefined)
		.filter((e) => TITLE_PREFIXES.some((p) => (e.title ?? "").startsWith(p)))
		.filter((e) => ONLY.length === 0 || ONLY.some((f) => e.id.includes(f)))
		.sort((a, b) => a.id.localeCompare(b.id));
}

async function main() {
	if (!existsSync(ROOT)) {
		throw new Error(
			`${ROOT} not found — run \`bun run build-storybook\` first`,
		);
	}
	const stories = await listStories(ROOT);
	if (stories.length === 0)
		throw new Error("no Pages/* or Shell/* stories found");
	await mkdir(OUT, { recursive: true });

	const server = staticServer(ROOT);
	await new Promise((r) => server.listen(0, "127.0.0.1", r));
	const port = server.address().port;

	const browser = await chromium.launch({
		args: ["--force-color-profile=srgb"],
	});
	const context = await browser.newContext({
		viewport: { width: WIDTH, height: HEIGHT },
		deviceScaleFactor: SCALE,
		colorScheme: "dark",
	});

	let ok = 0;
	for (const story of stories) {
		const page = await context.newPage();
		const url = `http://127.0.0.1:${port}/iframe.html?id=${encodeURIComponent(
			story.id,
		)}&viewMode=story`;
		try {
			await page.goto(url, { waitUntil: "networkidle", timeout: 30_000 });
			// Story root mounted with real content.
			await page.waitForSelector("#storybook-root > *", { timeout: 20_000 });
			// Web fonts settled (else text reflows / falls back in the shot).
			await page.evaluate(() => document.fonts.ready);
			// Recharts mounts behind a client-only guard — wait for the SVG if present.
			await page
				.locator(".recharts-surface")
				.first()
				.waitFor({ timeout: 4_000 })
				.catch(() => {});
			await page.waitForTimeout(SETTLE);
			const file = join(OUT, `${story.id}.png`);
			await page.screenshot({ path: file });
			console.log(`✓ ${story.id} → ${file}`);
			ok++;
		} catch (e) {
			console.warn(`✗ ${story.id}: ${e.message}`);
		} finally {
			await page.close();
		}
	}

	await browser.close();
	await new Promise((r) => server.close(r));
	console.log(`\n${ok}/${stories.length} stories captured → ${OUT}`);
	if (ok === 0) process.exit(1);
}

main().catch((e) => {
	console.error(e);
	process.exit(1);
});
