import {
	type ChildProcess,
	execFile,
	execSync,
	type SpawnOptions,
	spawn,
} from "node:child_process";
import { randomBytes } from "node:crypto";
import {
	existsSync,
	readFileSync,
	readlinkSync,
	unlinkSync,
	writeFileSync,
} from "node:fs";
import { request as httpRequest } from "node:http";
import { createServer } from "node:net";
import { join, resolve } from "node:path";
import {
	appRoot,
	webPidFilePath as defaultWebPidFilePath,
} from "./app-paths.js";

const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PACKAGE_ROOT = resolve(import.meta.dirname, "..");

/** Open a URL in the user's default browser. */
function openBrowser(url: string): void {
	if (process.platform === "win32") {
		// PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not.
		execFile(
			"powershell",
			["-c", `Start-Process '${url.replace(/'/g, "''")}'`],
			{ windowsHide: true },
			() => {},
		);
	} else {
		const cmd = process.platform === "darwin" ? "open" : "xdg-open";
		execFile(cmd, [url], () => {});
	}
}

type WritableLike = Pick<typeof process.stderr, "write">;

type ResourceBootstrapLike = {
	initResources: (agentDir: string) => void;
};

type SpawnedChildLike = Pick<ChildProcess, "once" | "unref" | "pid">;

export interface WebModeLaunchOptions {
	cwd: string;
	projectSessionsDir: string;
	agentDir: string;
	packageRoot?: string;
	host?: string;
	port?: number;
	/** Additional allowed origins for CORS (forwarded as SF_WEB_ALLOWED_ORIGINS). */
	allowedOrigins?: string[];
}

export interface ResolvedWebHostBootstrap {
	ok: true;
	kind: "packaged-standalone" | "source-dev";
	packageRoot: string;
	hostRoot: string;
	entryPath: string;
}

export interface UnresolvedWebHostBootstrap {
	ok: false;
	packageRoot: string;
	reason: string;
	candidates: string[];
}

export type WebHostBootstrap =
	| ResolvedWebHostBootstrap
	| UnresolvedWebHostBootstrap;

export interface WebModeLaunchSuccess {
	mode: "web";
	ok: true;
	cwd: string;
	projectSessionsDir: string;
	host: string;
	port: number;
	url: string;
	hostKind: ResolvedWebHostBootstrap["kind"];
	hostPath: string;
	hostRoot: string;
}

export interface WebModeLaunchFailure {
	mode: "web";
	ok: false;
	cwd: string;
	projectSessionsDir: string;
	host: string;
	port: number | null;
	url: string | null;
	hostKind: ResolvedWebHostBootstrap["kind"] | "unresolved";
	hostPath: string | null;
	hostRoot: string | null;
	failureReason: string;
	candidates?: string[];
}

export type WebModeLaunchStatus = WebModeLaunchSuccess | WebModeLaunchFailure;

export interface WebModeDeps {
	existsSync?: (path: string) => boolean;
	initResources?: (agentDir: string) => void;
	resolvePort?: (host: string) => Promise<number>;
	spawn?: (
		command: string,
		args: readonly string[],
		options: SpawnOptions,
	) => SpawnedChildLike;
	waitForBootReady?: (url: string) => Promise<void>;
	openBrowser?: (url: string) => void;
	stderr?: WritableLike;
	env?: NodeJS.ProcessEnv;
	platform?: NodeJS.Platform;
	execPath?: string;
	pidFilePath?: string;
	writePidFile?: (path: string, pid: number) => void;
	readPidFile?: (path: string) => number | null;
	deletePidFile?: (path: string) => void;
	/** Path to the multi-instance registry JSON (for testing). */
	registryPath?: string;
}

export interface WebModeStopResult {
	ok: boolean;
	reason?: string;
	/** How many instances were stopped (relevant for --all) */
	stoppedCount?: number;
}

// ─── Instance Registry ──────────────────────────────────────────────────────

