#!/usr/bin/env node

/**
 * Screenshot all templates by starting each dev server, capturing screenshots, and stopping.
 *
 * Usage:
 *   node scripts/screenshot-all-templates.mjs [template...]
 *   node scripts/screenshot-all-templates.mjs           # all templates
 *   node scripts/screenshot-all-templates.mjs blog      # just blog
 *   node scripts/screenshot-all-templates.mjs blog marketing  # blog and marketing
 */

import { spawn, execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");

const TEMPLATES = {
	blog: { dir: "templates/blog", port: 4321 },
	marketing: { dir: "templates/marketing", port: 4322 },
	portfolio: { dir: "templates/portfolio", port: 4323 },
};

function loadConfig() {
	const configPath = join(ROOT, "templates", "screenshots.json");
	return JSON.parse(readFileSync(configPath, "utf-8"));
}

/** Check if server is responding via HTTP */
async function isServerReady(port) {
	try {
		const controller = new AbortController();
		const timeout = setTimeout(() => controller.abort(), 2000);
		const response = await fetch(`http://localhost:${port}/`, {
			signal: controller.signal,
		});
		clearTimeout(timeout);
		return response.ok || response.status === 404; // 404 is fine, server is up
	} catch {
		return false;
	}
}

/** Wait for server to respond, with timeout */
async function waitForServer(port, timeoutMs = 60000) {
	const start = Date.now();
	while (Date.now() - start < timeoutMs) {
		if (await isServerReady(port)) {
			return true;
		}
		await new Promise((r) => setTimeout(r, 500));
	}
	return false;
}

/** Check if port has processes (via lsof) */
function hasProcessOnPort(port) {
	try {
		const result = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf-8" });
		return result.trim().length > 0;
	} catch {
		return false;
	}
}

/** Wait for port to be free */
async function waitForPortFree(port, timeoutMs = 10000) {
	const start = Date.now();
	while (Date.now() - start < timeoutMs) {
		if (!hasProcessOnPort(port)) {
			return true;
		}
		await new Promise((r) => setTimeout(r, 200));
	}
	return false;
}

/** Kill all processes listening on a port (macOS/Linux) */
function killProcessesOnPort(port) {
	try {
		// Get PIDs listening on this port
		const result = execSync(`lsof -ti tcp:${port} 2>/dev/null || true`, { encoding: "utf-8" });
		const pids = result
			.trim()
			.split("\n")
			.filter((p) => p);

		for (const pid of pids) {
			try {
				process.kill(Number(pid), "SIGTERM");
			} catch {
				// Process may already be dead
			}
		}

		// If still running after 2s, force kill
		if (pids.length > 0) {
			setTimeout(() => {
				for (const pid of pids) {
					try {
						process.kill(Number(pid), "SIGKILL");
					} catch {
						// Process may already be dead
					}
				}
			}, 2000);
		}
	} catch {
		// lsof may not be available or no processes found
	}
}

function startDevServer(templateDir, port) {
	return new Promise((resolve, reject) => {
		// Run astro dev directly in the template directory
		const proc = spawn("pnpm", ["exec", "astro", "dev", "--port", String(port)], {
			cwd: join(ROOT, templateDir),
			stdio: ["ignore", "pipe", "pipe"],
			detached: false,
		});

		let output = "";

		const onData = (data) => {
			output += data.toString();
			process.stdout.write(data); // Show output for debugging
		};

		proc.stdout.on("data", onData);
		proc.stderr.on("data", onData);

		proc.on("error", (err) => {
			reject(err);
		});

		proc.on("exit", (code) => {
			// If process exits before we resolve, that's an error
			reject(new Error(`Dev server exited with code ${code}`));
		});

		// Wait for server to respond via HTTP
		waitForServer(port, 60000)
			.then((ready) => {
				if (ready) {
					// Remove exit handler since we're resolving successfully
					proc.removeAllListeners("exit");
					// Re-add a silent exit handler
					proc.on("exit", () => {});
					resolve({ proc, port });
				} else {
					proc.kill();
					reject(new Error(`Timeout waiting for server on port ${port}`));
				}
				return undefined;
			})
			.catch(reject);
	});
}

function runScreenshots(template, url) {
	return new Promise((resolve, reject) => {
		const proc = spawn("node", [join(ROOT, "scripts", "screenshot-templates.mjs"), template, url], {
			cwd: ROOT,
			stdio: "inherit",
		});

		proc.on("error", reject);
		proc.on("exit", (code) => {
			if (code === 0) {
				resolve();
			} else {
				reject(new Error(`Screenshot script exited with code ${code}`));
			}
		});
	});
}

async function stopDevServer({ proc, port }) {
	// Kill the process tree
	try {
		proc.kill("SIGTERM");
	} catch {
		// May already be dead
	}

	// Also kill anything on the port (catches child processes)
	killProcessesOnPort(port);

	// Wait for port to actually be free
	const closed = await waitForPortFree(port, 10000);
	if (!closed) {
		console.warn(`Warning: Port ${port} still in use after stopping server`);
		// Force kill anything still on the port
		killProcessesOnPort(port);
		await waitForPortFree(port, 5000);
	}
}

/** Run bootstrap (reset db and seed) for a template */
async function bootstrapTemplate(templateDir) {
	return new Promise((resolve, reject) => {
		console.log(`Bootstrapping ${templateDir}...`);
		const proc = spawn("pnpm", ["bootstrap"], {
			cwd: join(ROOT, templateDir),
			stdio: "inherit",
		});

		proc.on("error", reject);
		proc.on("exit", (code) => {
			if (code === 0) {
				resolve();
			} else {
				reject(new Error(`Bootstrap exited with code ${code}`));
			}
		});
	});
}

async function processTemplate(template) {
	const config = TEMPLATES[template];
	if (!config) {
		console.error(`Unknown template: ${template}`);
		console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
		return false;
	}

	const screenshotsConfig = loadConfig();
	if (!screenshotsConfig[template]) {
		console.error(`No screenshot config for template: ${template}`);
		return false;
	}

	// Make sure port is free before starting
	if (hasProcessOnPort(config.port)) {
		console.log(`Port ${config.port} is in use, killing existing processes...`);
		killProcessesOnPort(config.port);
		await waitForPortFree(config.port, 5000);
	}

	console.log(`\n${"=".repeat(60)}`);
	console.log(`${template} (${config.dir})`);
	console.log("=".repeat(60));

	let server;
	try {
		// Bootstrap first to ensure database has seed content
		await bootstrapTemplate(config.dir);

		console.log(`Starting dev server...`);
		server = await startDevServer(config.dir, config.port);
		console.log(`Dev server ready at http://localhost:${config.port}\n`);

		await runScreenshots(template, `http://localhost:${config.port}`);
		return true;
	} catch (err) {
		console.error(`Failed to process ${template}:`, err.message);
		return false;
	} finally {
		if (server) {
			console.log(`Stopping ${template} dev server...`);
			await stopDevServer(server);
			// Extra pause to ensure cleanup
			await new Promise((r) => setTimeout(r, 1000));
		}
	}
}

async function run() {
	const args = process.argv.slice(2);
	const templates = args.length > 0 ? args : Object.keys(TEMPLATES);

	// Validate all templates first
	for (const template of templates) {
		if (!TEMPLATES[template]) {
			console.error(`Unknown template: ${template}`);
			console.error(`Available: ${Object.keys(TEMPLATES).join(", ")}`);
			process.exit(1);
		}
	}

	console.log(`\nScreenshotting templates: ${templates.join(", ")}`);

	const results = { success: [], failed: [] };

	for (const template of templates) {
		const success = await processTemplate(template);
		if (success) {
			results.success.push(template);
		} else {
			results.failed.push(template);
		}
	}

	console.log(`\n${"=".repeat(60)}`);
	console.log("Summary");
	console.log("=".repeat(60));

	if (results.success.length > 0) {
		console.log(`Succeeded: ${results.success.join(", ")}`);
	}
	if (results.failed.length > 0) {
		console.log(`Failed: ${results.failed.join(", ")}`);
		process.exit(1);
	}
}

run().catch((err) => {
	console.error(err);
	process.exit(1);
});
