#!/usr/bin/env node

import { spawn, spawnSync } from "node:child_process";
import { existsSync, readdirSync, statSync } from "node:fs";
import { join, resolve } from "node:path";

const __dirname = import.meta.dirname;
const root = resolve(__dirname, "..");
const sourceBinPath = resolve(root, "bin", "sf-from-source");
const ensureResourcesPath = resolve(
	root,
	"scripts",
	"ensure-source-resources.cjs",
);
const daemonCliPath = resolve(root, "packages", "daemon", "src", "cli-dev.ts");
const resolveTsPath = resolve(
	root,
	"src",
	"resources",
	"extensions",
	"sf",
	"tests",
	"resolve-ts.mjs",
);
const WATCH_INTERVAL_MS = Number(
	process.env.SF_DEV_SERVER_WATCH_INTERVAL_MS ?? 2_000,
);
const RESTART_GRACE_MS = Number(
	process.env.SF_DEV_SERVER_RESTART_GRACE_MS ?? 5_000,
);

const passthroughArgs = process.argv.slice(2);
const oneShot = passthroughArgs.some((arg) =>
	["--help", "-h", "--status", "--install", "--uninstall"].includes(arg),
);
const watchEnabled =
	process.env.SF_DEV_SERVER_WATCH !== "0" && !oneShot && WATCH_INTERVAL_MS > 0;

const watchedRoots = [
	resolve(root, "packages", "daemon", "src"),
	resolve(root, "packages", "daemon", "package.json"),
	resolve(root, "scripts", "dev-server.js"),
	resolve(root, "scripts", "copy-resources.cjs"),
	resolve(root, "scripts", "ensure-source-resources.cjs"),
	resolve(root, "package.json"),
];

function newestMtimeMs(path) {
	let latest = 0;
	const stack = [path];
	const skip = new Set(["dist", "node_modules", ".git", ".sf"]);

	while (stack.length > 0) {
		const current = stack.pop();
		if (!current || !existsSync(current)) continue;

		let stat;
		try {
			stat = statSync(current);
		} catch {
			continue;
		}
		latest = Math.max(latest, stat.mtimeMs);

		if (!stat.isDirectory()) continue;

		let entries;
		try {
			entries = readdirSync(current, { withFileTypes: true });
		} catch {
			continue;
		}

		for (const entry of entries) {
			if (skip.has(entry.name)) continue;
			stack.push(join(current, entry.name));
		}
	}

	return latest;
}

function sourceEpoch() {
	return Math.max(...watchedRoots.map(newestMtimeMs));
}

const resourceBuild = spawnSync(process.execPath, [ensureResourcesPath], {
	cwd: root,
	stdio: "inherit",
	env: process.env,
});

if (resourceBuild.status !== 0) {
	process.exit(resourceBuild.status ?? 1);
}

let child;
let stopping = false;
let restarting = false;
let currentEpoch = sourceEpoch();
let restartTimer;

function childEnv() {
	return {
		...process.env,
		SF_SOURCE_ROOT: process.env.SF_SOURCE_ROOT || root,
		SF_RUNTIME_SOURCE_ROOT: process.env.SF_RUNTIME_SOURCE_ROOT || root,
		SF_BIN_PATH: process.env.SF_BIN_PATH || resolve(root, "dist", "loader.js"),
		SF_CLI_PATH: process.env.SF_CLI_PATH || sourceBinPath,
	};
}

function spawnDaemon() {
	child = spawn(
		process.execPath,
		[
			"--import",
			resolveTsPath,
			"--experimental-strip-types",
			"--no-warnings",
			daemonCliPath,
			...passthroughArgs,
		],
		{
			cwd: process.cwd(),
			stdio: "inherit",
			env: childEnv(),
		},
	);

	child.on("error", (error) => {
		console.error(
			`[forge] Failed to launch local dev server: ${error instanceof Error ? error.message : String(error)}`,
		);
		if (!watchEnabled) process.exit(1);
	});

	child.on("exit", (code, signal) => {
		child = undefined;
		if (stopping) {
			if (signal) {
				process.kill(process.pid, signal);
				return;
			}
			process.exit(code ?? 0);
		}
		if (restarting) {
			restarting = false;
			spawnDaemon();
			return;
		}
		if (!watchEnabled) {
			if (signal) {
				process.kill(process.pid, signal);
				return;
			}
			process.exit(code ?? 0);
		}

		console.error(
			`[forge] sf-server exited (${signal ?? `code ${code ?? 0}`}); restarting in 2s...`,
		);
		setTimeout(spawnDaemon, 2_000);
	});
}

function requestRestart(reason) {
	if (stopping || restarting) return;
	restarting = true;
	console.error(`[forge] ${reason}; restarting sf-server dev child...`);

	if (!child || child.killed) {
		restarting = false;
		spawnDaemon();
		return;
	}

	const victim = child;
	victim.kill("SIGTERM");
	restartTimer = setTimeout(() => {
		if (victim.exitCode == null && victim.signalCode == null) {
			victim.kill("SIGKILL");
		}
	}, RESTART_GRACE_MS);
}

function stop(signal) {
	stopping = true;
	if (restartTimer) clearTimeout(restartTimer);
	if (!child || child.killed) {
		process.exit(0);
		return;
	}
	child.kill(signal);
}

process.on("SIGINT", () => stop("SIGINT"));
process.on("SIGTERM", () => stop("SIGTERM"));

spawnDaemon();

if (watchEnabled) {
	setInterval(() => {
		const nextEpoch = sourceEpoch();
		if (nextEpoch <= currentEpoch) return;
		currentEpoch = nextEpoch;
		requestRestart("daemon/dev source changed");
	}, WATCH_INTERVAL_MS);
}
