import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { logger } from "../utils/logger.js";
const DEFAULT_PORT = 4096;
const HEALTH_TIMEOUT_MS = 30_000;
const HEALTH_POLL_INTERVAL_MS = 500;
const HEALTH_REQUEST_TIMEOUT_MS = 2_000;
const MAX_RETRIES = 3;
const SHUTDOWN_TIMEOUT_MS = 5_000;
/**
 * Build the Basic auth header for OpenCode requests.
 */
function makeAuthHeader(password) {
    return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
}
/**
 * Wait for the OpenCode health endpoint to return 200 OK.
 * Polls GET /global/health every 500ms, times out after 30s.
 */
async function waitForHealth(baseUrl, authHeader, timeoutMs) {
    const startTime = Date.now();
    while (Date.now() - startTime < timeoutMs) {
        try {
            const res = await fetch(`${baseUrl}/global/health`, {
                headers: { Authorization: authHeader },
                signal: AbortSignal.timeout(HEALTH_REQUEST_TIMEOUT_MS),
            });
            if (res.ok) {
                return;
            }
        }
        catch {
            // Not ready yet
        }
        await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS));
    }
    throw new Error("OPENCODE_UNREACHABLE");
}
/**
 * Spawn an OpenCode server process with a single attempt.
 * Returns the child process and auth info, or throws on failure.
 */
function spawnOnce(port, password) {
    const child = spawn("opencode", ["serve", "--port", String(port)], {
        env: {
            ...process.env,
            OPENCODE_SERVER_PASSWORD: password,
        },
        stdio: ["ignore", "pipe", "pipe"],
    });
    let stderrOutput = "";
    child.stderr?.on("data", (chunk) => {
        stderrOutput += chunk.toString();
    });
    child.stdout?.on("data", (chunk) => {
        logger.debug(`opencode stdout: ${chunk.toString().trimEnd()}`);
    });
    child.on("error", (err) => {
        logger.error(`OpenCode process error: ${err.message}`);
    });
    child.on("exit", (code, signal) => {
        if (code !== null && code !== 0) {
            logger.warn(`OpenCode process exited with code ${code}: ${stderrOutput.trimEnd()}`);
        }
        else if (signal) {
            logger.info(`OpenCode process killed with signal ${signal}`);
        }
    });
    return child;
}
/**
 * Gracefully stop an OpenCode child process.
 * Sends SIGTERM first, then SIGKILL after timeout.
 */
function stopProcess(child) {
    return new Promise((resolve) => {
        if (child.exitCode !== null || child.killed) {
            resolve();
            return;
        }
        const timeout = setTimeout(() => {
            child.kill("SIGKILL");
            resolve();
        }, SHUTDOWN_TIMEOUT_MS);
        child.on("exit", () => {
            clearTimeout(timeout);
            resolve();
        });
        child.kill("SIGTERM");
    });
}
/**
 * Spawn an OpenCode server process with retries and health check.
 *
 * - Generates a random password for HTTP Basic auth
 * - Spawns `opencode serve --port <port>` with OPENCODE_SERVER_PASSWORD env
 * - Waits for GET /global/health to return 200
 * - Retries up to 3 times on failure
 *
 * @param port Port to bind to (default: config opencode.port or 4096)
 * @returns OpenCodeProcess with the child process, auth info, and stop function
 */
export async function spawnOpenCode(port) {
    const resolvedPort = port ?? DEFAULT_PORT;
    const password = randomUUID();
    const baseUrl = `http://127.0.0.1:${resolvedPort}`;
    const authHeader = makeAuthHeader(password);
    let lastError;
    for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
        logger.info(`Spawning OpenCode server (attempt ${attempt}/${MAX_RETRIES}) on port ${resolvedPort}`);
        const child = spawnOnce(resolvedPort, password);
        try {
            // Check if process died immediately
            await new Promise((resolve, reject) => {
                const earlyExitHandler = (code) => {
                    reject(new Error(`OpenCode process exited early with code ${code}`));
                };
                child.once("exit", earlyExitHandler);
                // Give it a brief moment to see if it crashes immediately
                setTimeout(() => {
                    child.removeListener("exit", earlyExitHandler);
                    resolve();
                }, 200);
            });
            await waitForHealth(baseUrl, authHeader, HEALTH_TIMEOUT_MS);
            logger.info(`OpenCode server ready on port ${resolvedPort}`);
            const auth = { password, baseUrl, authHeader };
            return {
                process: child,
                auth,
                stop: () => stopProcess(child),
            };
        }
        catch (err) {
            lastError = err instanceof Error ? err : new Error(String(err));
            logger.warn(`OpenCode spawn attempt ${attempt} failed: ${lastError.message}`);
            // Kill the child if it's still running
            await stopProcess(child);
        }
    }
    throw lastError ?? new Error("OPENCODE_UNREACHABLE");
}
//# sourceMappingURL=spawn.js.map