/**
 * ============================================================
 *  src/tools/bash.ts —— Bash 工具（万能命令兜底）
 * ------------------------------------------------------------
 *  对应 kakadeai 主仓库的 BashTool。
 *  这是 minimal-agent 的"瑞士军刀" —— 当 Read/Edit/Write/Glob/Grep
 *  搞不定时（rm 文件、chmod、git、npm/curl、跑测试...），就用它。
 *
 *  设计原则（重要）：
 *
 *  1. 优先用专用工具，Bash 只做兜底
 *     - 读文件 → Read（不要 cat/head/tail）
 *     - 写文件 → Write（不要 echo > / cat <<EOF）
 *     - 改文件 → Edit（不要 sed/awk）
 *     - 列文件 → Glob（不要 find/ls）
 *     - 搜内容 → Grep（不要 grep/rg）
 *     这条放进 description 里反复强调，让模型自觉。
 *
 *  2. 安全黑名单：拦截灾难性命令
 *     - rm -rf / 、 rm -rf $HOME、 rm -rf ~
 *     - mkfs / format c: / dd if=...of=/dev/sd*
 *     - shutdown / reboot / halt / poweroff
 *     - fork bomb :(){ :|:& };:
 *     - curl ... | sh / wget ... | bash （管道到 shell）
 *     不可绕过：在 call() 入口正则匹配，命中直接拒绝。
 *     注意：黑名单是兜底，不是真正的沙盒；模型规规矩矩工作时不会误伤。
 *
 *  3. 超时保护：默认 120s，可显式设到最多 600s（10 分钟）
 *     - 防止跑死的命令吃光会话；
 *     - AbortSignal + timer 双保险。
 *
 *  4. 跨平台：
 *     - 用 spawn({ shell: true })，让平台自己挑 shell
 *       （Unix → /bin/sh -c，Windows → cmd.exe /d /s /c）
 *     - 对模型透明：在 prompt 里说"假定 bash 语法"，
 *       Windows 用户可能要自行调整跨平台命令（这是 minimal 版本，不做转译）
 *
 *  5. 输出兜底：
 *     - stdout / stderr / exit code 一起返回（结构化文本）
 *     - 超过 30K 字符截断，避免把 npm install 的 200K 日志灌进上下文
 * ============================================================
 */
import { spawn } from 'node:child_process';
import { z } from 'zod';
import { getWorkingDir } from '../../bootstrap/workingDir.js';
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
import { toToolParameters } from '../../utils/zodToJson.js';
import { interpretCommandResult } from './semantics.js';
import { scanDestructiveCommand } from './warnings.js';
// ---------------- 0. 常量 ----------------
const DEFAULT_TIMEOUT_MS = 120_000; // 2 min
const MAX_TIMEOUT_MS = 600_000; // 10 min
/**
 * G1：stdout / stderr 即时缓冲上限（各自）。命令狂输出（如 cat 大文件 / yes）时，
 * 在最终统一截断之前先把内存封顶，防 OOM。取最终展示截断阈值的 2 倍 —— 正常输出不受影响，
 * 超大输出也只占有限内存（最终仍按 DEFAULT_MAX_RESULT_SIZE_CHARS 截断展示）。
 */
const MAX_STREAM_BUFFER_CHARS = DEFAULT_MAX_RESULT_SIZE_CHARS * 2;
/**
 * 灾难性命令黑名单。
 * 命中任意一条就直接拒绝执行 —— 这层是"防猪队友"而不是真沙盒。
 *
 * 选词依据（每条注释解释为啥要拦）：
 * - 历史上"误删生产/格盘/锁机"的真实事故都对应一类模式
 * - 工程协作里这些命令几乎不会被合法用到（如要用，肯定是用户在终端自己手敲）
 *
 * 工具仅是"模型可调用的能力"，模型规规矩矩地写代码、跑测试时永远不会触到这些。
 */
