import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { createServer } from "node:net";
import { SandboxManager, reserveLocalPort } from "../security/sandbox-manager.js";
const DEFAULT_MAX_CONTAINER_LIVE_VIEW_STARTS = 1;
let activeContainerLiveViewStarts = 0;
/**
 * Starts Xvfb + x11vnc + websockify to make a headed browser visible via noVNC.
 * Returns a LiveViewSession with the display to set as DISPLAY env var
 * and the websocket URL the frontend can connect to.
 */
export async function startLiveView(options) {
    const preferContainer = options?.preferContainer !== false;
    if (preferContainer) {
        try {
            const maxStarts = readMaxContainerLiveViewStarts();
            if (activeContainerLiveViewStarts >= maxStarts) {
                throw new Error(`Preview browser start already in progress (${activeContainerLiveViewStarts} active, max ${maxStarts})`);
            }
            activeContainerLiveViewStarts += 1;
            try {
                return await startContainerLiveView(options);
            }
            finally {
                activeContainerLiveViewStarts -= 1;
            }
        }
        catch (err) {
            throw new Error(`Docker sandbox browser failed: ${err?.message ?? err}`);
        }
    }
    return startHostLiveView(options);
}
function readMaxContainerLiveViewStarts() {
    const raw = process.env["JAIT_MAX_CONTAINER_LIVE_VIEW_STARTS"];
    if (!raw)
        return DEFAULT_MAX_CONTAINER_LIVE_VIEW_STARTS;
    const parsed = Number.parseInt(raw, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_CONTAINER_LIVE_VIEW_STARTS;
}
async function startContainerLiveView(options) {
    const projectRoot = options?.projectRoot ?? process.cwd();
    const [novncPort, vncPort, cdpPort] = await Promise.all([
        reserveLocalPort(),
        reserveLocalPort(),
        reserveLocalPort(),
    ]);
    const sandboxManager = new SandboxManager();
    const session = await sandboxManager.startBrowserSandbox({
        projectRoot,
        mountMode: "none",
        networkEnabled: true,
        hostGateway: true,
        novncPort,
        vncPort,
        cdpPort,
    });
    return {
        kind: "container",
        display: `container:${session.containerName}`,
        vncPort: session.vncPort,
        websockifyPort: session.novncPort,
        novncUrl: session.novncUrl,
        processes: [],
        containerName: session.containerName,
        cdpUrl: session.cdpUrl,
    };
}
async function startHostLiveView(options) {
    const width = options?.width ?? 1280;
    const height = options?.height ?? 720;
    const displayNum = options?.displayNumber ?? findAvailableDisplay();
    const display = `:${displayNum}`;
    const [vncPort, websockifyPort] = await Promise.all([
        reservePort(),
        reservePort(),
    ]);
    const processes = [];
    try {
        // 1. Start Xvfb
        const xvfb = await spawnChecked("Xvfb", [display, "-screen", "0", `${width}x${height}x24`]);
        processes.push(xvfb);
        await waitForDisplay(displayNum);
        // 2. Start x11vnc
        const x11vnc = await spawnChecked("x11vnc", [
            "-display", display,
            "-nopw",
            "-listen", "127.0.0.1",
            "-xkb",
            "-forever",
            "-shared",
            "-rfbport", String(vncPort),
        ]);
        processes.push(x11vnc);
        // Brief wait for x11vnc to bind
        await sleep(300);
        // 3. Start websockify
        const websockify = await spawnChecked("websockify", [String(websockifyPort), `127.0.0.1:${vncPort}`]);
        processes.push(websockify);
        // Brief wait for websockify to bind
        await sleep(200);
        return {
            kind: "host",
            display,
            vncPort,
            websockifyPort,
            novncUrl: `ws://127.0.0.1:${websockifyPort}`,
            processes,
        };
    }
    catch (err) {
        // Clean up on failure
        for (const proc of processes) {
            try {
                proc.kill();
            }
            catch { /* ignore */ }
        }
        throw err;
    }
}
async function spawnChecked(command, args) {
    const proc = spawn(command, args, { stdio: "ignore", detached: false });
    proc.unref();
    await new Promise((resolve, reject) => {
        const onSpawn = () => {
            cleanup();
            resolve();
        };
        const onError = (error) => {
            cleanup();
            reject(error);
        };
        const cleanup = () => {
            proc.off("spawn", onSpawn);
            proc.off("error", onError);
        };
        proc.once("spawn", onSpawn);
        proc.once("error", onError);
    });
    return proc;
}
export async function stopLiveView(session) {
    if (session.kind === "container" && session.containerName) {
        await new SandboxManager().stopContainer(session.containerName).catch(() => { });
        return;
    }
    for (const proc of [...session.processes].reverse()) {
        try {
            proc.kill();
        }
        catch { /* ignore */ }
    }
    session.processes.length = 0;
}
function findAvailableDisplay(start = 99, max = 199) {
    for (let n = start; n <= max; n++) {
        if (!existsSync(`/tmp/.X11-unix/X${n}`))
            return n;
    }
    throw new Error(`No available X display found in range :${start}-:${max}`);
}
async function waitForDisplay(displayNum, timeoutMs = 5000) {
    const socketPath = `/tmp/.X11-unix/X${displayNum}`;
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
        if (existsSync(socketPath))
            return;
        await sleep(100);
    }
    throw new Error(`Xvfb display :${displayNum} did not become ready within ${timeoutMs}ms`);
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
async function reservePort() {
    return await new Promise((resolve, reject) => {
        const server = createServer();
        server.unref();
        server.on("error", reject);
        server.listen(0, "127.0.0.1", () => {
            const address = server.address();
            if (!address || typeof address === "string") {
                server.close(() => reject(new Error("Failed to reserve a port")));
                return;
            }
            const { port } = address;
            server.close((error) => {
                if (error)
                    reject(error);
                else
                    resolve(port);
            });
        });
    });
}
//# sourceMappingURL=live-view-manager.js.map