#!/usr/bin/env node
import { spawn } from "node:child_process";
import { LOCAL_UI_SCOPES } from "../capabilities/LocalUiCapabilityService.js";
import { loadBridgeConfig } from "../config/bridgeConfig.js";
import { BRIDGE_VERSION } from "../version.js";
import { openHandoffInBrowser } from "./browserOpener.js";
import { acquireStartLock, clearDaemonRecord, LauncherLockError, readDaemonRecord, resolveDaemonStatus, stopProcessTree, writeDaemonRecord, } from "./daemonSupervisor.js";
import { loadOrCreateDurableBridgeToken } from "./durableBridgeToken.js";
import { loadOrCreateInstallIdentity } from "./installIdentity.js";
import { DEFAULT_WEB_PORT, LAUNCHER_BASELINE_SCOPES, LAUNCHER_FILES, LOOPBACK_HOST, } from "./launcherConstants.js";
import { mintLocalUiHandoff } from "./LocalUiHandoffLauncher.js";
import { bridgeBaseUrl, resolveBridgeEntry, resolveWebServerEntry, spawnDaemon, vendoredWebExists, waitForHttpOk, webBaseUrl, } from "./processes.js";
import { patchtrailAppDataDir } from "../config/appDataPath.js";
import path from "node:path";
import { checkForUpdate } from "./updateCheck.js";
const COMMANDS = new Set(["start", "stop", "status", "restart", "update"]);
async function main(argv, env) {
    if (argv.includes("--help") || argv.includes("-h")) {
        process.stdout.write(helpText());
        return;
    }
    if (argv.includes("--version") || argv.includes("-v")) {
        process.stdout.write(`${BRIDGE_VERSION}\n`);
        return;
    }
    const args = parseArgs(argv, env);
    switch (args.command) {
        case "start":
            await startCommand(args, env);
            return;
        case "stop":
            await stopCommand(env);
            return;
        case "status":
            await statusCommand(env);
            return;
        case "restart":
            await stopCommand(env);
            await startCommand(args, env);
            return;
        case "update":
            await updateCommand(env);
            return;
        default:
            throw new LauncherError(`Unknown command: ${args.command}`);
    }
}
function parseArgs(argv, env) {
    let command = "start";
    let webPort;
    const envWebPort = env.PATCHTRAIL_WEB_PORT
        ? Number.parseInt(env.PATCHTRAIL_WEB_PORT, 10)
        : undefined;
    if (envWebPort && Number.isInteger(envWebPort))
        webPort = envWebPort;
    let noUpdateCheck = Boolean(env.PATCHTRAIL_NO_UPDATE_CHECK);
    for (let i = 0; i < argv.length; i += 1) {
        const arg = argv[i];
        if (!arg)
            continue;
        if (COMMANDS.has(arg)) {
            command = arg;
            continue;
        }
        if (arg === "--no-update-check") {
            noUpdateCheck = true;
            continue;
        }
        if (arg === "--web-port") {
            const value = argv[(i += 1)];
            const parsed = value ? Number.parseInt(value, 10) : NaN;
            if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) {
                throw new LauncherError("--web-port must be a port between 1 and 65535.");
            }
            webPort = parsed;
            continue;
        }
        if (arg.startsWith("--web-port=")) {
            const parsed = Number.parseInt(arg.slice("--web-port=".length), 10);
            if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) {
                throw new LauncherError("--web-port must be a port between 1 and 65535.");
            }
            webPort = parsed;
            continue;
        }
    }
    return { command, webPort, noUpdateCheck };
}
async function startCommand(args, env) {
    // The launcher runs the bridge as a production daemon. Resolve config under a
    // guaranteed-valid mode so the launcher does not require its OWN NODE_ENV to
    // be set just to read the port; the spawned daemon gets daemonNodeEnv below.
    const daemonNodeEnv = resolveDaemonNodeEnv(env);
    const bridgeConfig = loadBridgeConfig({ ...env, NODE_ENV: daemonNodeEnv });
    const bridgePort = bridgeConfig.port;
    const webPort = args.webPort ?? DEFAULT_WEB_PORT;
    // Idempotent: if a healthy daemon at our version is already running, just
    // report status (don't start a second one).
    const existing = await resolveDaemonStatus(BRIDGE_VERSION, healthProbe, env);
    if (existing.state === "running") {
        process.stdout.write(`✓ patchtrail already running — bridge ${bridgeBaseUrl(existing.record.bridgePort)}, web ${webBaseUrl(existing.record.webPort)}\n`);
        await emitUpdateNotice(args, env);
        return;
    }
    // A stale or version-mismatched daemon must be cleared/stopped first.
    if (existing.state === "version-mismatch" || existing.state === "stale") {
        if (existing.record.bridgePid) {
            await stopProcessTree(existing.record.bridgePid);
        }
        if (existing.record.webPid) {
            await stopProcessTree(existing.record.webPid);
        }
        clearDaemonRecord(env);
    }
    if (!vendoredWebExists()) {
        throw new LauncherError("The vendored frontend bundle (web/server.js) is missing from this install. Reinstall @patchtrail/patchtrail.");
    }
    const release = acquireStartLock(env);
    try {
        const identity = loadOrCreateInstallIdentity(env);
        const bridgeToken = loadOrCreateDurableBridgeToken(env);
        const dir = patchtrailAppDataDir(env);
        const logPath = path.join(dir, LAUNCHER_FILES.log);
        // Spawn the bridge daemon with the durable token injected (never argv).
        const bridgeDaemon = spawnDaemon(process.execPath, [resolveBridgeEntry()], {
            cwd: dir,
            logPath,
            env: {
                ...env,
                NODE_ENV: daemonNodeEnv,
                PATCHTRAIL_BRIDGE_HOST: LOOPBACK_HOST,
                PATCHTRAIL_BRIDGE_PORT: String(bridgePort),
                PATCHTRAIL_BRIDGE_CAPABILITY_TOKEN: bridgeToken,
            },
        });
        const bridgeReady = await waitForHttpOk(`${bridgeBaseUrl(bridgePort)}/health`);
        if (!bridgeReady) {
            await stopProcessTree(bridgeDaemon.pid);
            throw new LauncherError(`The bridge did not become healthy on ${bridgeBaseUrl(bridgePort)}. See ${logPath}.`);
        }
        // Spawn the vendored frontend daemon.
        const webDaemon = spawnDaemon(process.execPath, [resolveWebServerEntry()], {
            cwd: dir,
            logPath,
            env: {
                ...env,
                NODE_ENV: "production",
                HOSTNAME: LOOPBACK_HOST,
                PORT: String(webPort),
                // The frontend reaches the bridge over loopback from the browser; no
                // hosted backend in local-first.
                NEXT_PUBLIC_PATCHTRAIL_WEB_VERSION: BRIDGE_VERSION,
            },
        });
        const webReady = await waitForHttpOk(`${webBaseUrl(webPort)}/`);
        if (!webReady) {
            await stopProcessTree(bridgeDaemon.pid);
            await stopProcessTree(webDaemon.pid);
            throw new LauncherError(`The frontend did not become reachable on ${webBaseUrl(webPort)}. If the port is busy, retry with --web-port <n> (note: GitHub forge OAuth needs a matching registered callback for a non-default port). See ${logPath}.`);
        }
        const record = {
            bridgePid: bridgeDaemon.pid,
            webPid: webDaemon.pid,
            version: BRIDGE_VERSION,
            bridgePort,
            webPort,
            startedAt: new Date().toISOString(),
        };
        writeDaemonRecord(record, env);
        // Mint the one-time handoff in-process and open the browser argv-safely.
        const uiUrl = `${webBaseUrl(webPort)}/app`;
        const handoff = await mintLocalUiHandoff({
            bridgeUrl: bridgeBaseUrl(bridgePort),
            bridgeToken,
            uiUrl,
            workspaceId: identity.workspaceId,
            deviceId: identity.deviceId,
            userSessionId: identity.userSessionId,
            scopes: [...LAUNCHER_BASELINE_SCOPES],
        });
        process.stdout.write(`✓ patchtrail started — bridge ${bridgeBaseUrl(bridgePort)}, web ${webBaseUrl(webPort)}\n`);
        // Open the browser argv-safely via the loopback courier. We wait a bounded
        // window for the browser to pick up the redirect, then close the courier so
        // this CLI process can exit and return the terminal regardless. A
        // non-delivery (headless box / no browser) is NOT an error — the daemons
        // are up; we just print a manual URL hint.
        const courier = await openHandoffInBrowser(handoff.handoffUrl, {
            timeoutMs: 10_000,
        });
        const { delivered } = await courier.served;
        courier.close();
        if (delivered) {
            process.stdout.write("Opening PatchTrail in your browser…\n");
        }
        else {
            process.stdout.write(`If your browser did not open, visit ${uiUrl} from the machine running patchtrail.\n`);
        }
        await emitUpdateNotice(args, env);
    }
    finally {
        release();
    }
}
async function stopCommand(env) {
    const record = readDaemonRecord(env);
    if (!record) {
        process.stdout.write("patchtrail is not running.\n");
        return;
    }
    if (record.webPid)
        await stopProcessTree(record.webPid);
    if (record.bridgePid)
        await stopProcessTree(record.bridgePid);
    clearDaemonRecord(env);
    process.stdout.write("✓ patchtrail stopped.\n");
}
async function statusCommand(env) {
    const status = await resolveDaemonStatus(BRIDGE_VERSION, healthProbe, env);
    switch (status.state) {
        case "absent":
            process.stdout.write("patchtrail is not running.\n");
            return;
        case "stale":
            process.stdout.write(`patchtrail is not running (stale pidfile: ${status.reason}). Run \`patchtrail start\`.\n`);
            clearDaemonRecord(env);
            return;
        case "version-mismatch":
            process.stdout.write(`patchtrail is running an OLD version (${status.record.version}); installed is ${BRIDGE_VERSION}. Run \`patchtrail restart\`.\n`);
            return;
        case "running":
            process.stdout.write(`✓ patchtrail running (v${status.record.version}) — bridge ${bridgeBaseUrl(status.record.bridgePort)}, web ${webBaseUrl(status.record.webPort)}; started ${status.record.startedAt}\n`);
            return;
    }
}
async function updateCommand(env) {
    process.stdout.write("Updating patchtrail to the latest published version (atomic, lockstep bridge + frontend)…\n");
    // Stop any running daemon first so the new version starts clean.
    await stopCommand(env);
    await new Promise((resolve) => {
        const child = spawn("npm", ["install", "-g", "@patchtrail/patchtrail@latest"], { stdio: "inherit", shell: false });
        child.on("error", (error) => {
            process.stderr.write(`Update failed to launch npm: ${error.message}\n`);
            resolve();
        });
        child.on("exit", (code) => {
            if (code === 0) {
                process.stdout.write("✓ Updated. Run `patchtrail` to start the new version.\n");
            }
            else {
                process.stderr.write(`Update exited with code ${code ?? "unknown"}. You can retry \`patchtrail update\`.\n`);
            }
            resolve();
        });
    });
}
async function emitUpdateNotice(args, _env) {
    const result = await checkForUpdate({
        currentVersion: BRIDGE_VERSION,
        disabled: args.noUpdateCheck,
    });
    if (result.available) {
        process.stdout.write(`[33m⚠ You're on v${result.current}; v${result.latest} is available — run \`patchtrail update\` when convenient.[0m\n`);
    }
}
/**
 * The bridge mode (its NODE_ENV) for the local-first launcher.
 *
 * Local-first runs in "development" mode: that is the documented same-machine
 * run-intent issuer mode (localRunIntentMintRoute uses the development-fixture
 * issuer in dev/test), and it is the mode the whole e2e harness boots. The
 * bridge's "production" mode is for hosted-coordinated deployments and REQUIRES
 * real run-intent receipt-trust keys local-first does not have — so it is not
 * the launcher default. An explicit, valid NODE_ENV override is honored.
 */