const FORBIDDEN_PATTERNS = [
    // ---- rm -rf / 及变体 ----
    // 经典："rm -rf /"、"rm -rf /*"、"rm -fr /usr"、"rm -rf $HOME"、"rm -rf ~"
    // 也覆盖 sudo rm -rf /
    {
        pattern: /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*\s+)+(?:\/+\*?|\$HOME|~)(?:\s|$)/,
        reason: '禁止递归删除根目录或家目录（rm -rf / 类）',
    },
    // 拦截 "rm -rf <绝对路径>" 中目标是关键系统目录的情况（防误伤其它正常 rm -rf ./tmp）
    {
        pattern: /\brm\s+(?:-[a-zA-Z]*[rRf][a-zA-Z]*\s+)+(?:\/etc|\/usr|\/bin|\/sbin|\/var|\/boot|\/lib|\/System|\/Windows|\/Users|\/home)(?:\/|\s|$)/,
        reason: '禁止递归删除系统关键目录',
    },
    // ---- 格盘 / 改写裸设备 ----
    {
        pattern: /\bmkfs(?:\.[a-z0-9]+)?\b/i,
        reason: '禁止格式化文件系统（mkfs）',
    },
    {
        // 末尾不能用 \b：盘符 "C:" 后是非 word char，\b 匹配不到。
        // 改成"盘符后跟空白/反斜杠/结尾"。
        pattern: /\bformat\s+[A-Za-z]:(?=\s|\\|$)/i,
        reason: '禁止 Windows 格盘命令（format X:）',
    },
    {
        pattern: /\bdd\s+[^|;&]*\bof=\/dev\/(?:sd[a-z]|nvme|disk|hd[a-z]|mmcblk)/i,
        reason: '禁止 dd 写入裸磁盘设备',
    },
    {
        pattern: />\s*\/dev\/(?:sd[a-z]|nvme|disk|hd[a-z]|mmcblk)/i,
        reason: '禁止重定向到裸磁盘设备',
    },
    // ---- 关机 / 重启 ----
    {
        pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/i,
        reason: '禁止关机/重启命令',
    },
    {
        pattern: /\binit\s+[06]\b/,
        reason: '禁止 init 0/6（关机/重启）',
    },
    // ---- Fork bomb ----
    {
        pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
        reason: '禁止 fork bomb',
    },
    // ---- 管道到 shell（典型恶意安装脚本模式） ----
    // curl ... | sh / wget ... | bash / curl ... | python
    {
        pattern: /\b(?:curl|wget|fetch)\b[^|;&]*\|\s*(?:sh|bash|zsh|dash|python|perl|ruby|node)\b/i,
        reason: '禁止从网络管道到 shell 解释器（curl ... | sh 模式）',
    },
    // ---- chmod 整盘 777 ----
    {
        pattern: /\bchmod\s+(?:-R\s+)?[0-7]*7[0-7]*\s+\/(?:\s|$)/,
        reason: '禁止对根目录 chmod 777',
    },
    // ---- Windows 系统级删除 ----
    {
        pattern: /\b(?:del|erase)\s+\/[sS][^|;&]*[A-Za-z]:\\?\s*$/,
        reason: '禁止 Windows 全盘删除（del /s 根目录）',
    },
    {
        pattern: /\brmdir\s+\/[sS][^|;&]*[A-Za-z]:\\?\s*$/,
        reason: '禁止 Windows 全盘 rmdir',
    },
    {
        pattern: /\bdiskpart\b/i,
        reason: '禁止 diskpart',
    },
];
/**
 * 检查命令是否命中黑名单。
 * 返回命中的原因字符串，否则返回 null。
 */
export function checkForbidden(command) {
    for (const { pattern, reason } of FORBIDDEN_PATTERNS) {
        if (pattern.test(command))
            return reason;
    }
    return null;
}
/**
 * 子进程环境变量：从 process.env 复制后**剥离 agent 自己的密钥**
 * （`MINIMAL_AGENT_*` / `TAVILY_*`）。
 *
 * 动机（安全）：main.tsx 启动时会把 saved config 里的 key 注入 process.env。
 * 若原样把 process.env 透传给 Bash 子进程，模型只要跑 `echo $MINIMAL_AGENT_API_KEY`
 * 就能把 key（绿色版里是卖家预置的）读进 tool result、回传 LLM、甚至打到 -p stdout。
 * 这些 env 是 agent 自身配置，子进程命令并不需要 —— 直接删掉，最小化泄露面。
 * 其余变量（PATH / HOME / 用户自己 export 的）照常透传。
 *
 * 导出供测试断言。
 */