export interface WebInstanceEntry {
	pid: number;
	port: number;
	url: string;
	cwd: string;
	startedAt: string;
}

export type WebInstanceRegistry = Record<string, WebInstanceEntry>;

const WEB_INSTANCES_PATH = join(appRoot, "web-instances.json");

export function readInstanceRegistry(
	registryPath = WEB_INSTANCES_PATH,
): WebInstanceRegistry {
	try {
		return JSON.parse(
			readFileSync(registryPath, "utf8"),
		) as WebInstanceRegistry;
	} catch {
		return {};
	}
}

export function writeInstanceRegistry(
	registry: WebInstanceRegistry,
	registryPath = WEB_INSTANCES_PATH,
): void {
	writeFileSync(registryPath, JSON.stringify(registry, null, 2), "utf8");
}

export function registerInstance(
	cwd: string,
	entry: Omit<WebInstanceEntry, "cwd" | "startedAt">,
	registryPath = WEB_INSTANCES_PATH,
): void {
	const registry = readInstanceRegistry(registryPath);
	registry[resolve(cwd)] = {
		...entry,
		cwd: resolve(cwd),
		startedAt: new Date().toISOString(),
	};
	writeInstanceRegistry(registry, registryPath);
}

export function unregisterInstance(
	cwd: string,
	registryPath = WEB_INSTANCES_PATH,
): void {
	const registry = readInstanceRegistry(registryPath);
	delete registry[resolve(cwd)];
	writeInstanceRegistry(registry, registryPath);
}

function killPid(pid: number): "killed" | "already-dead" | { error: string } {
	try {
		process.kill(pid, "SIGTERM");
		return "killed";
	} catch (error) {
		const isAlreadyDead =
			error instanceof Error &&
			"code" in error &&
			(error as NodeJS.ErrnoException).code === "ESRCH";
		if (isAlreadyDead) return "already-dead";
		return { error: error instanceof Error ? error.message : String(error) };
	}
}

export function writePidFile(filePath: string, pid: number): void {
	writeFileSync(filePath, String(pid), "utf8");
}

export function readPidFile(filePath: string): number | null {
	try {
		const content = readFileSync(filePath, "utf8").trim();
		const pid = parseInt(content, 10);
		return Number.isFinite(pid) && pid > 0 ? pid : null;
	} catch {
		return null;
	}
}

export function deletePidFile(filePath: string): void {
	try {
		unlinkSync(filePath);
	} catch {
		// Non-fatal — file may already be gone
	}
}

export interface WebModeStopOptions {
	/** Stop instance for a specific project path */
	projectCwd?: string;
	/** Stop all running instances */
	all?: boolean;
}