function resolveDaemonNodeEnv(env) {
    const value = env.NODE_ENV;
    if (value === "production" || value === "development" || value === "test") {
        return value;
    }
    return "development";
}
async function healthProbe(record) {
    try {
        const response = await fetch(`${bridgeBaseUrl(record.bridgePort)}/health`, {
            cache: "no-store",
        });
        if (!response.ok)
            return false;
        const body = (await response.json());
        return body.service === "patchtrail-agent-bridge";
    }
    catch {
        return false;
    }
}
class LauncherError extends Error {
    constructor(message) {
        super(message);
        this.name = "LauncherError";
    }
}
function helpText() {
    return `Usage: patchtrail [command] [options]

Commands:
  start            Start the local bridge + frontend as a background daemon and
                   open PatchTrail in your browser (default).
  stop             Stop the running daemon.
  status           Show the running daemon's version, ports, and start time.
  restart          Stop then start (used after an update).
  update           Update to the latest published version (atomic, lockstep).

Options:
  --web-port <n>   Frontend loopback port (default ${DEFAULT_WEB_PORT}). A non-default port
                   needs a matching registered GitHub App OAuth callback for forge connect.
  --no-update-check  Skip the bounded, offline-safe "newer version available" check.
  --version, -v    Print the installed version.
  --help, -h       Show this help.

Local UI scopes minted by default: ${LAUNCHER_BASELINE_SCOPES.join(", ")}.
(debug:reveal and remote:control are granted only by explicit in-app action.)
All supported scopes: ${LOCAL_UI_SCOPES.join(", ")}.
`;
}
main(process.argv.slice(2), process.env).catch((error) => {
    if (error instanceof LauncherLockError || error instanceof LauncherError) {
        process.stderr.write(`${error.message}\n`);
        process.exitCode = 1;
        return;
    }
    process.stderr.write(`${error instanceof Error ? error.message : "Unexpected launcher error."}\n`);
    process.exitCode = 1;
});