export function sanitizedChildEnv(base = process.env) {
    const env = { ...base };
    for (const key of Object.keys(env)) {
        if (key.startsWith('MINIMAL_AGENT_') || key.startsWith('TAVILY_')) {
            delete env[key];
        }
    }
    return env;
}
// ---------------- 1. Zod 输入 schema ----------------
const inputSchema = z.object({
    command: z
        .string()
        .min(1, '必须提供 command')
        .describe('要执行的 shell 命令（假定 bash 语法）'),
    timeout: z
        .number()
        .int()
        .positive()
        .max(MAX_TIMEOUT_MS)
        .optional()
        .describe(`超时（毫秒），最多 ${MAX_TIMEOUT_MS}（${MAX_TIMEOUT_MS / 60_000} 分钟）；不填默认 ${DEFAULT_TIMEOUT_MS}`),
    description: z
        .string()
        .optional()
        .describe('用一句话主动语态描述命令做什么（5-10 词），如 "List files in current directory"'),
});
// ---------------- 2. JSON Schema（由 Zod 自动派生） ----------------
const parameters = toToolParameters(inputSchema);
// ---------------- 3. Description（给 LLM 看的工具说明） ----------------
//
// 提炼自 kakadeai/tools/BashTool/prompt.ts 的 getSimplePrompt()。
// 核心逻辑：
//   - 先点明"专用工具优先"的原则，把每类操作映射到对应工具
//   - 再列 Bash 自身的使用规范（绝对路径 / 不 cd / 超时 / 多命令并行...）
//   - 最后强调安全黑名单（让模型知道哪些命令会被拒，避免反复试）
const description = `Executes a given bash command and returns its output.

The working directory persists between commands, but shell state does not. The shell environment is initialized from the user's profile.

IMPORTANT: Avoid using this tool to run \`find\`, \`grep\`, \`cat\`, \`head\`, \`tail\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or after you have verified that a dedicated tool cannot accomplish your task. Instead, use the appropriate dedicated tool as this will provide a much better experience for the user:

 - File search: Use Glob (NOT find or ls)
 - Content search: Use Grep (NOT grep or rg)
 - Read files: Use Read (NOT cat/head/tail)
 - Edit files: Use Edit (NOT sed/awk)
 - Write files: Use Write (NOT echo >/cat <<EOF)
 - Communication: Output text directly (NOT echo/printf)
While the Bash tool can do similar things, it's better to use the built-in tools as they provide a better user experience and make it easier to review tool calls and give permission.

# Instructions
 - If your command will create new directories or files, first use this tool to run \`ls\` to verify the parent directory exists and is the correct location.
 - Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt").
 - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of \`cd\`. You may use \`cd\` if the User explicitly requests it.
 - You may specify an optional timeout in milliseconds (up to ${MAX_TIMEOUT_MS}ms / ${MAX_TIMEOUT_MS / 60_000} minutes). By default, your command will timeout after ${DEFAULT_TIMEOUT_MS}ms (${DEFAULT_TIMEOUT_MS / 60_000} minutes).
 - When issuing multiple commands:
  - If the commands are independent and can run in parallel, make multiple Bash tool calls in a single message.
  - If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them.
  - Use ';' only when you need to run commands sequentially but don't care if earlier commands fail.
  - DO NOT use newlines to separate commands (newlines are ok in quoted strings and here-strings).
 - For git commands:
  - Prefer to create a new commit rather than amending an existing commit.
  - Before running destructive operations (e.g., git reset --hard, git push --force, git checkout --), consider whether there is a safer alternative that achieves the same goal. Only use destructive operations when they are truly the best approach.
  - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
 - Avoid unnecessary \`sleep\` commands; do not retry failing commands in a sleep loop — diagnose the root cause.

# Exit code semantics
Some commands return non-zero exit codes for informational (non-error) reasons. Bash recognizes these and reports them as success (ok=true) — do NOT retry just because exit code is 1:
 - \`grep\` / \`rg\` exit 1 → no match found (not an error)
 - \`find\` exit 1 → some directories inaccessible (non-fatal, partial results still returned)
 - \`diff\` / \`cmp\` exit 1 → files differ (informational, not an error)
 - \`test\` / \`[\` exit 1 → condition is false (the answer to a question, not a failure)
Only exit codes ≥ 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.

# Safety
The following command patterns are blocked at the tool level and will fail before execution (no need to try them):
 - \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
 - Filesystem destruction: \`mkfs\`, \`format X:\`, \`dd of=/dev/sdX\`, redirects to raw block devices
 - Power management: \`shutdown\`, \`reboot\`, \`halt\`, \`poweroff\`, \`init 0/6\`
 - Fork bombs
 - Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
 - \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`

If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal — do not try to bypass the check.

