import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import crypto from 'node:crypto';
import { z } from 'zod';
import { truncateOutput } from './types.js';
const schema = z.object({
    command: z.string().min(1),
    timeout_ms: z.number().int().positive().max(600_000).optional(),
});
/**
 * A stateful bash session: cwd, environment variables, virtualenv
 * activations, etc. survive across tool calls. Commands are written to a
 * temp file and `source`d, so unbalanced quotes/braces in a command produce
 * a bash error instead of wedging the session. One command runs at a time;
 * a timeout kills and respawns the shell.
 */
class PersistentShell {
    initialCwd;
    child = null;
    chain = Promise.resolve();
    constructor(initialCwd) {
        this.initialCwd = initialCwd;
    }
    ensure() {
        if (this.child && this.child.exitCode === null && !this.child.killed)
            return this.child;
        this.child = spawn('bash', ['--noprofile', '--norc'], {
            cwd: this.initialCwd,
            env: process.env,
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        this.child.on('error', () => {
            this.child = null;
        });
        return this.child;
    }
    exec(command, timeoutMs) {
        // Serialize commands; parallel writes to one shell would interleave output.
        const run = this.chain.then(() => this.execNow(command, timeoutMs));
        this.chain = run.catch(() => undefined);
        return run;
    }
    execNow(command, timeoutMs) {
        const child = this.ensure();
        const sentinel = `__CHEAPCODE_DONE_${crypto.randomBytes(8).toString('hex')}__`;
        const scriptFile = path.join(os.tmpdir(), `cheapcode-cmd-${crypto.randomBytes(6).toString('hex')}.sh`);
        fs.writeFileSync(scriptFile, command);
        return new Promise((resolve) => {
            let out = '';
            let settled = false;
            const finish = (result) => {
                if (settled)
                    return;
                settled = true;
                clearTimeout(timer);
                child.stdout.off('data', onData);
                child.stderr.off('data', onData);
                child.off('close', onClose);
                try {
                    fs.unlinkSync(scriptFile);
                }
                catch {
                    /* already gone */
                }
                resolve(result);
            };
            const checkDone = () => {
                const m = out.match(new RegExp(`${sentinel} (\\d+)\\n?$`));
                if (m) {
                    const body = truncateOutput(out.slice(0, m.index).trimEnd());
                    finish(`[exit code ${m[1]}]\n${body}`);
                }
            };
            const onData = (d) => {
                out += d.toString();
                checkDone();
            };
            const onClose = () => {
                this.child = null;
                finish(`[shell exited unexpectedly]\n${truncateOutput(out.trimEnd())}`);
            };
            const timer = setTimeout(() => {
                try {
                    child.kill('SIGKILL');
                }
                catch {
                    /* already dead */
                }
                this.child = null; // respawn fresh on next call
                finish(`[timed out after ${timeoutMs}ms; shell was reset, cwd/env changes from this command are lost]\n${truncateOutput(out.trimEnd())}`);
            }, timeoutMs);
            child.stdout.on('data', onData);
            child.stderr.on('data', onData);
            child.on('close', onClose);
            child.stdin.write(`source ${JSON.stringify(scriptFile)} 2>&1\nprintf '\\n%s %d\\n' '${sentinel}' $?\n`);
        });
    }
    dispose() {
        try {
            this.child?.kill();
        }
        catch {
            /* already dead */
        }
        this.child = null;
    }
}
export function createBashTool(cwd) {
    const shell = new PersistentShell(cwd);
    return {
        name: 'bash',
        description: 'Execute a shell command in a PERSISTENT bash session: cwd, exports, and activated ' +
            'environments carry over to your next bash call. Returns combined stdout/stderr. ' +
            'Default timeout 120s (override with timeout_ms, max 600000); a timeout resets the session.',
        schema,
        jsonSchema: {
            type: 'object',
            properties: {
                command: { type: 'string', description: 'The shell command to execute' },
                timeout_ms: { type: 'number', description: 'Timeout in milliseconds (default 120000)' },
            },
            required: ['command'],
        },
        readOnly: false,
        summarize: (i) => (i.command.length > 80 ? i.command.slice(0, 77) + '...' : i.command),
        run: (input) => shell.exec(input.command, input.timeout_ms ?? 120_000),
    };
}
