import { spawn } from "node:child_process";
import { StringDecoder } from "node:string_decoder";
const MAX_OUTPUT_BYTES = 50 * 1024;
export async function runCustomTool(tool, input, cwd, signal) {
    if (typeof input !== "string")
        throw new Error(`${tool.name} expects a string input`);
    if (signal?.aborted)
        throw new Error(`${tool.name} aborted`);
    const args = tool.input === "arg" ? [...tool.args, input] : [...tool.args];
    return new Promise((resolve, reject) => {
        const child = spawn(tool.command, args, {
            cwd,
            shell: false,
            detached: process.platform !== "win32",
            stdio: [tool.input === "stdin" ? "pipe" : "ignore", "pipe", "pipe"],
        });
        let stdout = "";
        let stderr = "";
        let bytes = 0;
        const stdoutDecoder = new StringDecoder("utf8");
        const stderrDecoder = new StringDecoder("utf8");
        let settled = false;
        const kill = () => {
            try {
                if (process.platform !== "win32" && child.pid)
                    process.kill(-child.pid, "SIGKILL");
                else
                    child.kill("SIGKILL");
            }
            catch {
                child.kill();
            }
        };
        const finish = (callback) => {
            if (settled)
                return;
            settled = true;
            signal?.removeEventListener("abort", onAbort);
            callback();
        };
        const append = (target, chunk) => {
            const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
            bytes += buffer.length;
            if (bytes > MAX_OUTPUT_BYTES) {
                kill();
                finish(() => reject(new Error(`${tool.name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
                return;
            }
            if (target === "stdout")
                stdout += stdoutDecoder.write(buffer);
            else
                stderr += stderrDecoder.write(buffer);
        };
        const onAbort = () => {
            kill();
            finish(() => reject(new Error(`${tool.name} aborted`)));
        };
        child.stdout?.on("data", (chunk) => append("stdout", chunk));
        child.stderr?.on("data", (chunk) => append("stderr", chunk));
        child.on("error", (error) => finish(() => reject(error)));
        child.on("close", (code) => finish(() => {
            stdout += stdoutDecoder.end();
            stderr += stderrDecoder.end();
            if (code !== 0) {
                reject(new Error(`${tool.name} exited with code ${code ?? "unknown"}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
                return;
            }
            resolve(stdout.trimEnd() || stderr.trimEnd() || "(no output)");
        }));
        signal?.addEventListener("abort", onAbort, { once: true });
        if (tool.input === "stdin")
            child.stdin?.end(input);
    });
}