export function stopWebMode(
	deps: Pick<
		WebModeDeps,
		"pidFilePath" | "readPidFile" | "deletePidFile" | "stderr"
	> = {},
	options: WebModeStopOptions = {},
): WebModeStopResult {
	const stderr = deps.stderr ?? process.stderr;

	// ── Stop all instances ──────────────────────────────────────────────
	if (options.all) {
		const registry = readInstanceRegistry();
		const entries = Object.entries(registry);
		if (entries.length === 0) {
			// Fall back to legacy PID file
			return stopLegacyPidFile(deps);
		}
		let stopped = 0;
		for (const [cwd, entry] of entries) {
			const result = killPid(entry.pid);
			if (result === "killed") {
				stderr.write(
					`[forge] Stopped SF server for ${cwd} (pid=${entry.pid})\n`,
				);
				stopped++;
			} else if (result === "already-dead") {
				stderr.write(
					`[forge] Web server for ${cwd} was already stopped (pid=${entry.pid})\n`,
				);
				stopped++;
			} else {
				stderr.write(
					`[forge] Failed to stop SF server for ${cwd}: ${result.error}\n`,
				);
			}
			unregisterInstance(cwd);
		}
		// Also clean up legacy PID file
		const deletePid = deps.deletePidFile ?? deletePidFile;
		const pidFilePath = deps.pidFilePath ?? defaultWebPidFilePath;
		deletePid(pidFilePath);
		stderr.write(
			`[forge] Stopped ${stopped} instance${stopped === 1 ? "" : "s"}.\n`,
		);
		// Also reap orphaned next-server processes (sf-mooe4m5k-6fm7z9)
		const orphanResult = reapOrphanedNextServerProcesses(stderr);
		if (orphanResult.reaped > 0) {
			stderr.write(
				`[forge] Reaped ${orphanResult.reaped} orphaned next-server process${orphanResult.reaped === 1 ? "" : "es"}.\n`,
			);
		}
		return { ok: true, stoppedCount: stopped };
	}

	// ── Stop specific project ──────────────────────────────────────────
	if (options.projectCwd) {
		const resolvedCwd = resolve(options.projectCwd);
		const registry = readInstanceRegistry();
		const entry = registry[resolvedCwd];
		if (!entry) {
			stderr.write(`[forge] No SF server running for ${resolvedCwd}\n`);
			return { ok: false, reason: "not-found" };
		}
		const result = killPid(entry.pid);
		unregisterInstance(resolvedCwd);
		if (result === "killed") {
			stderr.write(
				`[forge] Stopped SF server for ${resolvedCwd} (pid=${entry.pid})\n`,
			);
			return { ok: true, stoppedCount: 1 };
		} else if (result === "already-dead") {
			stderr.write(
				`[forge] Web server for ${resolvedCwd} was already stopped — cleared stale entry.\n`,
			);
			return { ok: true, stoppedCount: 1 };
		} else {
			stderr.write(
				`[forge] Failed to stop SF server for ${resolvedCwd}: ${result.error}\n`,
			);
			return { ok: false, reason: result.error };
		}
	}

	// ── Default: stop via legacy PID file (backward compat) ─────────────
	return stopLegacyPidFile(deps);
}

function stopLegacyPidFile(
	deps: Pick<
		WebModeDeps,
		"pidFilePath" | "readPidFile" | "deletePidFile" | "stderr"
	>,
): WebModeStopResult {
	const stderr = deps.stderr ?? process.stderr;
	const pidFilePath = deps.pidFilePath ?? defaultWebPidFilePath;
	const readPid = deps.readPidFile ?? readPidFile;
	const deletePid = deps.deletePidFile ?? deletePidFile;

	const pid = readPid(pidFilePath);
	if (pid === null) {
		stderr.write(`[forge] Web server is not running (no PID file found)\n`);
		return { ok: false, reason: "no-pid-file" };
	}

	stderr.write(`[forge] Stopping SF server (pid=${pid})…\n`);

	const result = killPid(pid);
	deletePid(pidFilePath);
	if (result === "killed") {
		stderr.write(`[forge] Web server stopped.\n`);
		return { ok: true };
	} else if (result === "already-dead") {
		stderr.write(
			`[forge] Web server was already stopped — cleared stale PID file.\n`,
		);
		return { ok: true };
	} else {
		stderr.write(`[forge] Failed to stop SF server: ${result.error}\n`);
		return { ok: false, reason: result.error };
	}
}

async function loadResourceBootstrap(): Promise<ResourceBootstrapLike> {
	const mod = await import("./resource-loader.js");
	return {
		initResources: mod.initResources,
	};
}

