/**
 * 命令工具（1 个）
 *
 * run_command — 在沙箱环境中执行 Shell 命令
 *
 * 安全增强（v0.2.0）：
 * 1. 接入 SandboxManager.scanCode 进行静态代码安全扫描
 * 2. 通过 SandboxManager.validatePath 校验工作目录
 * 3. 命令注入模式检测扩展
 *
 * C-4 修复（v0.3.1）：双层命令注入防御
 * 1. 主层 — 使用 shell-quote 解析命令为 token 数组，对每个 token 做模式匹配
 * 2. 补充层 — 保留原正则检测作为 fallback，覆盖 Windows cmd 语法
 *
 * @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具 + run_command 安全规则
 */

import { exec, execFile } from 'child_process';
import { promisify } from 'util';
import { resolve } from 'path';
import { parse as shellQuoteParse } from 'shell-quote';
import log from 'electron-log';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
import { commandTouchesProtectedFile, isPathWithinWorkspace } from './file-guard';
import type { SandboxManager } from '../../sandbox/sandbox';

const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);

// ===== Windows 编码智能解码 =====
// 某些 Windows 程序（dir、systeminfo、ipconfig 等）不响应 chcp 65001，
// 仍按系统代码页（GBK/CP936）输出字节流。
// 策略：先尝试严格 UTF-8 解码，失败则用 GBK 解码，最后降级为 UTF-8 宽松模式。
function decodeBuffer(buf: Buffer): string {
  if (buf.length === 0) return '';
  try {
    // 严格模式：包含非法 UTF-8 字节序列时抛错
    return new TextDecoder('utf-8', { fatal: true }).decode(buf);
  } catch {
    // UTF-8 解码失败 → 用 GBK/CP936 解码（Windows 中文系统默认代码页）
    try {
      return new TextDecoder('gbk').decode(buf);
    } catch {
      // 最终降级：UTF-8 宽松模式（用 替换字符 代替非法字节）
      return buf.toString('utf-8');
    }
  }
}

/**
 * #9 修复 + 审查修复: 构建安全的子进程环境变量
 *
 * 审查修复: 原白名单方案遗漏了 GIT_* / PYTHONPATH / HTTP_PROXY 等常用变量，导致子进程功能破坏。
 * 改为黑名单方案：剔除包含敏感后缀的变量，保留其余。
 */
function buildSafeCommandEnv(isWindows: boolean): Record<string, string> {
  // 敏感变量后缀黑名单
  const SENSITIVE_SUFFIXES = [
    '_API_KEY', '_TOKEN', '_SECRET', '_PASSWORD', '_PASSWD',
    '_CREDENTIAL', '_CREDENTIALS', '_PRIVATE_KEY',
  ];
  // 敏感变量名黑名单（精确匹配）
  const SENSITIVE_KEYS = new Set([
    'DEEPSEEK_API_KEY', 'AGNES_API_KEY', 'MIMO_API_KEY',
    'GITEA_PASSWORD', 'DATABASE_PASSWORD',
  ]);

  const env: Record<string, string> = {};
  for (const [key, val] of Object.entries(process.env)) {
    if (!val) continue;
    if (SENSITIVE_KEYS.has(key)) continue;
    if (SENSITIVE_SUFFIXES.some((suffix) => key.toUpperCase().endsWith(suffix))) continue;
    env[key] = val;
  }

  // 添加必要的运行时变量
  env.NODE_ENV = 'production';
  if (isWindows) {
    env.PYTHONIOENCODING = 'utf-8';
    env.LANG = 'zh_CN.UTF-8';
    env.LC_ALL = 'zh_CN.UTF-8';
  }

  return env;
}

// ===== 9. run_command =====

