import { spawn } from "node:child_process";
import { createOutputEventCollector, } from "./commandOutputEvents.js";
import { prepareShellCommand, shellExecutable, } from "./shellCommandPreparation.js";
import { resolveClosedShellCommand, } from "./shellCommandResult.js";
import { createShellOutputDecoder, decodeShellOutputChunk, } from "./shellOutputDecoder.js";
const KILL_ESCALATION_DELAY_MS = 1000;
export async function runShellCommand(options) {
    const shell = shellExecutable();
    const prepared = await prepareShellCommand(options.commandText, options.workspaceRoot, shell);
    return new Promise((resolve, reject) => {
        const startedAt = new Date();
        const child = spawn(shell.command, [...shell.argsPrefix, prepared.commandText], {
            cwd: options.cwd,
            env: process.env,
            windowsVerbatimArguments: process.platform === "win32",
            windowsHide: true,
        });
        const outputEvents = createOutputEventCollector({
            cwd: options.cwd,
            commandLineCount: prepared.commandLineCount,
            commandFilePath: prepared.commandFilePath,
            onOutput: options.onOutput,
        });
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        let inactivityTimedOut = false;
        let killEscalationTimer;
        let inactivityTimer;
        const stdoutDecoder = createShellOutputDecoder();
        const stderrDecoder = createShellOutputDecoder();
        const clearTimers = () => {
            if (inactivityTimer)
                clearTimeout(inactivityTimer);
            if (killEscalationTimer)
                clearTimeout(killEscalationTimer);
            inactivityTimer = undefined;
            killEscalationTimer = undefined;
        };
        const escalateKill = () => {
            if (killEscalationTimer)
                return;
            killEscalationTimer = setTimeout(() => {
                if (!child.killed) {
                    child.kill("SIGKILL");
                }
            }, KILL_ESCALATION_DELAY_MS);
            killEscalationTimer.unref?.();
        };
        const timer = setTimeout(() => {
            timedOut = true;
            child.kill("SIGTERM");
            escalateKill();
        }, options.timeoutMs);
        timer.unref?.();
        const resetInactivityTimer = () => {
            if (inactivityTimer)
                clearTimeout(inactivityTimer);
            inactivityTimer = setTimeout(() => {
                inactivityTimedOut = true;
                timedOut = true;
                child.kill("SIGTERM");
                escalateKill();
            }, options.inactivityTimeoutMs);
            inactivityTimer.unref?.();
        };
        resetInactivityTimer();
        const appendStdout = (text) => {
            if (!text)
                return;
            stdout += text;
            outputEvents.push("stdout", text);
            resetInactivityTimer();
        };
        const appendStderr = (text) => {
            if (!text)
                return;
            stderr += text;
            outputEvents.push("stderr", text);
            resetInactivityTimer();
        };
        child.stdout?.on("data", (chunk) => {
            appendStdout(decodeShellOutputChunk(stdoutDecoder, chunk));
        });
        child.stderr?.on("data", (chunk) => {
            appendStderr(decodeShellOutputChunk(stderrDecoder, chunk));
        });
        child.on("error", (error) => {
            clearTimeout(timer);
            clearTimers();
            outputEvents.close();
            reject(error);
        });
        child.on("close", (exitCode, signal) => {
            clearTimeout(timer);
            clearTimers();
            appendStdout(decodeShellOutputChunk(stdoutDecoder, undefined, true));
            appendStderr(decodeShellOutputChunk(stderrDecoder, undefined, true));
            outputEvents.close();
            void resolveClosedShellCommand({
                exitCode,
                signal,
                stdout,
                stderr,
                startedAt,
                timedOut,
                inactivityTimedOut,
                prepared,
                workspaceRoot: options.workspaceRoot,
                cwd: options.cwd,
            }).then(resolve, reject);
        });
    });
}
