import { spawn } from "node:child_process";
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
import path from "node:path";
import { patchtrailAppDataDir } from "../config/appDataPath.js";
import { LAUNCHER_FILES } from "./launcherConstants.js";
function pidfilePath(env) {
    return path.join(patchtrailAppDataDir(env), LAUNCHER_FILES.pidfile);
}
export function readDaemonRecord(env = process.env) {
    try {
        const parsed = JSON.parse(readFileSync(pidfilePath(env), "utf8"));
        if (!isDaemonRecord(parsed))
            return undefined;
        return parsed;
    }
    catch {
        return undefined;
    }
}
export function writeDaemonRecord(record, env = process.env) {
    const dir = patchtrailAppDataDir(env);
    mkdirSync(dir, { recursive: true, mode: 0o700 });
    writeFileSync(path.join(dir, LAUNCHER_FILES.pidfile), `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
}
export function clearDaemonRecord(env = process.env) {
    try {
        rmSync(pidfilePath(env), { force: true });
    }
    catch {
        // best-effort
    }
}
/** True if a PID is alive (signal 0 probe). PID-reuse is disambiguated by the
 * caller via a /health identity probe — liveness alone is never trusted. */
export function isPidAlive(pid) {
    if (!Number.isInteger(pid) || pid <= 0)
        return false;
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        // ESRCH = no such process; EPERM = exists but not ours (still "alive").
        return error.code === "EPERM";
    }
}
/**
 * Resolves the true daemon state. A pidfile is never trusted on PID-liveness
 * alone (PID reuse): the caller supplies a /health probe that confirms the
 * process answering on the recorded port is actually our bridge at the recorded
 * version. "running but wrong version" is surfaced distinctly so `update` can
 * stop-old / start-new.
 */
export async function resolveDaemonStatus(currentVersion, probe, env = process.env) {
    const record = readDaemonRecord(env);
    if (!record)
        return { state: "absent" };
    if (!isPidAlive(record.bridgePid)) {
        return { state: "stale", record, reason: "dead" };
    }
    const healthy = await probe(record);
    if (!healthy) {
        // PID alive but not answering as our bridge → PID was reused / crashed.
        return { state: "stale", record, reason: "not-ours" };
    }
    if (record.version !== currentVersion) {
        return { state: "version-mismatch", record };
    }
    return { state: "running", record };
}
/**
 * Terminates a process tree cross-platform.
 *
 * POSIX: the daemon is spawned detached (its own process group), so signalling
 * the negative PID reaches the whole group — bridge plus any OpenCode children.
 * We try SIGTERM (graceful), then SIGKILL after a grace period.
 *
 * Windows: there is no POSIX signal/process-group kill, so we shell out to
 * `taskkill /T /F`, which terminates the PID and its descendants.
 */
export async function stopProcessTree(pid, options = {}) {
    const platform = options.platform ?? process.platform;
    const graceMs = options.graceMs ?? 4_000;
    const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
    if (!Number.isInteger(pid) || pid <= 0)
        return;
    if (platform === "win32") {
        await new Promise((resolve) => {
            const child = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
                stdio: "ignore",
                windowsHide: true,
            });
            child.on("error", () => resolve());
            child.on("exit", () => resolve());
        });
        return;
    }
    // POSIX: signal the process group (negative pid). Fall back to the bare pid
    // if the group signal fails (e.g. not a group leader).
    const signalGroup = (signal) => {
        try {
            process.kill(-pid, signal);
            return true;
        }
        catch {
            try {
                process.kill(pid, signal);
                return true;
            }
            catch {
                return false;
            }
        }
    };
    if (!signalGroup("SIGTERM"))
        return; // already gone
    await sleep(graceMs);
    if (isPidAlive(pid)) {
        signalGroup("SIGKILL");
    }
}
/**
 * Acquires an exclusive start lock so two concurrent `patchtrail start`
 * invocations cannot both try to bind the ports. Returns a release fn, or
 * throws if the lock is already held. `O_CREAT | O_EXCL` (the `wx` flag) is
 * atomic on POSIX and Windows.
 */
export function acquireStartLock(env = process.env) {
    const dir = patchtrailAppDataDir(env);
    mkdirSync(dir, { recursive: true, mode: 0o700 });
    const lockPath = path.join(dir, LAUNCHER_FILES.startLock);
    let fd;
    try {
        fd = openSync(lockPath, "wx", 0o600);
    }
    catch (error) {
        if (error.code === "EEXIST") {
            throw new LauncherLockError("Another `patchtrail start` is in progress. If this is wrong, remove the stale lock and retry.");
        }
        throw error;
    }
    try {
        writeFileSync(fd, `${process.pid}\n`);
    }
    catch {
        // non-fatal
    }
    let released = false;
    return () => {
        if (released)
            return;
        released = true;
        try {
            closeSync(fd);
        }
        catch {
            // ignore
        }
        try {
            rmSync(lockPath, { force: true });
        }
        catch {
            // ignore
        }
    };
}
export class LauncherLockError extends Error {
    constructor(message) {
        super(message);
        this.name = "LauncherLockError";
    }
}
function isDaemonRecord(value) {
    if (typeof value !== "object" || value === null)
        return false;
    const v = value;
    return (typeof v.bridgePid === "number" &&
        typeof v.webPid === "number" &&
        typeof v.version === "string" &&
        typeof v.bridgePort === "number" &&
        typeof v.webPort === "number" &&
        typeof v.startedAt === "string");
}
