import { randomBytes } from 'node:crypto';
import { execFile, spawn } from 'node:child_process';
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { request as httpRequest } from 'node:http';
import { createServer } from 'node:net';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { appRoot, webPidFilePath as defaultWebPidFilePath } from './app-paths.js';
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
/** Open a URL in the user's default browser. */
function openBrowser(url) {
    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, "''")}'`], () => { });
    }
    else {
        const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
        execFile(cmd, [url], () => { });
    }
}
const WEB_INSTANCES_PATH = join(appRoot, 'web-instances.json');
export function readInstanceRegistry(registryPath = WEB_INSTANCES_PATH) {
    try {
        return JSON.parse(readFileSync(registryPath, 'utf8'));
    }
    catch {
        return {};
    }
}
export function writeInstanceRegistry(registry, registryPath = WEB_INSTANCES_PATH) {
    try {
        writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
    }
    catch (err) {
        process.stderr.write(`[nerf] Warning: failed to write instance registry ${registryPath}: ${err instanceof Error ? err.message : String(err)}\n`);
    }
}
export function registerInstance(cwd, entry, registryPath = WEB_INSTANCES_PATH) {
    const registry = readInstanceRegistry(registryPath);
    registry[resolve(cwd)] = {
        ...entry,
        cwd: resolve(cwd),
        startedAt: new Date().toISOString(),
    };
    writeInstanceRegistry(registry, registryPath);
}
export function unregisterInstance(cwd, registryPath = WEB_INSTANCES_PATH) {
    const registry = readInstanceRegistry(registryPath);
    delete registry[resolve(cwd)];
    writeInstanceRegistry(registry, registryPath);
}
function killPid(pid) {
    try {
        process.kill(pid, 'SIGTERM');
        return 'killed';
    }
    catch (error) {
        const isAlreadyDead = error instanceof Error && 'code' in error && error.code === 'ESRCH';
        if (isAlreadyDead)
            return 'already-dead';
        return { error: error instanceof Error ? error.message : String(error) };
    }
}
export function writePidFile(filePath, pid) {
    writeFileSync(filePath, String(pid), 'utf8');
}
export function readPidFile(filePath) {
    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) {
    try {
        unlinkSync(filePath);
    }
    catch {
        // Non-fatal — file may already be gone
    }
}
export function stopWebMode(deps = {}, options = {}) {
    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(`[nerf] Stopped web server for ${cwd} (pid=${entry.pid})\n`);
                stopped++;
            }
            else if (result === 'already-dead') {
                stderr.write(`[nerf] Web server for ${cwd} was already stopped (pid=${entry.pid})\n`);
                stopped++;
            }
            else {
                stderr.write(`[nerf] Failed to stop web 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(`[nerf] Stopped ${stopped} instance${stopped === 1 ? '' : 's'}.\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(`[nerf] No web server running for ${resolvedCwd}\n`);
            return { ok: false, reason: 'not-found' };
        }
        const result = killPid(entry.pid);
        unregisterInstance(resolvedCwd);
        if (result === 'killed') {
            stderr.write(`[nerf] Stopped web server for ${resolvedCwd} (pid=${entry.pid})\n`);
            return { ok: true, stoppedCount: 1 };
        }
        else if (result === 'already-dead') {
            stderr.write(`[nerf] Web server for ${resolvedCwd} was already stopped — cleared stale entry.\n`);
            return { ok: true, stoppedCount: 1 };
        }
        else {
            stderr.write(`[nerf] Failed to stop web 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) {
    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(`[nerf] Web server is not running (no PID file found)\n`);
        return { ok: false, reason: 'no-pid-file' };
    }
    stderr.write(`[nerf] Stopping web server (pid=${pid})…\n`);
    const result = killPid(pid);
    deletePid(pidFilePath);
    if (result === 'killed') {
        stderr.write(`[nerf] Web server stopped.\n`);
        return { ok: true };
    }
    else if (result === 'already-dead') {
        stderr.write(`[nerf] Web server was already stopped — cleared stale PID file.\n`);
        return { ok: true };
    }
    else {
        stderr.write(`[nerf] Failed to stop web server: ${result.error}\n`);
        return { ok: false, reason: result.error };
    }
}
async function loadResourceBootstrap() {
    const mod = await import('./resource-loader.js');
    return {
        initResources: mod.initResources,
    };
}
export function resolveWebHostBootstrap(options = {}) {
    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) {
    return await new Promise((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) {
    return platform === 'win32' ? 'npm.cmd' : 'npm';
}
function formatLaunchStatus(status) {
    if (status.ok) {
        return `[nerf] Web mode startup: status=started cwd=${status.cwd} port=${status.port} host=${status.hostPath} kind=${status.hostKind} url=${status.url}\n`;
    }
    return `[nerf] Web mode startup: status=failed cwd=${status.cwd} port=${status.port ?? 'n/a'} host=${status.hostPath ?? 'unresolved'} kind=${status.hostKind} reason=${status.failureReason}\n`;
}
function emitLaunchStatus(stderr, status) {
    stderr.write(formatLaunchStatus(status));
}
function buildSpawnSpec(resolution, host, port, platform, execPath) {
    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, args, options) {
    return await new Promise((resolve) => {
        try {
            const child = spawnCommand(command, args, options);
            let settled = false;
            const finish = (result) => {
                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, timeoutMs, authToken) {
    return await new Promise((resolve, reject) => {
        const headers = {
            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, timeoutMs = 180_000, stderr, authToken) {
    const deadline = Date.now() + timeoutMs;
    const startedAt = Date.now();
    let lastError = null;
    let lastBody = 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(`[nerf] 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(`[nerf] Still waiting… (${elapsed()})\n`);
            }
            else {
                stderr?.write(`[nerf] 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 web 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 `nerf --web`
 * was terminated without clean shutdown (e.g. terminal closed).
 */
function cleanupStaleInstance(cwd, stderr, registryPath) {
    const registry = readInstanceRegistry(registryPath);
    const key = resolve(cwd);
    const stale = registry[key];
    if (!stale)
        return;
    stderr.write(`[nerf] Cleaning up stale web server for ${key} (pid=${stale.pid}, port=${stale.port})…\n`);
    const result = killPid(stale.pid);
    if (result === 'killed') {
        stderr.write(`[nerf] Killed stale web server (pid=${stale.pid}).\n`);
    }
    else if (result === 'already-dead') {
        stderr.write(`[nerf] Stale web server was already stopped (pid=${stale.pid}) — clearing entry.\n`);
    }
    else {
        stderr.write(`[nerf] Could not kill stale web server (pid=${stale.pid}): ${result.error}\n`);
    }
    unregisterInstance(cwd, registryPath);
}
export async function launchWebMode(options, deps = {}) {
    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 = {
            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(`[nerf] Starting web mode…\n`);
    // Kill any stale server instance for this project before reserving a port.
    // This prevents EADDRINUSE when the previous `nerf --web` was terminated
    // without a clean shutdown (e.g. terminal closed, crash).
    cleanupStaleInstance(options.cwd, stderr, deps.registryPath);
    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),
        NERF_WEB_HOST: host,
        NERF_WEB_PORT: String(port),
        NERF_WEB_AUTH_TOKEN: authToken,
        NERF_WEB_PROJECT_CWD: options.cwd,
        NERF_WEB_PROJECT_SESSIONS_DIR: options.projectSessionsDir,
        NERF_WEB_PACKAGE_ROOT: resolution.packageRoot,
        NERF_WEB_HOST_KIND: resolution.kind,
        ...(resolution.kind === 'source-dev' ? { NEXT_PUBLIC_NERF_DEV: '1' } : {}),
        ...(options.allowedOrigins?.length ? { NERF_WEB_ALLOWED_ORIGINS: options.allowedOrigins.join(',') } : {}),
    };
    try {
        stderr.write(`[nerf] Initialising resources…\n`);
        const bootstrap = deps.initResources ? { initResources: deps.initResources } : await loadResourceBootstrap();
        bootstrap.initResources(options.agentDir);
    }
    catch (error) {
        const failure = {
            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(`[nerf] 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',
        env,
    });
    if (!spawnResult.ok) {
        const failure = {
            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) => waitForBootReady(u, 180_000, stderr, authToken));
        await bootReadyFn(url);
    }
    catch (error) {
        const failure = {
            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(`[nerf] Could not open browser: ${browserError instanceof Error ? browserError.message : String(browserError)}\n`);
        }
    }
    catch (error) {
        const failure = {
            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 = {
        mode: 'web',
        ok: true,
        cwd: options.cwd,
        projectSessionsDir: options.projectSessionsDir,
        host,
        port,
        url,
        hostKind: resolution.kind,
        hostPath: resolution.entryPath,
        hostRoot: resolution.hostRoot,
    };
    stderr.write(`[nerf] Ready → ${authenticatedUrl}\n`);
    emitLaunchStatus(stderr, success);
    return success;
}
