/**
 * Bash tool — runs a shell command in the workspace.
 *
 * Stays small on purpose: combined stdout+stderr, hard timeout, kills the
 * process on session abort. No interactive subshells, no background jobs.
 */
import { spawn } from 'child_process';
import type { PiTool } from './types.js';

const DEFAULT_TIMEOUT_MS = 60_000;
const HARD_TIMEOUT_MS = 5 * 60_000;
const OUTPUT_CAP_BYTES = 200 * 1024;  // 200 KB; matches Claude SDK's behavior

export const bashTool: PiTool = {
  name: 'Bash',
  description:
    'Run a shell command in the workspace and return its combined stdout+stderr. Use this for non-interactive commands only — no editors, no long-running servers.',
  inputSchema: {
    type: 'object',
    properties: {
      command: { type: 'string', description: 'The shell command to execute.' },
      description: { type: 'string', description: 'A short description (5–10 words) of what the command does.' },
      timeout: { type: 'integer', description: 'Timeout in milliseconds (default 60 000, max 300 000).' },
    },
    required: ['command'],
  },

  async run(input, ctx) {
    const command = typeof input?.command === 'string' ? input.command : '';
    if (!command.trim()) return { output: 'command is required.', isError: true };

    const requestedTimeout = Number(input?.timeout) || DEFAULT_TIMEOUT_MS;
    const timeout = Math.min(HARD_TIMEOUT_MS, Math.max(1000, requestedTimeout));

    return await new Promise((resolve) => {
      let out = '';
      let truncated = false;
      let timedOut = false;
      let settled = false;

      const child = spawn('bash', ['-lc', command], {
        cwd: ctx.cwd,
        env: process.env,
        stdio: ['ignore', 'pipe', 'pipe'],
      });

      const append = (chunk: Buffer) => {
        if (truncated) return;
        const remaining = OUTPUT_CAP_BYTES - Buffer.byteLength(out, 'utf-8');
        if (remaining <= 0) {
          truncated = true;
          return;
        }
        const text = chunk.toString('utf-8');
        if (Buffer.byteLength(text, 'utf-8') > remaining) {
          out += text.slice(0, remaining);
          truncated = true;
        } else {
          out += text;
        }
      };

      child.stdout?.on('data', append);
      child.stderr?.on('data', append);

      const timer = setTimeout(() => {
        timedOut = true;
        try { child.kill('SIGKILL'); } catch {}
      }, timeout);

      const onAbort = () => {
        try { child.kill('SIGKILL'); } catch {}
      };
      ctx.signal?.addEventListener('abort', onAbort);

      child.on('error', (err) => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        ctx.signal?.removeEventListener('abort', onAbort);
        resolve({ output: `Failed to spawn command: ${err.message}`, isError: true });
      });

      child.on('close', (code, signal) => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        ctx.signal?.removeEventListener('abort', onAbort);
        const tail = truncated ? `\n\n[Output truncated at ${OUTPUT_CAP_BYTES} bytes]` : '';
        if (timedOut) {
          resolve({ output: `Command timed out after ${timeout}ms.\n\n${out}${tail}`, isError: true });
          return;
        }
        if (ctx.signal?.aborted) {
          resolve({ output: 'Command aborted (session ended).', isError: true });
          return;
        }
        if (code === 0) {
          resolve({ output: (out || '(no output)') + tail });
        } else {
          resolve({
            output: `Command exited with code ${code}${signal ? ` (signal ${signal})` : ''}.\n\n${out}${tail}`,
            isError: true,
          });
        }
      });
    });
  },
};