export class RunCommandTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'run_command',
    description: 'Execute a shell command in a sandboxed environment. Commands run in the workspace directory. High-risk commands require user confirmation. Passes through SandboxManager static code scan and path validation.',
    parameters: {
      type: 'object',
      properties: {
        command: { type: 'string', description: 'Shell command to execute' },
        workdir: { type: 'string', description: 'Working directory (default: workspace root)' },
        timeout: { type: 'number', description: 'Timeout in milliseconds (default 120000)' },
      },
      required: ['command'],
    },
    category: MetonaToolCategory.CODE_EXECUTION,
    riskLevel: MetonaRiskLevel.HIGH,
    requiresPermission: true,
    timeoutMs: 120_000,
  };

  /** v0.2.0: 可选注入 SandboxManager 进行双重安全校验 */
  private sandboxManager: SandboxManager | null = null;

  /** 注入 SandboxManager（由 main.ts 在注册时调用） */
  setSandboxManager(manager: SandboxManager): void {
    this.sandboxManager = manager;
  }

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const command = args.command as string;
    const workdir = (args.workdir as string) ?? context.workspacePath;
    const timeout = Math.min(300_000, Math.max(1_000, (args.timeout as number) ?? 120_000));

    // 安全校验：workdir 必须在工作空间内
    const resolvedWorkdir = resolve(context.workspacePath, workdir);
    if (!isPathWithinWorkspace(workdir, context.workspacePath)) {
      return { success: false, error: `Working directory must be within workspace: ${workdir}`, command };
    }

    // v0.2.0: SandboxManager 双重安全校验 — fail-closed 设计
    // 若 SandboxManager 未注入（配置错误），拒绝执行高风险命令
    if (!this.sandboxManager) {
      log.error('[RunCommandTool] SandboxManager not injected — refusing to execute (fail-closed)');
      return {
        success: false,
        error: 'Security sandbox unavailable — command execution disabled',
        command,
      };
    }

    // SandboxManager 静态代码扫描
    const scanResult = this.sandboxManager.scanCode(command);
    if (!scanResult.safe) {
      return { success: false, error: 'Command blocked by security policy', command };
    }
    // SandboxManager 路径校验
    const pathResult = this.sandboxManager.validatePath(resolvedWorkdir);
    if (!pathResult.allowed) {
      return { success: false, error: 'Command blocked by security policy', command };
    }

    // 命令安全校验（内置硬阻止列表）
    const validation = this.validateCommand(command);
    if (!validation.allowed) {
      return { success: false, error: validation.reason, command };
    }

    try {
      // Windows 中文编码修复：通过 chcp 65001 切换控制台到 UTF-8 代码页
      // 避免 GBK/CP936 输出被 Node.js 按 UTF-8 解码导致中文乱码
      const isWindows = process.platform === 'win32';

      // #8 修复: 优先使用 execFile（不经过 shell，从根本上防止命令注入）
      // 仅当命令为简单命令（无管道、重定向、&& 等 shell 运算符）时使用 execFile
      // 复杂命令（含 shell 语法）降级到 exec（已有 SandboxManager + validateCommand 双层校验）
      const simpleCmd = this.parseCommandSimple(command);

      // 使用 encoding: 'buffer' 获取原始字节，手动智能解码
      // 部分程序（如 dir、systeminfo）不响应 chcp 65001，仍输出 GBK 字节流
      // #9 修复: 不透传完整 process.env，仅保留子进程运行所需的最小环境变量集合
      // 防止 API keys / tokens / passwords 通过环境变量泄露给子进程
      const execEnv = buildSafeCommandEnv(isWindows);
      const execOpts = {
        cwd: resolvedWorkdir,
        timeout,
        maxBuffer: 1024 * 1024, // 1MB
        encoding: 'buffer' as const, // 返回 Buffer 而非字符串，便于智能解码
        env: execEnv,
      };

      let stdout: Buffer;
      let stderr: Buffer;

      // #8 修复 + 审查修复: 简单命令使用 execFile（不经过 shell，防止命令注入）
      // 但 Windows 上 npm/npx/yarn/pnpm/tsc 等是 .cmd 批处理，execFile 无法执行（ENOENT）
      // 因此 Windows 上仍用 exec（已有 SandboxManager.scanCode + validateCommand 双层校验）
      // 非 Windows 上对简单命令用 execFile
      if (simpleCmd && !isWindows) {
        const result = await execFileAsync(simpleCmd.command, simpleCmd.args, execOpts);
        stdout = result.stdout;
        stderr = result.stderr;
      } else {
        // 复杂命令（含管道/重定向/&& 等 shell 语法）或 Windows — 使用 exec
        // 已有 SandboxManager.scanCode + validateCommand 双层安全校验
        const finalCommand = isWindows
          ? `chcp 65001 >nul 2>&1 && ${command}`
          : command;
        const result = await execAsync(finalCommand, execOpts);
        stdout = result.stdout;
        stderr = result.stderr;
      }

      return {
        success: true,
        stdout: decodeBuffer(stdout).slice(0, 50_000),
        stderr: decodeBuffer(stderr).slice(0, 10_000),
        command,
      };
    } catch (error) {
      const err = error as { message: string; code?: number; stdout?: Buffer; stderr?: Buffer };
      return {
        success: false,
        error: err.message,
        exit_code: err.code,
        stdout: decodeBuffer(err.stdout ?? Buffer.alloc(0)).slice(0, 10_000),
        stderr: decodeBuffer(err.stderr ?? Buffer.alloc(0)).slice(0, 10_000),
        command,
      };
    }
  }

  /**
   * 命令安全校验
   *
   * C-4 修复: 采用双层防御
   * 1. 主层 — 使用 shell-quote 解析命令为 token 数组，对每个 token 做模式匹配
   *    可防御所有字符串拼接绕过（如 `r"m" -rf /`、`$'rm'`、`$(echo rm)`、`r''m`）
   * 2. 补充层 — 保留原正则检测作为 fallback，覆盖 Windows cmd 语法和 shell-quote 无法解析的场景
   *
   * @see docs/MetonaAI-Desktop 架构与交互设计.html — run_command 安全规则
   * @see standard/开发规范.md — 使用 shell-quote 解析命令（禁止简单字符串匹配）
   */
  private validateCommand(command: string): { allowed: boolean; reason?: string } {
    const cmd = command.trim().toLowerCase();

    // 受保护文件检查：禁止通过命令行读写工作空间根目录的 MEMORY.md
    if (commandTouchesProtectedFile(command)) {
      return { allowed: false, reason: 'Access denied: MEMORY.md is managed by the memory system and cannot be accessed via command execution' };
    }

    // ===== 主层: shell-quote token-level 检测 =====
    // 解析失败（Windows cmd 语法等）时降级到正则补充层
    const tokenBlock = this.checkTokens(command);
    if (tokenBlock !== null) return tokenBlock;

    // ===== 补充层: 原正则检测（保留所有原模式） =====
    // 覆盖 shell-quote 无法解析的场景：
    // - Windows cmd 语法（&、|、>nul、chcp 等）
    // - 复杂管道序列（curl ... | sh）
    // - Fork bomb 等特殊语法
    const hardBlocks = [
      // 文件系统破坏
      { pattern: /\brm\b.*\//, reason: 'rm with absolute path is forbidden' },
      { pattern: /\brm\s+-rf?\s+\/(?:[^|;&\s]*\s)*?(?:bin|boot|dev|etc|lib|proc|root|sbin|sys|usr|var)\b/i, reason: 'rm on system directories is forbidden' },
      { pattern: /\b(sudo|su|doas)\b/, reason: 'Privilege escalation commands are forbidden' },
      // 系统控制
      { pattern: /\b(shutdown|reboot|halt|poweroff)\b/, reason: 'System shutdown commands are forbidden' },
      { pattern: /\b(killall|pkill)\s+-9\b/, reason: 'Force kill all processes is forbidden' },
      // 远程代码执行
      { pattern: /curl.*\|\s*(ba)?sh/, reason: 'Remote code execution via pipe is forbidden' },
      { pattern: /wget.*\|\s*(ba)?sh/, reason: 'Remote code execution via pipe is forbidden' },
      { pattern: /\bcurl\s+.*\s*-o\s+\/etc\//i, reason: 'Writing to system directories via curl is forbidden' },
      // 设备文件
      { pattern: /\bdd\b.*of=\/dev\//, reason: 'Writing to device files is forbidden' },
      // 磁盘格式化
      { pattern: /\b(mkfs|fdisk)\b/, reason: 'Disk formatting commands are forbidden' },
      // 权限滥用
      { pattern: /\bchmod\s+777\b/, reason: 'chmod 777 is forbidden' },
      { pattern: /\bchown\s+-R\s+\S+\s+\/(?:\s|$)/i, reason: 'Recursive chown on root is forbidden' },
      // 环境变量窃取
      { pattern: /\b(env|export|printenv)\s*\|.*\b(curl|wget|nc|ncat)\b/i, reason: 'Exfiltrating environment variables is forbidden' },
      // 反向 shell
      { pattern: /\b(bash|sh|zsh)\s+-i\s+>\s*&\s*\/dev\/tcp\//i, reason: 'Reverse shell via /dev/tcp is forbidden' },
      { pattern: /\bnc\s+.*\s+-e\s+(bash|sh)/i, reason: 'Reverse shell via netcat is forbidden' },
      // Windows 危险命令
      { pattern: /\b(format|diskpart)\b/i, reason: 'Disk formatting commands are forbidden' },
      { pattern: /\bshutdown\s*\//i, reason: 'System shutdown commands are forbidden' },
      { pattern: /\breg\s+(add|delete|import|restore)/i, reason: 'Registry modification commands are forbidden' },
      { pattern: /\b(taskkill|kill)\s*\//i, reason: 'Process termination with system flags is forbidden' },
      { pattern: /\bpowershell\s+-enc\s+/i, reason: 'PowerShell encoded command execution is forbidden' },
      // 后台进程与管道炸弹
      { pattern: /&\s*\(/, reason: 'Background subshell execution is forbidden' },
      { pattern: /\|\s*&/, reason: 'Pipe to background process is forbidden' },
      { pattern: /:\(\)\s*\{\s*:\|:&\s*\}\s*;:/, reason: 'Fork bomb is forbidden' },
    ];

    for (const block of hardBlocks) {
      if (block.pattern.test(cmd)) {
        return { allowed: false, reason: block.reason };
      }
    }

    return { allowed: true };
  }

  /**
   * C-4 修复: shell-quote token-level 安全检测
   *
   * 将命令解析为 token 数组，提取所有命令名和参数（忽略 shell 运算符），
   * 对每个 token 做精确匹配。可防御所有字符串拼接绕过：
   * - `r"m" -rf /` → 解析为 ['rm', '-rf', '/'] → 命中 rm 检测
   * - `$'rm'` → 解析为 ['rm'] → 命中 rm 检测
   * - `$(echo rm) -rf /` → 命令替换会被 shell-quote 识别为运算符序列
   *
   * @returns null 表示通过检测；非 null 表示被阻止（含 reason）
   */
  private checkTokens(command: string): { allowed: boolean; reason?: string } | null {
    let tokens: ReturnType<typeof shellQuoteParse>;
    try {
      tokens = shellQuoteParse(command);
    } catch {
      // 解析失败（Windows cmd 语法、不完整的引号等）— 降级到正则补充层
      return null;
    }

    // 提取所有 word token（命令名和参数），忽略运算符（&&、|、; 等）
    // 同时跟踪管道运算符，检测危险组合（如 `| sh`）
    const words: string[] = [];
    let prevWasPipe = false;

    for (const entry of tokens) {
      if (typeof entry === 'string') {
        // 裸字符串 token — 若前一 token 是管道，检测是否为 sh/bash（远程代码执行）
        if (prevWasPipe && /^(ba)?sh$/i.test(entry)) {
          return { allowed: false, reason: 'Remote code execution via pipe is forbidden' };
        }
        words.push(entry);
        prevWasPipe = false;
      } else if (typeof entry === 'object' && entry !== null) {
        const obj = entry as { word?: unknown; op?: unknown };
        if (typeof obj.word === 'string') {
          // 引号包裹或变量替换后的 token
          if (prevWasPipe && /^(ba)?sh$/i.test(obj.word)) {
            return { allowed: false, reason: 'Remote code execution via pipe is forbidden' };
          }
          words.push(obj.word);
          prevWasPipe = false;
        } else if (typeof obj.op === 'string') {
          // 跟踪管道运算符，用于下一轮检测 `| sh`
          prevWasPipe = (obj.op === '|');
        }
      }
    }

    // 危险命令名 token（精确匹配，大小写不敏感）
    const dangerousCommands = new Set([
      'sudo', 'su', 'doas',
      'shutdown', 'reboot', 'halt', 'poweroff',
      'mkfs', 'fdisk', 'format', 'diskpart',
    ]);
    // 危险参数 token
    const dangerousArgs = new Set([
      '-enc', '-encodedcommand',  // PowerShell 编码执行
    ]);

    for (const word of words) {
      const lower = word.toLowerCase();
      // 1. 危险命令名（精确匹配，或 mkfs/fdisk 前缀匹配如 mkfs.ext4）
      if (dangerousCommands.has(lower) || /^(mkfs|fdisk)\b/.test(lower)) {
        if (['sudo', 'su', 'doas'].includes(lower)) {
          return { allowed: false, reason: 'Privilege escalation commands are forbidden' };
        }
        if (['shutdown', 'reboot', 'halt', 'poweroff'].includes(lower)) {
          return { allowed: false, reason: 'System shutdown commands are forbidden' };
        }
        // mkfs/fdisk/format/diskpart + mkfs.ext4 等前缀匹配
        return { allowed: false, reason: 'Disk formatting commands are forbidden' };
      }
      // 2. 危险参数（精确匹配）
      if (dangerousArgs.has(lower)) {
        return { allowed: false, reason: 'PowerShell encoded command execution is forbidden' };
      }
      // 3. dd 写设备文件：of=/dev/...（不依赖系统目录前置，独立检测）
      if (/^of=\/dev\//.test(lower)) {
        return { allowed: false, reason: 'Writing to device files is forbidden' };
      }
    }

    // 4. 检测 rm 与系统目录的组合（token 序列检测）
    // 例如: ['rm', '-rf', '/etc'] 应被拦截
    let hasRm = false;
    let hasSystemPath = false;
    for (const word of words) {
      const lower = word.toLowerCase();
      if (lower === 'rm') hasRm = true;
      if (/^\/(?:bin|boot|dev|etc|lib|proc|root|sbin|sys|usr|var)\b/.test(lower)) {
        hasSystemPath = true;
      }
    }
    if (hasRm && hasSystemPath) {
      return { allowed: false, reason: 'rm on system directories is forbidden' };
    }

    return null;
  }

  /**
   * #8 修复: 将命令解析为简单的 command + args 数组
   *
   * 使用 shell-quote 解析命令，如果命令只包含 word tokens（无管道、重定向、&& 等 shell 运算符），
   * 返回 { command, args } 供 execFile 使用（不经过 shell，从根本上防止命令注入）。
   * 如果包含 shell 运算符或解析失败，返回 null，调用方降级到 exec（已有安全校验）。
   *
   * @returns 简单命令的 { command, args }，或 null 表示复杂命令
   */
  private parseCommandSimple(command: string): { command: string; args: string[] } | null {
    let tokens: ReturnType<typeof shellQuoteParse>;
    try {
      tokens = shellQuoteParse(command);
    } catch {
      // 解析失败（Windows cmd 语法、不完整引号等）— 降级到 exec
      return null;
    }

    const words: string[] = [];
    for (const entry of tokens) {
      if (typeof entry === 'string') {
        words.push(entry);
      } else if (typeof entry === 'object' && entry !== null) {
        const obj = entry as { word?: unknown; op?: unknown };
        if (typeof obj.word === 'string') {
          words.push(obj.word);
        } else if (typeof obj.op === 'string') {
          // 遇到 shell 运算符（&&、|、;、> 等）— 复杂命令，降级到 exec
          return null;
        }
      }
    }

    if (words.length === 0) return null;
    return { command: words[0], args: words.slice(1) };
  }
}