export function resolveWebHostBootstrap(
	options: {
		packageRoot?: string;
		existsSync?: (path: string) => boolean;
	} = {},
): WebHostBootstrap {
	const packageRoot = options.packageRoot ?? DEFAULT_PACKAGE_ROOT;
	const checkExists = options.existsSync ?? existsSync;
	const packagedStandaloneServer = join(
		packageRoot,
		"dist",
		"web",
		"standalone",
		"server.js",
	);
	if (checkExists(packagedStandaloneServer)) {
		return {
			ok: true,
			kind: "packaged-standalone",
			packageRoot,
			hostRoot: join(packageRoot, "dist", "web", "standalone"),
			entryPath: packagedStandaloneServer,
		};
	}

	const sourceWebRoot = join(packageRoot, "web");
	const sourceManifest = join(sourceWebRoot, "package.json");
	if (checkExists(sourceManifest)) {
		return {
			ok: true,
			kind: "source-dev",
			packageRoot,
			hostRoot: sourceWebRoot,
			entryPath: sourceManifest,
		};
	}

	return {
		ok: false,
		packageRoot,
		reason: "host bootstrap not found",
		candidates: [packagedStandaloneServer, sourceManifest],
	};
}

export async function reserveWebPort(host = DEFAULT_HOST): Promise<number> {
	return await new Promise<number>((resolvePort, reject) => {
		const server = createServer();
		server.unref();
		server.once("error", reject);
		server.listen(0, host, () => {
			const address = server.address();
			if (!address || typeof address === "string") {
				server.close(() =>
					reject(new Error("failed to determine reserved web port")),
				);
				return;
			}
			server.close((error) => {
				if (error) {
					reject(error);
					return;
				}
				resolvePort(address.port);
			});
		});
	});
}

function getSpawnCommandForSourceHost(platform: NodeJS.Platform): string {
	return platform === "win32" ? "npm.cmd" : "npm";
}

function needsWindowsShell(
	command: string,
	platform: NodeJS.Platform,
): boolean {
	return platform === "win32" && /\.(cmd|bat)$/i.test(command);
}

function formatLaunchStatus(status: WebModeLaunchStatus): string {
	if (status.ok) {
		return `[forge] Server mode startup: status=started cwd=${status.cwd} port=${status.port} host=${status.hostPath} kind=${status.hostKind} url=${status.url}\n`;
	}

	return `[forge] Server mode startup: status=failed cwd=${status.cwd} port=${status.port ?? "n/a"} host=${status.hostPath ?? "unresolved"} kind=${status.hostKind} reason=${(status as any).failureReason}\n`;
}

function emitLaunchStatus(
	stderr: WritableLike,
	status: WebModeLaunchStatus,
): void {
	stderr.write(formatLaunchStatus(status));
}

function buildSpawnSpec(
	resolution: ResolvedWebHostBootstrap,
	host: string,
	port: number,
	platform: NodeJS.Platform,
	execPath: string,
): { command: string; args: string[]; cwd: string } {
	if (resolution.kind === "packaged-standalone") {
		return {
			command: execPath,
			args: [resolution.entryPath],
			cwd: resolution.hostRoot,
		};
	}

	return {
		command: getSpawnCommandForSourceHost(platform),
		args: ["run", "dev", "--", "--hostname", host, "--port", String(port)],
		cwd: resolution.hostRoot,
	};
}

async function spawnDetachedProcess(
	spawnCommand: (
		command: string,
		args: readonly string[],
		options: SpawnOptions,
	) => SpawnedChildLike,
	command: string,
	args: string[],
	options: SpawnOptions,
): Promise<
	{ ok: true; child: SpawnedChildLike } | { ok: false; error: unknown }
> {
	return await new Promise((resolve) => {
		try {
			const child = spawnCommand(command, args, options);
			let settled = false;
			const finish = (
				result:
					| { ok: true; child: SpawnedChildLike }
					| { ok: false; error: unknown },
			) => {
				if (settled) return;
				settled = true;
				resolve(result);
			};

			child.once?.("error", (error) => finish({ ok: false, error }));
			setImmediate(() => finish({ ok: true, child }));
		} catch (error) {
			resolve({ ok: false, error });
		}
	});
}

