import { spawn } from "node:child_process";
import { constants } from "node:os";
import { setTimeout as delay } from "node:timers/promises";
import { hasOwnErrorCode } from "./error-codes.js";
const TERMINATION_GRACE_MS = 1_000;
const PROCESS_GROUP_POLL_MS = 25;
const PROCESS_GROUP_FINAL_WAIT_MS = 500;
export function runCommand(command, args, inputOptions) {
    return new Promise((resolve) => {
        const options = normalizeCommandRunnerOptions(inputOptions);
        if (options?.signal?.aborted === true) {
            resolve({
                stdout: "",
                stderr: "Command aborted before start.",
                exitCode: 130,
                aborted: true
            });
            return;
        }
        const hasStdin = options?.stdin != null;
        const timeoutMs = options?.timeoutMs;
        const hasTimeout = typeof timeoutMs === "number" && timeoutMs > 0;
        const canAbort = options?.signal !== undefined;
        const killProcessGroup = process.platform !== "win32" && (hasTimeout || canAbort);
        const child = spawn(command, args, createNullRecord({
            stdio: [hasStdin ? "pipe" : "ignore", "pipe", "pipe"],
            cwd: options?.cwd,
            env: options?.env
                ? {
                    ...process.env,
                    ...options.env
                }
                : undefined,
            ...(killProcessGroup ? { detached: true } : {})
        }));
        if (killProcessGroup) {
            child.unref();
        }
        let stdout = "";
        let stderr = "";
        let settled = false;
        let timedOut = false;
        let aborted = false;
        let terminationMessage;
        let timeout;
        let escalationTimeout;
        const cleanup = () => {
            if (timeout !== undefined) {
                clearTimeout(timeout);
                timeout = undefined;
            }
            if (escalationTimeout !== undefined) {
                clearTimeout(escalationTimeout);
                escalationTimeout = undefined;
            }
            options?.signal?.removeEventListener("abort", abortCommand);
        };
        const finish = (result) => {
            if (settled) {
                return;
            }
            settled = true;
            cleanup();
            resolve(result);
        };
        const terminate = (reason) => {
            if (settled || timedOut || aborted) {
                return;
            }
            timedOut = reason === "timeout";
            aborted = reason === "abort";
            terminationMessage =
                reason === "timeout"
                    ? `Command timed out after ${timeoutMs} ms.`
                    : "Command aborted.";
            killChild("SIGTERM");
            escalationTimeout = setTimeout(() => {
                killChild("SIGKILL");
            }, TERMINATION_GRACE_MS);
        };
        const killChild = (signal) => {
            if (killProcessGroup && typeof child.pid === "number" && child.pid > 0) {
                try {
                    process.kill(-child.pid, signal);
                    return;
                }
                catch {
                    // Fall back to the direct child when the process group is already gone or unavailable.
                }
            }
            child.kill(signal);
        };
        function abortCommand() {
            terminate("abort");
        }
        if (hasTimeout) {
            timeout = setTimeout(() => terminate("timeout"), timeoutMs);
        }
        options?.signal?.addEventListener("abort", abortCommand, { once: true });
        if (hasStdin && child.stdin) {
            child.stdin.on("error", () => { });
            child.stdin.end(options.stdin);
        }
        child.stdout?.setEncoding("utf8");
        child.stdout?.on("data", (chunk) => {
            stdout += chunk.toString();
        });
        child.stderr?.setEncoding("utf8");
        child.stderr?.on("data", (chunk) => {
            stderr += chunk.toString();
        });
        child.on("error", (error) => {
            const ownCode = Object.prototype.hasOwnProperty.call(error, "code")
                ? error.code
                : undefined;
            const exitCode = typeof ownCode === "number"
                ? ownCode
                : typeof error.errno === "number"
                    ? error.errno
                    : 127;
            const message = error instanceof Error ? error.message : String(error ?? "error");
            finish({
                stdout,
                stderr: stderr ? `${stderr}${message}` : message,
                exitCode
            });
        });
        child.on("close", (code, signal) => {
            void (async () => {
                if ((timedOut || aborted) && killProcessGroup && typeof child.pid === "number") {
                    await waitForProcessGroupExit(child.pid);
                }
                const exitCode = timedOut ? 124 : aborted ? 130 : code ?? signalExitCode(signal);
                const finalStderr = terminationMessage === undefined
                    ? stderr
                    : stderr
                        ? `${stderr}${terminationMessage}`
                        : terminationMessage;
                finish({
                    stdout,
                    stderr: finalStderr,
                    exitCode,
                    ...(timedOut ? { timedOut: true } : {}),
                    ...(aborted ? { aborted: true } : {})
                });
            })();
        });
    });
}
function normalizeCommandRunnerOptions(options) {
    if (options === undefined) {
        return createNullRecord({});
    }
    return createNullRecord({
        ...optionalOwnProperty(options, "cwd"),
        ...optionalOwnProperty(options, "env"),
        ...optionalOwnProperty(options, "stdin"),
        ...optionalOwnProperty(options, "timeoutMs"),
        ...optionalOwnProperty(options, "signal")
    });
}
function optionalOwnProperty(options, name) {
    const value = getOwnProperty(options, name);
    return value === undefined ? {} : { [name]: value };
}
function getOwnProperty(value, name) {
    return hasOwnProperty(value, name) ? value[name] : undefined;
}
function hasOwnProperty(value, name) {
    return Object.prototype.hasOwnProperty.call(value, name);
}
function createNullRecord(value) {
    return Object.assign(Object.create(null), value);
}
async function waitForProcessGroupExit(pid) {
    const deadline = Date.now() + TERMINATION_GRACE_MS + PROCESS_GROUP_FINAL_WAIT_MS;
    while (Date.now() < deadline) {
        if (!isProcessGroupAlive(pid)) {
            return;
        }
        await delay(PROCESS_GROUP_POLL_MS);
    }
}
function isProcessGroupAlive(pid) {
    try {
        process.kill(-pid, 0);
        return true;
    }
    catch (error) {
        return !isNoSuchProcess(error);
    }
}
function isNoSuchProcess(error) {
    return hasOwnErrorCode(error, "ESRCH");
}
function signalExitCode(signal) {
    const signalNumber = signal ? constants.signals[signal] : undefined;
    return typeof signalNumber === "number" ? 128 + signalNumber : 1;
}
