import { exec, execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { logger } from "../logger.js";
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
const PACKAGE_NAME = "visionclaw";
const RESTART_EXIT_CODE = 42;
export function isElectronChild() {
    return process.env.VISIONCLAW_ELECTRON === "1";
}
export function detectProcessManager() {
    if (isElectronChild()) {
        return "electron";
    }
    if (process.env.pm_id !== undefined || process.env.PM2_HOME !== undefined) {
        return "pm2";
    }
    // systemd is Linux-only — skip on Windows
    if (process.platform !== "win32" &&
        (process.env.INVOCATION_ID !== undefined ||
            process.env.SYSTEMD_EXEC_PID !== undefined)) {
        return "systemd";
    }
    return null;
}
async function restartViaProcessManager(pm, options) {
    if (pm === "electron") {
        logger.system("Exiting with restart code — Electron host will respawn the agent.");
        process.exit(RESTART_EXIT_CODE);
    }
    if (pm === "pm2") {
        // Best-effort: refresh the PM2 ecosystem file so `pm2 list` shows the
        // freshly installed version after a self-upgrade, then reload from it.
        // Falls back to a plain restart if the ecosystem file is unavailable
        // (e.g. legacy installs that started pm2 directly without an ecosystem
        // config, or non-darwin deployments where the daemon command never ran).
        let refreshed = false;
        let ecosystemPath = null;
        try {
            const daemon = await import("../service/daemon.js");
            refreshed = daemon.refreshPm2EcosystemVersion(options.pm2Version);
            if (refreshed)
                ecosystemPath = daemon.getEcosystemPath();
        }
        catch (err) {
            // Importing service/daemon should not fail under normal conditions;
            // if it does, we still want to attempt a restart rather than abort.
            logger.warn(`Could not load daemon module for ecosystem refresh: ${err instanceof Error ? err.message : String(err)}`);
        }
        if (refreshed && ecosystemPath) {
            // `--update-env` is honored on reload, and `startOrReload` re-reads the
            // ecosystem file's env block on (re)start, so this works in both paths.
            const { stdout, stderr } = await execFileAsync("pm2", [
                "startOrReload",
                ecosystemPath,
                "--update-env",
            ]);
            return (stdout + stderr).trim();
        }
        const pmId = process.env.pm_id ?? process.env.name ?? PACKAGE_NAME;
        const { stdout, stderr } = await execFileAsync("pm2", ["restart", pmId, "--update-env"]);
        return (stdout + stderr).trim();
    }
    const { stdout, stderr } = await execAsync(`systemctl restart ${PACKAGE_NAME}`);
    return (stdout + stderr).trim();
}
/**
 * Restart the current process.
 *
 * If running under a process manager (pm2 / systemd), delegates to it.
 * Otherwise replaces the current process via execFileSync (Unix exec
 * semantics — the new process inherits the same PID, stdio, and
 * controlling terminal).
 *
 * @param delayMs - delay before restarting (gives in-flight I/O time to flush)
 * @param onBeforeRestart - optional async callback invoked just before the
 *   process exits; used for graceful shutdown (stop channels, flush queues).
 *
 * NOTE: This function schedules the restart and returns immediately.
 * The process will exit/be replaced after `delayMs`.
 */
export function scheduleRestart(delayMs = 5_000, onBeforeRestart, options = {}) {
    const pm = detectProcessManager();
    if (pm) {
        logger.system(`Restarting via ${pm} in ${delayMs / 1000}s...`);
        setTimeout(() => {
            const doRestart = async () => {
                if (onBeforeRestart)
                    await onBeforeRestart();
                try {
                    await restartViaProcessManager(pm, options);
                }
                catch (err) {
                    // If the process manager restart fails (e.g. PM2 daemon refuses,
                    // restart procedure is interrupted, app id drifted), we are now
                    // in a half-shutdown state: channels stopped, pollers stopped,
                    // but the process is still alive. Leaving it that way leads to
                    // confusing "statement has been finalized" / dead-channel errors
                    // hours later. Force-exit instead so the process manager can
                    // respawn us cleanly via its normal autorestart path.
                    logger.err(`Restart via ${pm} failed, force-exiting so ${pm} can respawn: ${err instanceof Error ? err.message : String(err)}`);
                    process.exit(1);
                }
            };
            doRestart().catch((err) => {
                logger.err(`Restart via ${pm} failed: ${err instanceof Error ? err.message : String(err)}`);
            });
        }, delayMs);
        return;
    }
    logger.system(`Restarting process in ${delayMs / 1000}s...`);
    if (process.platform === "win32") {
        logger.system("Tip: For reliable restart support on Windows, run under pm2.");
    }
    setTimeout(() => {
        const doRestart = async () => {
            if (onBeforeRestart)
                await onBeforeRestart();
            const entryScript = process.argv[1];
            const args = process.argv.slice(1);
            // In dev mode the entry point is a .ts file run via tsx.
            // process.execPath is always "node", so we need to use tsx instead.
            const isTsSource = entryScript.endsWith(".ts");
            const bin = isTsSource ? "tsx" : process.execPath;
            logger.system(`Re-exec: ${bin} ${args.join(" ")}`);
            if (isTsSource) {
                // In dev mode (tsx), we cannot reliably replace the current process:
                // execFileSync blocks the parent (stale PID lock), while spawn creates
                // an orphaned child that escapes Ctrl+C. Instead, just exit cleanly
                // and let the developer re-run `pnpm dev` manually.
                logger.system("Dev mode detected — exiting. Please re-run `pnpm dev` to restart.");
                process.exit(0);
            }
            // Production (compiled JS): use execFileSync for true exec-like replacement.
            // execFileSync never returns on success — the current process image is replaced.
            try {
                execFileSync(bin, args, {
                    stdio: "inherit",
                    env: process.env,
                });
            }
            catch {
                // execFileSync throws when the child exits (even with code 0).
                // The replacement process has already run to completion at this point.
            }
            process.exit(0);
        };
        doRestart().catch((err) => {
            logger.err(`Restart failed: ${err instanceof Error ? err.message : String(err)}`);
            process.exit(1);
        });
    }, delayMs);
}
//# sourceMappingURL=restart.js.map