async function requestLocalJson(
	url: string,
	timeoutMs: number,
	authToken?: string,
): Promise<{ statusCode: number; body: string }> {
	return await new Promise((resolve, reject) => {
		const headers: Record<string, string> = {
			Accept: "application/json",
			// Keep launch readiness on the cheapest uncompressed path. The
			// packaged host can spend noticeable time compressing the large boot
			// snapshot, which adds avoidable startup jitter for a local health
			// check that only needs the JSON payload itself.
			"Accept-Encoding": "identity",
		};
		if (authToken) {
			headers["Authorization"] = `Bearer ${authToken}`;
		}
		const request = httpRequest(
			url,
			{
				method: "GET",
				headers,
			},
			(response) => {
				const statusCode = response.statusCode ?? 0;
				let body = "";
				response.setEncoding("utf8");
				response.on("data", (chunk) => {
					body += chunk;
				});
				response.on("end", () => resolve({ statusCode, body }));
			},
		);

		request.setTimeout(timeoutMs, () => {
			request.destroy(new Error(`request timed out after ${timeoutMs}ms`));
		});
		request.once("error", reject);
		request.end();
	});
}

async function waitForBootReady(
	url: string,
	timeoutMs = 180_000,
	stderr?: WritableLike,
	authToken?: string,
): Promise<void> {
	const deadline = Date.now() + timeoutMs;
	const startedAt = Date.now();
	let lastError: string | null = null;
	let lastBody: string | null = null;
	let hostUp = false;
	let consecutive5xx = 0;
	const MAX_CONSECUTIVE_5XX = 3;
	// Print a progress dot every N ms while waiting so the terminal isn't silent
	const TICKER_INTERVAL_MS = 5_000;
	let lastTickAt = startedAt;

	const elapsed = () => `${Math.round((Date.now() - startedAt) / 1000)}s`;

	while (Date.now() < deadline) {
		try {
			// Give the packaged host enough time to finish a cold /api/boot render.
			const response = await requestLocalJson(
				`${url}/api/boot`,
				45_000,
				authToken,
			);

			if (response.statusCode >= 200 && response.statusCode < 300) {
				if (!hostUp) {
					hostUp = true;
					stderr?.write(`[forge] Web host ready.\n`);
				}
				consecutive5xx = 0;
				// Host responded successfully — it's ready for the browser
				return;
			} else if (response.statusCode >= 500) {
				consecutive5xx++;
				lastError = `http ${response.statusCode}`;
				lastBody = response.body || null;
				if (consecutive5xx >= MAX_CONSECUTIVE_5XX) {
					const detail = lastBody ? `: ${lastBody.slice(0, 500)}` : "";
					throw new Error(
						`boot route returned ${MAX_CONSECUTIVE_5XX} consecutive 5xx responses (last: ${response.statusCode})${detail}`,
					);
				}
			} else {
				consecutive5xx = 0;
				lastError = `http ${response.statusCode}`;
			}
		} catch (error) {
			if (
				error instanceof Error &&
				error.message.startsWith("boot route returned")
			) {
				throw error;
			}
			// Connection refused, timeout, etc. — transient during cold start
			consecutive5xx = 0;
			lastError = error instanceof Error ? error.message : String(error);
		}

		// Emit a heartbeat line every TICKER_INTERVAL_MS to show we're alive
		const now = Date.now();
		if (now - lastTickAt >= TICKER_INTERVAL_MS) {
			lastTickAt = now;
			if (hostUp) {
				stderr?.write(`[forge] Still waiting… (${elapsed()})\n`);
			} else {
				stderr?.write(`[forge] Waiting for web host… (${elapsed()})\n`);
			}
		}

		await new Promise((resolve) => setTimeout(resolve, 250));
	}

	throw new Error(lastError ?? "timed out waiting for boot readiness");
}

/**
 * If a previous SF server instance is registered for the same `cwd`, attempt
 * to kill it and remove its registry entry so the new launch can bind the port
 * cleanly.  This handles the "orphan process" scenario where a prior `sf server`
 * was terminated without clean shutdown (e.g. terminal closed).
 */
