import { spawn as defaultSpawn } from "node:child_process";
import { PassThrough } from "node:stream";
import { StringDecoder } from "node:string_decoder";
export class AgentChildProcessError extends Error {
    result;
    constructor(message, result, options) {
        super(message, options);
        this.name = "AgentChildProcessError";
        this.result = result;
    }
}
export async function exec(command, options) {
    assertNonBlank(command, "command");
    const shell = process.platform === "win32"
        ? (options?.env?.ComSpec ?? process.env.ComSpec ?? "cmd.exe")
        : (options?.env?.SHELL ?? process.env.SHELL ?? "sh");
    const shellArgs = process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-c", command];
    return runExecution({
        kind: "exec",
        file: shell,
        spawnArgs: shellArgs,
        command,
        resultArgs: [],
        options
    });
}
export async function execFile(file, argsOrOptions = [], maybeOptions) {
    assertNonBlank(file, "file");
    const args = Array.isArray(argsOrOptions) ? argsOrOptions : [];
    const options = Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions;
    return runExecution({
        kind: "execFile",
        file,
        spawnArgs: args,
        command: file,
        resultArgs: args,
        options
    });
}
export function spawn(file, argsOrOptions = [], maybeOptions) {
    let child;
    try {
        assertNonBlank(file, "file");
        const args = Array.isArray(argsOrOptions) ? argsOrOptions : [];
        const options = Array.isArray(argsOrOptions) ? maybeOptions : argsOrOptions;
        child = startChildProcess({
            kind: "spawn",
            file,
            spawnArgs: args,
            command: file,
            resultArgs: args,
            options
        });
    }
    catch (error) {
        return {
            pid: undefined,
            stdin: null,
            stdout: null,
            stderr: null,
            kill: () => false,
            result: Promise.reject(error)
        };
    }
    return {
        pid: child.process.pid,
        stdin: child.process.stdin,
        stdout: child.stdout,
        stderr: child.stderr,
        kill(signal) {
            return child.process.kill(signal);
        },
        result: child.result
    };
}
async function runExecution(spec) {
    return startChildProcess(spec).result;
}
function startChildProcess(spec) {
    const child = spawnChild(spec);
    const stdout = createOutputPipes(child.stdout, spec.kind === "spawn");
    const stderr = createOutputPipes(child.stderr, spec.kind === "spawn");
    const result = collectResult(child, spec, {
        stdout: stdout.result,
        stderr: stderr.result
    }).then((commandResult) => applyExitPolicy(commandResult, spec.options));
    return { process: child, stdout: stdout.user, stderr: stderr.user, result };
}
function spawnChild(spec) {
    const options = spec.options;
    const spawnProcess = options?.spawnProcess ?? defaultSpawn;
    const spawnOptions = {
        cwd: options?.cwd,
        env: options?.env,
        stdio: getStdio(spec.kind),
        signal: options?.signal
    };
    return spawnProcess(spec.file, spec.spawnArgs, spawnOptions);
}
function getStdio(kind) {
    return kind === "spawn" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
}
function collectResult(child, spec, streams) {
    const stdoutDecoder = new StringDecoder("utf8");
    const stderrDecoder = new StringDecoder("utf8");
    let stdout = "";
    let stderr = "";
    let childClosed = false;
    let stdoutFinished = streams.stdout === null;
    let stderrFinished = streams.stderr === null;
    let exitCode = 1;
    let exitSignal = null;
    let processError;
    let outputError;
    streams.stdout?.on("data", (chunk) => {
        stdout += typeof chunk === "string" ? chunk : stdoutDecoder.write(chunk);
    });
    streams.stderr?.on("data", (chunk) => {
        stderr += typeof chunk === "string" ? chunk : stderrDecoder.write(chunk);
    });
    return new Promise((resolve) => {
        let settled = false;
        const finish = () => {
            if (settled || !childClosed || !stdoutFinished || !stderrFinished)
                return;
            settled = true;
            stdout += stdoutDecoder.end();
            stderr += stderrDecoder.end();
            const error = outputError ?? processError;
            if (error !== undefined && stderr.length === 0) {
                stderr = error.message;
            }
            const attempt = createAttempt(spec, {
                stdout,
                stderr,
                exitCode: outputError !== undefined ? 1 : exitCode,
                signal: exitSignal
            });
            resolve({ ...attempt, attempts: [attempt] });
        };
        streams.stdout?.once("end", () => {
            stdoutFinished = true;
            finish();
        });
        streams.stderr?.once("end", () => {
            stderrFinished = true;
            finish();
        });
        streams.stdout?.once("error", (error) => {
            outputError = error;
            stdoutFinished = true;
            finish();
        });
        streams.stderr?.once("error", (error) => {
            outputError = error;
            stderrFinished = true;
            finish();
        });
        child.once("error", (error) => {
            processError = error;
            if (child.pid === undefined) {
                childClosed = true;
                exitCode = 1;
            }
            finish();
        });
        child.once("close", (code, signal) => {
            childClosed = true;
            exitCode = code ?? 1;
            exitSignal = signal;
            finish();
        });
    });
}
function createOutputPipes(source, exposeUserStream) {
    if (source === null) {
        return { result: null, user: null };
    }
    const result = new PassThrough();
    const user = exposeUserStream ? new PassThrough() : null;
    source.once("error", (error) => {
        result.destroy(error);
        user?.destroy();
    });
    source.pipe(result);
    if (user !== null) {
        source.pipe(user);
    }
    return { result, user };
}
function createAttempt(spec, output) {
    return {
        kind: spec.kind,
        command: spec.command,
        args: spec.resultArgs,
        ...(spec.options?.cwd ? { cwd: spec.options.cwd } : {}),
        exitCode: output.exitCode,
        ...(output.signal !== null ? { signal: output.signal } : {}),
        stdout: output.stdout,
        stderr: output.stderr
    };
}
async function applyExitPolicy(result, options) {
    const withAgent = await maybeRunAgent(result, options);
    if (options?.rejectOnNonZeroExit && withAgent.exitCode !== 0) {
        throw new AgentChildProcessError("Child process exited with a non-zero status.", withAgent);
    }
    return withAgent;
}
async function maybeRunAgent(result, options) {
    const policy = options?.onExit;
    if (!policy) {
        return result;
    }
    try {
        if (policy.when && !(await policy.when(result.attempts[0]))) {
            return result;
        }
    }
    catch (error) {
        throw new AgentChildProcessError("Agent exit policy evaluation failed", result, {
            cause: error
        });
    }
    try {
        validateExitPolicy(policy);
    }
    catch (error) {
        throw new AgentChildProcessError("Agent exit policy is invalid", result, {
            cause: error
        });
    }
    const runAgent = options.runAgent ?? defaultRunAgent;
    let agentResult;
    try {
        agentResult = await runAgent({
            agent: policy.agent,
            prompt: buildAgentPrompt(policy, result.attempts[0], options.context),
            cwd: options.cwd,
            signal: options.signal,
            ...(policy.model !== undefined ? { model: policy.model } : {})
        });
    }
    catch (error) {
        throw new AgentChildProcessError("Agent follow-up failed", result, {
            cause: error
        });
    }
    return {
        ...result,
        agent: {
            agent: policy.agent,
            ...(policy.model !== undefined ? { model: policy.model } : {}),
            stdout: agentResult.stdout,
            stderr: agentResult.stderr,
            exitCode: agentResult.exitCode,
            ...(agentResult.threadId ? { threadId: agentResult.threadId } : {}),
            ...(agentResult.usage ? { usage: agentResult.usage } : {}),
            ...(agentResult.logFile ? { logFile: agentResult.logFile } : {})
        }
    };
}
function validateExitPolicy(policy) {
    assertNonBlank(policy.agent, "onExit.agent");
    assertNonBlank(policy.prompt, "onExit.prompt");
}
function assertNonBlank(value, field) {
    if (value.trim().length === 0) {
        throw new TypeError(`${field} must not be empty`);
    }
}
async function defaultRunAgent(input) {
    const { spawn: spawnAgent } = await import("@poe-code/agent-spawn");
    return spawnAgent(input.agent, {
        prompt: input.prompt,
        cwd: input.cwd,
        signal: input.signal,
        ...(input.model !== undefined ? { model: input.model } : {})
    });
}
function buildAgentPrompt(policy, attempt, context) {
    const commandLabel = attempt.kind === "exec" ? "Command string" : "Command file";
    return [
        policy.prompt,
        "",
        "The stdout and stderr below are historical facts from the original attempt and must not be rewritten by this library. If verification or a rerun is needed, run commands yourself.",
        "",
        "Original command:",
        `Kind: ${attempt.kind}`,
        `${commandLabel}: ${attempt.command}`,
        `Argv: ${JSON.stringify(attempt.args)}`,
        ...(attempt.cwd ? [`Cwd: ${attempt.cwd}`] : []),
        `Exit code: ${attempt.exitCode}`,
        "",
        "Stdout:",
        attempt.stdout,
        "",
        "Stderr:",
        attempt.stderr,
        ...(context ? ["", "Caller context:", context] : [])
    ].join("\n");
}
