import { spawn } from "node:child_process";
import { closeSync, existsSync, openSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { LOOPBACK_HOST } from "./launcherConstants.js";
/**
 * Resolves the vendored Next standalone server entry (web/server.js) relative
 * to the compiled launcher (dist/launcher/cli.js → ../../web/server.js). The
 * web bundle ships INSIDE this package, so there is no second download or
 * source tree at runtime.
 */
export function resolveWebServerEntry(fromUrl = import.meta.url) {
    const here = path.dirname(fileURLToPath(fromUrl));
    // dist/launcher → package root is two up.
    return path.resolve(here, "..", "..", "web", "server.js");
}
/** Resolves the compiled bridge entry (dist/main.js). */
export function resolveBridgeEntry(fromUrl = import.meta.url) {
    const here = path.dirname(fileURLToPath(fromUrl));
    return path.resolve(here, "..", "main.js");
}
export function vendoredWebExists(fromUrl = import.meta.url) {
    return existsSync(resolveWebServerEntry(fromUrl));
}
/**
 * Spawns a long-lived child as a detached daemon: its own process group
 * (POSIX) so the launcher can later signal the whole tree, stdio redirected to
 * the shared log fd, windowsHide on Windows, and unref'd so the launcher
 * process can exit and return the terminal.
 */
export function spawnDaemon(command, args, options) {
    const platform = options.platform ?? process.platform;
    const out = openSync(options.logPath, "a");
    try {
        const child = spawn(command, args, {
            cwd: options.cwd,
            env: options.env,
            detached: platform !== "win32",
            stdio: ["ignore", out, out],
            windowsHide: true,
        });
        child.unref();
        if (typeof child.pid !== "number") {
            throw new Error(`Failed to spawn daemon: ${command}`);
        }
        return { pid: child.pid };
    }
    finally {
        // The parent drops its copy of the log fd so it does not keep the pipe open
        // and block its own clean exit; the detached child holds its own dup.
        closeSync(out);
    }
}
/**
 * Polls an HTTP endpoint until it responds OK or the deadline passes. Used to
 * confirm each daemon is actually serving before the launcher proceeds.
 */
export async function waitForHttpOk(url, options = {}) {
    const fetchImpl = options.fetchImpl ?? globalThis.fetch;
    const timeoutMs = options.timeoutMs ?? 15_000;
    const intervalMs = options.intervalMs ?? 200;
    const now = options.now ?? (() => Date.now());
    const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
    const deadline = now() + timeoutMs;
    for (;;) {
        try {
            const response = await fetchImpl(url, { cache: "no-store" });
            if (response.ok)
                return true;
        }
        catch {
            // not up yet
        }
        if (now() >= deadline)
            return false;
        await sleep(intervalMs);
    }
}
export function bridgeBaseUrl(port) {
    return `http://${LOOPBACK_HOST}:${port}`;
}
export function webBaseUrl(port) {
    return `http://${LOOPBACK_HOST}:${port}`;
}