function cleanupStaleInstance(
	cwd: string,
	stderr: WritableLike,
	registryPath?: string,
): void {
	const registry = readInstanceRegistry(registryPath);
	const key = resolve(cwd);
	const stale = registry[key];
	if (!stale) return;

	stderr.write(
		`[forge] Cleaning up stale SF server for ${key} (pid=${stale.pid}, port=${stale.port})…\n`,
	);
	const result = killPid(stale.pid);
	if (result === "killed") {
		stderr.write(`[forge] Killed stale SF server (pid=${stale.pid}).\n`);
	} else if (result === "already-dead") {
		stderr.write(
			`[forge] Stale SF server was already stopped (pid=${stale.pid}) — clearing entry.\n`,
		);
	} else {
		stderr.write(
			`[forge] Could not kill stale SF server (pid=${stale.pid}): ${result.error}\n`,
		);
	}
	unregisterInstance(cwd, registryPath);
}

/**
 * Detect and reap orphaned next-server processes that outlived their parent
 * web host. These orphans have cwd under dist/web/standalone (or a deleted
 * variant) and parent PID 1 (init). They are created when the web host process
 * exits without cleanly terminating its next-server child.
 *
 * Purpose: prevent stale next-server processes from accumulating and holding
 * ports or consuming resources after sf server stop or host replacement.
 * Consumer: launchWebMode before binding a new port, and stopWebMode --all.
 *
 * AC2 from sf-mooe4m5k-6fm7z9: Orphaned next-server processes with cwd under
 * dist/web/standalone are detected and reaped on next server launch for the same repo.
 */
export function reapOrphanedNextServerProcesses(
	stderr: WritableLike,
	packageRoot = DEFAULT_PACKAGE_ROOT,
	deps: {
		execSync?: typeof execSync;
		readlinkSync?: typeof readlinkSync;
		kill?: typeof process.kill;
		platform?: NodeJS.Platform;
	} = {},
): { reaped: number; errors: string[] } {
	const errors: string[] = [];
	let reaped = 0;
	const platform = deps.platform ?? process.platform;
	if (platform === "win32") {
		// Windows orphan detection not implemented; rely on port-kill fallback
		return { reaped: 0, errors: [] };
	}
	try {
		// Find next-server processes with cwd matching our standalone host path
		const standalonePath = resolve(packageRoot, "dist", "web", "standalone");
		// Use ps to find node processes with next-server in their command line
		const psOutput = (deps.execSync ?? execSync)(
			"ps -eo pid,ppid,cmd,comm --no-headers",
			{ encoding: "utf8", timeout: 5000 },
		) as string;
		const lines = psOutput.split("\n").filter((line) => line.trim());
		for (const line of lines) {
			const parts = line.trim().split(/\s+/);
			if (parts.length < 4) continue;
			const pidStr = parts[0];
			const ppidStr = parts[1];
			const cmd = parts.slice(2).join(" ");
			const pid = Number.parseInt(pidStr, 10);
			const ppid = Number.parseInt(ppidStr, 10);
			if (!Number.isFinite(pid) || pid <= 1) continue;
			// Look for next-server in command line
			if (!cmd.includes("next-server") && !cmd.includes("server.js")) continue;
			// Check if the process cwd matches our standalone path (or deleted variant)
			let cwd: string | null = null;
			try {
				cwd = (deps.readlinkSync ?? readlinkSync)(`/proc/${pid}/cwd`);
			} catch {
				// Process may have exited between ps and readlink
				continue;
			}
			if (
				cwd &&
				(cwd.startsWith(standalonePath) || cwd.includes("standalone (deleted)"))
			) {
				// Orphan: parent is init (ppid=1) or the parent is dead
				const isOrphan = ppid === 1;
				if (isOrphan) {
					try {
						(deps.kill ?? process.kill)(pid, "SIGTERM");
						reaped++;
						stderr.write(
							`[forge] Reaped orphaned next-server (pid=${pid}, cwd=${cwd})\n`,
						);
					} catch (killErr) {
						const msg =
							killErr instanceof Error ? killErr.message : String(killErr);
						errors.push(`pid=${pid}: ${msg}`);
					}
				}
			}
		}
	} catch (execErr) {
		const msg = execErr instanceof Error ? execErr.message : String(execErr);
		errors.push(`ps exec failed: ${msg}`);
	}
	return { reaped, errors };
}