Separately, Bash scans for common destructive-but-recoverable patterns (\`git reset --hard\`, \`git push -f\` / \`--force\` / \`--force-with-lease\`, \`git checkout .\`, \`git restore .\`, \`git clean -f\`, \`git stash drop/clear\`, \`git branch -D\`, \`git commit --amend\` / \`--no-verify\`, \`rm -rf <path>\`, \`DROP TABLE\`, \`TRUNCATE\`, \`DELETE FROM\`, \`kubectl delete\`, \`terraform destroy\`, etc.) and prepends a \`⚠️ 警告:\` line to the output. These commands are NOT blocked — the warning is informational. Treat it as a signal to double-check intent and surface the warning to the user when relevant.`;
// ---------------- 4. call() —— 真正干活 ----------------
async function call(input, signal) {
    const command = input.command;
    const timeoutMs = Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
    // 4a. 安全黑名单（命中即拒）
    const forbiddenReason = checkForbidden(command);
    if (forbiddenReason) {
        return {
            ok: false,
            error: `安全检查拒绝执行：${forbiddenReason}\n命令：${command}`,
        };
    }
    // 4a-2. 破坏性命令告警扫描（仅警告，不阻塞）
    //       命中后把警告挂到结果输出顶部，让模型/用户看见苗头。
    const destructiveWarning = scanDestructiveCommand(command);
    // 4b. spawn —— shell:true 让平台自己挑 shell
    //     Unix:    /bin/sh -c <command>
    //     Windows: cmd.exe /d /s /c "<command>"
    let stdout = '';
    let stderr = '';
    let exitCode = null;
    let timedOut = false;
    let killedBySignal = false;
    // 用 AbortController 把外部 signal + 我们自己的 timeout 合并成一个
    const ac = new AbortController();
    const onAbort = () => ac.abort();
    signal?.addEventListener('abort', onAbort, { once: true });
    const timer = setTimeout(() => {
        timedOut = true;
        ac.abort();
    }, timeoutMs);
    try {
        await new Promise((resolveP, rejectP) => {
            const child = spawn(command, {
                shell: true,
                cwd: getWorkingDir(),
                signal: ac.signal,
                env: sanitizedChildEnv(),
                windowsHide: true,
            });
            child.stdout?.setEncoding('utf8');
            child.stderr?.setEncoding('utf8');
            child.stdout?.on('data', (chunk) => {
                // G1：即时封顶，防止狂输出命令在最终截断前撑爆内存（OOM）。不强行 kill，
                // 让命令自己跑完或被 timeout 收走；超出上限的部分直接丢弃，最终仍按上限截断展示。
                if (stdout.length < MAX_STREAM_BUFFER_CHARS)
                    stdout += chunk;
            });
            child.stderr?.on('data', (chunk) => {
                if (stderr.length < MAX_STREAM_BUFFER_CHARS)
                    stderr += chunk;
            });
            child.on('error', (e) => {
                // signal abort 会先触发 error('AbortError')；把这种 case 转成正常分支
                if (e.code === 'ABORT_ERR') {
                    killedBySignal = true;
                    resolveP();
                }
                else {
                    rejectP(e);
                }
            });
            child.on('close', (code, sig) => {
                exitCode = code;
                if (sig)
                    killedBySignal = true;
                resolveP();
            });
        });
    }
    catch (e) {
        return {
            ok: false,
            error: `执行命令失败：${e.message}\n命令：${command}`,
        };
    }
    finally {
        clearTimeout(timer);
        signal?.removeEventListener('abort', onAbort);
    }
    // 4c. 拼输出
    const parts = [];
    if (stdout)
        parts.push(`<stdout>\n${stdout.replace(/\s+$/, '')}\n</stdout>`);
    if (stderr)
        parts.push(`<stderr>\n${stderr.replace(/\s+$/, '')}\n</stderr>`);
    if (parts.length === 0)
        parts.push('<no output>');
    if (timedOut) {
        parts.push(`\n[超时 ${timeoutMs}ms 后被中断]`);
    }
    else if (killedBySignal && !timedOut) {
        parts.push(`\n[被信号中断]`);
    }
    parts.push(`\n[exit code: ${exitCode === null ? 'killed' : exitCode}]`);
    let combinedOutput = parts.join('\n');
    // 4d. 大小兜底
    if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
        combinedOutput =
            combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) +
                `\n\n... (输出超过 ${DEFAULT_MAX_RESULT_SIZE_CHARS} 字符，已截断)`;
    }
    // 4e. 中断 / 超时仍然算硬失败
    if (timedOut || killedBySignal) {
        return { ok: false, error: combinedOutput };
    }
    // 4f. 语义化退出码判定
    //     交给 semantics 模块：grep/rg/find/diff/test 有"信息性退出码"，
    //     其它命令用默认语义（exitCode !== 0 即错）。
    //     destructiveWarning 命中时把告警贴在输出头部，模型/用户都能看见。
    const semantic = interpretCommandResult(command, exitCode ?? 0, stdout, stderr);
    const finalContent = destructiveWarning
        ? `⚠️ 警告: ${destructiveWarning}\n\n${combinedOutput}`
        : combinedOutput;
    if (semantic.isError) {
        return {
            ok: false,
            error: `命令失败 (exit ${exitCode}): ${stderr || stdout || semantic.message || ''}`.trim() +
                `\n\n${finalContent}`,
        };
    }
    return { ok: true, content: finalContent };
}
// ---------------- 5. 导出 ----------------
export const bashTool = {
    name: 'Bash',
    description,
    inputSchema,
    parameters,
    isReadOnly: false,
    isConcurrencySafe: false, // 默认按"会改状态"算；保守不并发
    maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
    call,
};
