"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandError = void 0;
exports.commandExists = commandExists;
exports.commandExistsSync = commandExistsSync;
exports.runCommand = runCommand;
exports.runStreamingCommand = runStreamingCommand;
const node_child_process_1 = require("node:child_process");
const branding_1 = require("./branding");
const catalog_1 = require("../i18n/catalog");
const git_command_env_1 = require("./git-command-env");
class CommandError extends Error {
    command;
    args;
    cwd;
    stdout;
    stderr;
    exitCode;
    signal;
    timedOut;
    timeoutKind;
    timeoutMs;
    durationMs;
    startedAt;
    endedAt;
    lastOutputAt;
    locale;
    constructor(options, result) {
        const rendered = result.timedOut
            ? options.command
            : [options.command, ...(options.args ?? [])].join(" ");
        const locale = options.locale ?? catalog_1.DEFAULT_APP_LOCALE;
        super((0, catalog_1.translate)(locale, "command.error.failed", {
            exitCode: result.exitCode,
            command: rendered,
        }));
        this.name = "CommandError";
        this.command = options.command;
        this.args = options.args ?? [];
        this.cwd = options.cwd;
        this.stdout = result.stdout;
        this.stderr = result.stderr;
        this.exitCode = result.exitCode;
        this.signal = result.signal ?? null;
        this.timedOut = result.timedOut ?? false;
        this.timeoutKind = result.timeoutKind ?? null;
        this.timeoutMs = result.timeoutMs;
        this.durationMs = result.durationMs;
        this.startedAt = result.startedAt;
        this.endedAt = result.endedAt;
        this.lastOutputAt = result.lastOutputAt;
        this.locale = locale;
    }
}
exports.CommandError = CommandError;
async function commandExists(command) {
    try {
        await runCommand({
            command: "bash",
            args: ["-lc", `command -v ${escapeShellWord(command)}`],
            allowNonZeroExit: false,
            timeoutMs: 5_000,
        });
        return true;
    }
    catch {
        return false;
    }
}
function commandExistsSync(command) {
    try {
        (0, node_child_process_1.execFileSync)("bash", ["-lc", `command -v ${escapeShellWord(command)}`], {
            stdio: "ignore",
            timeout: 5_000,
        });
        return true;
    }
    catch {
        return false;
    }
}
async function runCommand(options) {
    return runStreamingCommand(options);
}
async function runStreamingCommand(options, handlers = {}) {
    const args = options.args ?? [];
    const timeoutMs = options.timeoutMs ?? 60_000;
    const noOutputTimeoutMs = options.noOutputTimeoutMs ?? null;
    const startedAtMs = Date.now();
    const startedAt = new Date(startedAtMs).toISOString();
    return new Promise((resolve, reject) => {
        const child = (0, node_child_process_1.execFile)(options.command, args, {
            cwd: options.cwd,
            env: buildCommandEnv(options),
            maxBuffer: 10 * 1024 * 1024,
        });
        let stdout = "";
        let stderr = "";
        let timedOut = false;
        let timeoutKind = null;
        let activeTimeoutMs;
        let lastOutputAtMs = null;
        let killTimer;
        let idleTimer;
        const requestTimeout = (kind, triggeredTimeoutMs) => {
            if (timedOut) {
                return;
            }
            timedOut = true;
            timeoutKind = kind;
            activeTimeoutMs = triggeredTimeoutMs;
            child.kill("SIGTERM");
            killTimer = setTimeout(() => {
                child.kill("SIGKILL");
            }, 5_000);
            killTimer.unref();
        };
        const timeoutTimer = timeoutMs === 0
            ? undefined
            : setTimeout(() => {
                requestTimeout("total", timeoutMs);
            }, timeoutMs);
        timeoutTimer?.unref();
        const clearTimeouts = () => {
            if (timeoutTimer) {
                clearTimeout(timeoutTimer);
            }
            if (idleTimer) {
                clearTimeout(idleTimer);
            }
            if (killTimer) {
                clearTimeout(killTimer);
            }
        };
        const resetIdleTimer = () => {
            if (idleTimer) {
                clearTimeout(idleTimer);
                idleTimer = undefined;
            }
            if (noOutputTimeoutMs === null || noOutputTimeoutMs === 0 || timedOut) {
                return;
            }
            idleTimer = setTimeout(() => {
                requestTimeout("idle", noOutputTimeoutMs);
            }, noOutputTimeoutMs);
            idleTimer.unref();
        };
        const appendStderr = (chunk) => {
            stderr += chunk;
            lastOutputAtMs = Date.now();
            resetIdleTimer();
            handlers.onStderrChunk?.(chunk);
        };
        resetIdleTimer();
        child.stdout?.setEncoding("utf8");
        child.stderr?.setEncoding("utf8");
        child.stdout?.on("data", (chunk) => {
            stdout += chunk;
            lastOutputAtMs = Date.now();
            resetIdleTimer();
            handlers.onStdoutChunk?.(chunk);
        });
        child.stderr?.on("data", (chunk) => {
            appendStderr(chunk);
        });
        child.on("error", (error) => {
            clearTimeouts();
            reject(error);
        });
        child.on("close", (code, signal) => {
            clearTimeouts();
            const endedAtMs = Date.now();
            const outputLastSeenAt = lastOutputAtMs === null ? null : new Date(lastOutputAtMs).toISOString();
            if (timedOut) {
                const diagnostic = formatCommandDiagnostic(timeoutKind === "idle" ? "idle_timed_out" : "timed_out", {
                    command: options.command,
                    exitCode: code,
                    signal,
                    timeoutKind,
                    timeoutMs: activeTimeoutMs ?? timeoutMs,
                    durationMs: endedAtMs - startedAtMs,
                    stdout,
                    stderr,
                    lastOutputAt: outputLastSeenAt,
                });
                stderr += diagnostic;
                handlers.onStderrChunk?.(diagnostic);
            }
            const result = {
                stdout,
                stderr,
                exitCode: timedOut ? 124 : (code ?? 1),
                signal: signal ?? null,
                timedOut,
                timeoutKind,
                timeoutMs: activeTimeoutMs ?? timeoutMs,
                durationMs: endedAtMs - startedAtMs,
                startedAt,
                endedAt: new Date(endedAtMs).toISOString(),
                lastOutputAt: outputLastSeenAt,
            };
            if (result.exitCode !== 0 && !options.allowNonZeroExit) {
                reject(new CommandError(options, result));
                return;
            }
            resolve(result);
        });
        if (options.input) {
            child.stdin?.write(options.input);
        }
        child.stdin?.end();
    });
}
function formatCommandDiagnostic(status, input) {
    return `${[
        `[${branding_1.CLI_COMMAND_NAME}] command ${status}`,
        `command=${input.command}`,
        `exitCode=${input.exitCode ?? "null"}`,
        `signal=${input.signal ?? "null"}`,
        `durationMs=${input.durationMs}`,
        `timeoutKind=${input.timeoutKind ?? "unknown"}`,
        `timeoutMs=${input.timeoutMs}`,
        `stdoutBytes=${Buffer.byteLength(input.stdout, "utf8")}`,
        `stderrBytes=${Buffer.byteLength(input.stderr, "utf8")}`,
        `lastOutputAt=${input.lastOutputAt ?? "none"}`,
    ].join(" ")}\n`;
}
function buildCommandEnv(options) {
    if (!isGitCommand(options.command)) {
        return options.env;
    }
    return (0, git_command_env_1.buildGitCommandEnv)(options.env);
}
function isGitCommand(command) {
    const normalized = command.replaceAll("\\", "/");
    return normalized === "git" || normalized.endsWith("/git");
}
function escapeShellWord(value) {
    return `'${value.replaceAll("'", "'\\''")}'`;
}