export async function launchWebMode(
	options: WebModeLaunchOptions,
	deps: WebModeDeps = {},
): Promise<WebModeLaunchStatus> {
	const stderr = deps.stderr ?? process.stderr;
	const host = options.host ?? DEFAULT_HOST;
	const resolution = resolveWebHostBootstrap({
		packageRoot: options.packageRoot,
		existsSync: deps.existsSync,
	});

	if (!resolution.ok) {
		const failure: WebModeLaunchFailure = {
			mode: "web",
			ok: false,
			cwd: options.cwd,
			projectSessionsDir: options.projectSessionsDir,
			host,
			port: null,
			url: null,
			hostKind: "unresolved",
			hostPath: null,
			hostRoot: null,
			failureReason: `${resolution.reason}; checked=${resolution.candidates.join(",")}`,
			candidates: resolution.candidates,
		};
		emitLaunchStatus(stderr, failure);
		return failure;
	}

	stderr.write(`[forge] Starting server mode…\n`);

	// Kill any stale server instance for this project before reserving a port.
	// This prevents EADDRINUSE when the previous `sf server` was terminated
	// without a clean shutdown (e.g. terminal closed, crash).
	cleanupStaleInstance(options.cwd, stderr, deps.registryPath);

	// Also reap orphaned next-server processes from prior unclean shutdowns
	// (sf-mooe4m5k-6fm7z9): orphaned next-server processes with cwd under
	// dist/web/standalone are detected and reaped on next server launch.
	const orphanResult = reapOrphanedNextServerProcesses(
		stderr,
		options.packageRoot,
	);
	if (orphanResult.reaped > 0) {
		stderr.write(
			`[forge] Reaped ${orphanResult.reaped} orphaned next-server process${orphanResult.reaped === 1 ? "" : "es"} before launch.\n`,
		);
	}

	const port =
		options.port ?? (await (deps.resolvePort ?? reserveWebPort)(host));
	const authToken = randomBytes(32).toString("hex");
	const url = `http://${host}:${port}`;
	const env = {
		...(deps.env ?? process.env),
		HOSTNAME: host,
		PORT: String(port),
		SF_WEB_HOST: host,
		SF_WEB_PORT: String(port),
		SF_WEB_AUTH_TOKEN: authToken,
		SF_WEB_PROJECT_CWD: options.cwd,
		SF_WEB_PROJECT_SESSIONS_DIR: options.projectSessionsDir,
		SF_WEB_PACKAGE_ROOT: resolution.packageRoot,
		SF_WEB_HOST_KIND: resolution.kind,
		...(resolution.kind === "source-dev" ? { NEXT_PUBLIC_SF_DEV: "1" } : {}),
		...(options.allowedOrigins?.length
			? { SF_WEB_ALLOWED_ORIGINS: options.allowedOrigins.join(",") }
			: {}),
	};

	try {
		stderr.write(`[forge] Initialising resources…\n`);
		const bootstrap = deps.initResources
			? { initResources: deps.initResources }
			: await loadResourceBootstrap();
		bootstrap.initResources(options.agentDir);
	} catch (error) {
		const failure: WebModeLaunchFailure = {
			mode: "web",
			ok: false,
			cwd: options.cwd,
			projectSessionsDir: options.projectSessionsDir,
			host,
			port,
			url,
			hostKind: resolution.kind,
			hostPath: resolution.entryPath,
			hostRoot: resolution.hostRoot,
			failureReason: `bootstrap:${error instanceof Error ? error.message : String(error)}`,
		};
		emitLaunchStatus(stderr, failure);
		return failure;
	}

	const spawnSpec = buildSpawnSpec(
		resolution,
		host,
		port,
		deps.platform ?? process.platform,
		deps.execPath ?? process.execPath,
	);

	stderr.write(`[forge] Launching web host on port ${port}…\n`);

	const spawnResult = await spawnDetachedProcess(
		deps.spawn ??
			((command, args, spawnOptions) => spawn(command, args, spawnOptions)),
		spawnSpec.command,
		spawnSpec.args,
		{
			cwd: spawnSpec.cwd,
			detached: true,
			stdio: "ignore",
			windowsHide: true,
			shell: needsWindowsShell(
				spawnSpec.command,
				deps.platform ?? process.platform,
			),
			env,
		},
	);

	if (!spawnResult.ok) {
		const failure: WebModeLaunchFailure = {
			mode: "web",
			ok: false,
			cwd: options.cwd,
			projectSessionsDir: options.projectSessionsDir,
			host,
			port,
			url,
			hostKind: resolution.kind,
			hostPath: resolution.entryPath,
			hostRoot: resolution.hostRoot,
			failureReason: `launch:${spawnResult.error instanceof Error ? spawnResult.error.message : String(spawnResult.error)}`,
		};
		emitLaunchStatus(stderr, failure);
		return failure;
	}

	try {
		const bootReadyFn =
			deps.waitForBootReady ??
			((u: string) => waitForBootReady(u, 180_000, stderr, authToken));
		await bootReadyFn(url);
	} catch (error) {
		const failure: WebModeLaunchFailure = {
			mode: "web",
			ok: false,
			cwd: options.cwd,
			projectSessionsDir: options.projectSessionsDir,
			host,
			port,
			url,
			hostKind: resolution.kind,
			hostPath: resolution.entryPath,
			hostRoot: resolution.hostRoot,
			failureReason: `boot-ready:${error instanceof Error ? error.message : String(error)}`,
		};
		emitLaunchStatus(stderr, failure);
		return failure;
	}

	try {
		spawnResult.child.unref?.();
		const pid = spawnResult.child.pid;
		if (pid !== undefined) {
			const pidFilePath = deps.pidFilePath ?? defaultWebPidFilePath;
			(deps.writePidFile ?? writePidFile)(pidFilePath, pid);
			// Register in multi-instance registry
			registerInstance(options.cwd, { pid, port, url }, deps.registryPath);
		}
		const authenticatedUrl = `${url}/#token=${authToken}`;
		try {
			(deps.openBrowser ?? openBrowser)(authenticatedUrl);
		} catch (browserError) {
			stderr.write(
				`[forge] Could not open browser: ${browserError instanceof Error ? browserError.message : String(browserError)}\n`,
			);
		}
	} catch (error) {
		const failure: WebModeLaunchFailure = {
			mode: "web",
			ok: false,
			cwd: options.cwd,
			projectSessionsDir: options.projectSessionsDir,
			host,
			port,
			url,
			hostKind: resolution.kind,
			hostPath: resolution.entryPath,
			hostRoot: resolution.hostRoot,
			failureReason: `browser-open:${error instanceof Error ? error.message : String(error)}`,
		};
		emitLaunchStatus(stderr, failure);
		return failure;
	}

	const authenticatedUrl = `${url}/#token=${authToken}`;
	const success: WebModeLaunchSuccess = {
		mode: "web",
		ok: true,
		cwd: options.cwd,
		projectSessionsDir: options.projectSessionsDir,
		host,
		port,
		url,
		hostKind: resolution.kind,
		hostPath: resolution.entryPath,
		hostRoot: resolution.hostRoot,
	};
	stderr.write(`[forge] Ready → ${authenticatedUrl}\n`);
	emitLaunchStatus(stderr, success);
	return success;
}
