import { Type } from '@sinclair/typebox';
import { spawn } from 'child_process';
import { randomBytes } from 'node:crypto';
import { createWriteStream, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import stripAnsi from 'strip-ansi';
import { ToolExecutionError } from '../../core/tool-execution-error.js';
import { manageChildProcess } from '../../shared/process/lifecycle.js';
import { BASH } from '../../tool-names.js';
import { ABORT_AGENT_MESSAGE } from '../../utils/abort.js';
import { DEFAULT_MAX_CHARS } from '../../utils/truncate.js';
import { requirePermissionPolicy } from '../permissions/require-policy.js';
import { getShellConfig, getShellEnv, sanitizeBinaryOutput } from './bash-runtime.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const TOOL_DESCRIPTION_BASE = `
Execute a bash command in the current working directory. Returns stdout and stderr. 
Output is truncated to ${DEFAULT_MAX_CHARS} characters. If truncated, full output is saved to a temp file. 
For external APIs, prefer Python packages over raw curl (e.g. yfinance for market data). pip install if missing.
Default timeout is 30 seconds. Override with the timeout parameter for long-running commands.
`;
const WINDOWS_SECTION = `
Python and pip are available for data processing, scripting, and package installation.
- ALWAYS use bash to execute python code instead going through the excel COM interface -- that channel reserved for Excel communication
- Use python / python3 (not via an absolute path) to invoke Python.

On Windows, this tool runs Git Bash (MSYS2), not a native shell. Two consequences:
- Always use forward slashes in file paths (e.g., C:/Users/peter). Backslashes are escape characters in bash strings.
- Forward-slash flags get auto-converted to paths — use double slashes for literal flags (e.g., reg query //v).
- If bash commands fail on Windows, you may resort to native cmd.exe or PowerShell commands, but notify the user first.
- This tool holds stdout/stderr pipes open until all child processes exit. To launch a process without blocking, use Git Bash's built-in \`start\` with the program name (e.g., \`start excel\`).
`;
const MAC_SECTION = `
Python 3 and pip are available for data processing, scripting, and package installation.
- ALWAYS use bash to execute python code — the excel_exec channel is reserved for spreadsheet operations.
- Use python3 (not python or an absolute path) to invoke Python.
- Use \`uv pip install <pkg>\` to install packages. Packages persist across commands.`;
const SANDBOX_SECTION = `
Commands run in an isolated Linux sandbox (WSL2 + bubblewrap) with restricted filesystem access.
- Use Linux paths: the Windows drive C:\\ is mounted at /mnt/c/ (e.g., C:\\Users\\jane → /mnt/c/Users/jane)
- The working directory is always writable. The user's home directory is readable.
- File tools (Read, Edit, Glob, Grep) still use Windows paths — only bash uses Linux paths
- Network access is controlled by sandbox settings — if a command fails due to network, tell the user to enable it in settings

**Filesystem access**: Read and write mounts are configured in the sandbox section of the agent settings file. If you get "Read-only file system" or "No such file or directory" for a path that should exist, check the sandbox settings and tell the user — they can add paths via /sandbox settings.

Python 3 and pip are available for data processing, scripting, and package installation.
- \`pip install <pkg>\` works — packages are installed to a persistent user directory that survives across commands.
- ALWAYS use bash to execute python and other commands.
- Use python (not python3 or an absolute path) to invoke Python.`;
const CONVENTIONS = `
**Conventions:**
- When creating files, tell users the file path.
- Stop retrying after 1-2 unexplained failures.`;
function getToolDescription() {
    if (process.platform === 'win32') {
        return TOOL_DESCRIPTION_BASE + WINDOWS_SECTION + CONVENTIONS;
    }
    return TOOL_DESCRIPTION_BASE + MAC_SECTION + CONVENTIONS;
}
const TOOL_DESCRIPTION_SANDBOX = TOOL_DESCRIPTION_BASE + SANDBOX_SECTION + CONVENTIONS;
// ---------------------------------------------------------------------------
/**
 * Generate a unique temp file path for bash output
 */
function getTempFilePath() {
    const id = randomBytes(8).toString('hex');
    return join(tmpdir(), `pi-bash-${id}.log`);
}
const bashSchema = Type.Object({
    description: Type.String({
        description: 'A short description of what the command does for non-technical users.'
    }),
    command: Type.String({ description: 'Bash command to execute' }),
    timeout: Type.Optional(Type.Number({
        description: 'Timeout in seconds (default: 30). Increase for long-running commands.'
    })),
    connections: Type.Optional(Type.Array(Type.String(), {
        description: 'Names of configured connections whose credentials should be injected as environment variables for this command. ' +
            'Triggers elevated approval. Field names are used directly as env var keys (e.g. a field named GITHUB_TOKEN becomes $GITHUB_TOKEN).'
    }))
});
function buildHeadTruncation(headOutput, totalChars, maxChars = DEFAULT_MAX_CHARS) {
    if (totalChars <= maxChars) {
        return {
            content: headOutput,
            truncated: false,
            totalChars,
            outputChars: headOutput.length,
            maxChars
        };
    }
    const cutPoint = headOutput.lastIndexOf('\n', maxChars);
    const content = cutPoint > 0 ? headOutput.slice(0, cutPoint) : headOutput.slice(0, maxChars);
    return {
        content,
        truncated: true,
        totalChars,
        outputChars: content.length,
        maxChars
    };
}
// ---------------------------------------------------------------------------
// Semantic exit code interpretation
// ---------------------------------------------------------------------------
/**
 * Translate a non-zero exit code into a human-readable hint appended to tool output.
 * Returns `undefined` when no useful interpretation exists.
 */
export function interpretExitCode(exitCode, command) {
    const baseCommand = command.trim().split(/\s+/)[0];
    if (exitCode === 1) {
        if (['grep', 'rg', 'ag', 'ack'].includes(baseCommand)) {
            return 'No matches found (this is normal, not an error)';
        }
        if (['diff', 'cmp'].includes(baseCommand)) {
            return 'Files differ';
        }
        if (baseCommand === 'test' || baseCommand === '[') {
            return 'Test condition is false';
        }
    }
    switch (exitCode) {
        case 2:
            return 'Invalid arguments or misuse of command';
        case 126:
            return 'Permission denied — cannot execute';
        case 127:
            return 'Command not found — check spelling or install the program';
        case 128:
            return 'Invalid exit argument';
        case 130:
            return 'Interrupted (SIGINT)';
        case 137:
            return 'Killed (SIGKILL) — likely out of memory';
        case 139:
            return 'Segmentation fault (SIGSEGV)';
        case 143:
            return 'Terminated (SIGTERM)';
        default:
            return exitCode > 128 ? `Killed by signal ${exitCode - 128}` : undefined;
    }
}
/**
 * Returns `true` when a non-zero exit code represents an expected/informational
 * result rather than an error. These exits resolve (not reject) so the agent
 * loop does not mark them as `isError = true` and they receive the full 50K
 * char output limit instead of the 500 char error limit.
 */
export function isInformationalExitCode(exitCode, command) {
    const baseCommand = command.trim().split(/\s+/)[0];
    if (exitCode === 1 && ['grep', 'rg', 'ag', 'ack'].includes(baseCommand))
        return true;
    if (exitCode === 1 && ['diff', 'cmp'].includes(baseCommand))
        return true;
    if (exitCode === 1 && (baseCommand === 'test' || baseCommand === '['))
        return true;
    return false;
}
// ---------------------------------------------------------------------------
/**
 * Default bash operations using local shell
 */
function createDefaultBashOperations(shellPath) {
    const { shell: resolvedShell, args: shellArgs } = getShellConfig(shellPath);
    return {
        exec: (command, cwd, { onData, signal, timeout, env }) => {
            if (!existsSync(cwd)) {
                return Promise.reject(new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`));
            }
            const child = spawn(resolvedShell, [...shellArgs, command], {
                cwd,
                // On Unix, detached creates a process group so killProcessTree can use -pid.
                // On Windows, detached creates a new console which breaks stdout pipes for
                // grandchild processes (external executables). Windows uses taskkill /T instead.
                detached: process.platform !== 'win32',
                env: env ?? getShellEnv(),
                stdio: ['ignore', 'pipe', 'pipe']
            });
            return manageChildProcess(child, { onData, signal, timeout });
        }
    };
}
export function createBashTool(cwd, options) {
    const ops = options?.operations ?? createDefaultBashOperations(options?.shellPath);
    const commandPrefix = options?.commandPrefix;
    const resolveConnEnv = options?.resolveConnectionEnv;
    const description = options?.sandboxActive ? TOOL_DESCRIPTION_SANDBOX : getToolDescription();
    function assertShellPolicy(command, ctx) {
        const shellPolicy = requirePermissionPolicy(ctx, BASH).shell;
        const shellApprovalGrants = ctx?.approvalGrants?.shell ?? [];
        if (!shellPolicy.enabled || shellPolicy.mutationBehavior === 'deny') {
            throw new Error('Shell execution is denied by the current permission policy.');
        }
        if (shellPolicy.mutationBehavior === 'require_approval') {
            const approved = shellApprovalGrants.some((grant) => grant.command === command);
            if (!approved) {
                throw new Error('Shell execution requires explicit approval.');
            }
        }
    }
    return {
        name: BASH,
        label: BASH,
        isReadOnly: false,
        description,
        parameters: bashSchema,
        execute: async (_toolCallId, { command, timeout: explicitTimeout, connections }, signal, onUpdate, ctx) => {
            assertShellPolicy(command, ctx);
            const timeout = explicitTimeout ?? 30;
            const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
            // Resolve connection credentials into env vars for this subprocess only
            let connectionEnv;
            if (connections?.length && resolveConnEnv) {
                connectionEnv = resolveConnEnv(connections);
            }
            return new Promise((resolve, reject) => {
                // We'll stream to a temp file if output gets large
                let tempFilePath;
                let tempFileStream;
                // Accumulate output — maintain a running string to avoid O(n²) joins
                let headOutput = '';
                let totalChars = 0;
                const decoder = new TextDecoder();
                const buildDetails = (status) => {
                    const truncation = buildHeadTruncation(headOutput, totalChars);
                    const details = {
                        ...status,
                        truncation: truncation.truncated ? truncation : undefined,
                        fullOutputPath: tempFilePath
                    };
                    return Object.values(details).some((value) => value !== undefined) ? details : undefined;
                };
                const handleData = (data) => {
                    // Sanitize: strip ANSI, remove binary garbage, normalize \r (matches old bash-executor)
                    const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\r/g, '');
                    const previousHeadOutput = headOutput;
                    const nextTotalChars = totalChars + text.length;
                    // Start writing to temp file once we exceed the threshold
                    if (nextTotalChars > DEFAULT_MAX_CHARS && !tempFilePath) {
                        tempFilePath = getTempFilePath();
                        tempFileStream = createWriteStream(tempFilePath);
                        tempFileStream.write(previousHeadOutput);
                        tempFileStream.write(text);
                    }
                    else if (tempFileStream) {
                        tempFileStream.write(text);
                    }
                    if (!tempFilePath) {
                        headOutput = previousHeadOutput + text;
                    }
                    else if (previousHeadOutput.length < DEFAULT_MAX_CHARS) {
                        const remainingHeadChars = DEFAULT_MAX_CHARS - previousHeadOutput.length;
                        headOutput = previousHeadOutput + text.slice(0, remainingHeadChars);
                    }
                    totalChars = nextTotalChars;
                    // Stream partial output to callback
                    if (onUpdate) {
                        const truncation = buildHeadTruncation(headOutput, totalChars);
                        onUpdate({
                            content: [{ type: 'text', text: truncation.content || '' }],
                            details: {
                                truncation: truncation.truncated ? truncation : undefined,
                                fullOutputPath: tempFilePath
                            }
                        });
                    }
                };
                // Merge connection env into shell env for this subprocess only
                const execEnv = connectionEnv ? { ...getShellEnv(), ...connectionEnv } : undefined;
                ops
                    .exec(resolvedCommand, cwd, {
                    onData: handleData,
                    signal,
                    timeout,
                    env: execEnv
                })
                    .then(({ exitCode }) => {
                    if (tempFileStream) {
                        tempFileStream.end();
                    }
                    const truncation = buildHeadTruncation(headOutput, totalChars);
                    let outputText = truncation.content || '(no output)';
                    const details = buildDetails();
                    if (truncation.truncated && tempFilePath) {
                        outputText += `\nFull output: ${tempFilePath}`;
                    }
                    if (exitCode !== 0 && exitCode !== null) {
                        const interpretation = interpretExitCode(exitCode, command);
                        const suffix = interpretation ? ` — ${interpretation}` : '';
                        outputText += `\n\nCommand exited with code ${exitCode}${suffix}`;
                        if (isInformationalExitCode(exitCode, command)) {
                            // Resolve so agent-loop doesn't mark as isError=true (avoids
                            // false error recovery AND keeps the full 50K char output limit).
                            resolve({
                                content: [{ type: 'text', text: outputText }],
                                details: buildDetails({ exitCode })
                            });
                        }
                        else {
                            reject(new ToolExecutionError(outputText, buildDetails({ exitCode })));
                        }
                    }
                    else {
                        resolve({ content: [{ type: 'text', text: outputText }], details });
                    }
                })
                    .catch((err) => {
                    if (tempFileStream) {
                        tempFileStream.end();
                    }
                    const truncation = buildHeadTruncation(headOutput, totalChars);
                    let output = truncation.content;
                    if (err.message === 'aborted') {
                        if (output)
                            output += '\n\n';
                        output += ABORT_AGENT_MESSAGE;
                        reject(new ToolExecutionError(output, buildDetails({ cancelled: true })));
                    }
                    else if (err.message.startsWith('timeout:')) {
                        const timeoutSecs = err.message.split(':')[1];
                        if (output)
                            output += '\n\n';
                        output += `Command timed out after ${timeoutSecs} seconds`;
                        reject(new ToolExecutionError(output, buildDetails({ timedOut: true })));
                    }
                    else {
                        reject(err);
                    }
                });
            });
        }
    };
}
//# sourceMappingURL=bash.js.map