/**
 * Shell protocol runtime for connector tools.
 *
 * Spawns a process with an explicit arg array (no shell:true) so
 * templated values can never inject shell metacharacters. Each arg
 * is templated independently — e.g. `["git", "-C", "{args.repo}",
 * "log", "-n", "{args.n}"]` produces a literal arg, not a shell line.
 *
 * Returns combined output. Non-zero exit code is surfaced as is_error
 * with the exit code in the preamble and stderr if any.
 *
 * Safety boundary: connectors are user-installed manifests. A malicious
 * yaml could still pick a destructive `command[0]` (rm, etc.), so
 * shell-protocol tools must be reviewed at install time. There's no
 * automatic command allow-list in v1.
 */

import type { ConnectorTool } from '../../connectors/types';
import { expandAllTokens } from '../../plugins/templates';
import { spawn } from 'node:child_process';

export interface ShellProtocolArgs {
  tool: ConnectorTool;
  settings: Record<string, any>;
  args: Record<string, any>;
}

export interface ShellProtocolResult {
  content: string;
  is_error?: boolean;
}

const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_TIMEOUT_MS = 300_000;
const MAX_OUTPUT_BYTES = 8 * 1024;

function truncate(s: string): string {
  const buf = Buffer.from(s, 'utf-8');
  if (buf.byteLength <= MAX_OUTPUT_BYTES) return s;
  return buf.subarray(0, MAX_OUTPUT_BYTES).toString('utf-8') +
         `\n(...truncated, total ${buf.byteLength} bytes)`;
}

export async function runShell({ tool, settings, args }: ShellProtocolArgs): Promise<ShellProtocolResult> {
  const cmd = tool.command;
  if (!cmd || !Array.isArray(cmd) || cmd.length === 0) {
    return { content: 'shell tool missing `command` array', is_error: true };
  }
  const expanded = cmd.map((part) => expandAllTokens(String(part), settings, args));
  const [bin, ...argv] = expanded;
  const cwd = tool.cwd ? expandAllTokens(tool.cwd, settings, args) : undefined;
  const env = tool.env
    ? Object.fromEntries(Object.entries(tool.env).map(([k, v]) => [k, expandAllTokens(String(v), settings, args)]))
    : undefined;
  const timeoutMs = Math.min(MAX_TIMEOUT_MS, Math.max(1, Number(tool.timeout_ms || DEFAULT_TIMEOUT_MS)));

  return new Promise<ShellProtocolResult>((resolve) => {
    let proc;
    try {
      proc = spawn(bin!, argv, {
        cwd,
        env: env ? { ...process.env, ...env } : process.env,
        stdio: ['ignore', 'pipe', 'pipe'],
      });
    } catch (e) {
      return resolve({ content: `shell spawn failed: ${(e as Error).message}`, is_error: true });
    }

    let stdout = '';
    let stderr = '';
    let killed = false;
    const timer = setTimeout(() => {
      killed = true;
      try { proc.kill('SIGTERM'); } catch {}
    }, timeoutMs);

    proc.stdout?.on('data', (b) => { stdout += b.toString('utf-8'); });
    proc.stderr?.on('data', (b) => { stderr += b.toString('utf-8'); });

    proc.on('error', (err) => {
      clearTimeout(timer);
      resolve({ content: `shell error: ${err.message}`, is_error: true });
    });

    proc.on('close', (code, signal) => {
      clearTimeout(timer);
      const preamble = killed
        ? `[killed after ${timeoutMs}ms]\n`
        : signal
          ? `[signal ${signal}]\n`
          : code === 0
            ? ''
            : `[exit ${code}]\n`;
      const trail = stderr.trim() && code !== 0 ? `\n--- stderr ---\n${truncate(stderr)}` : '';
      resolve({
        content: preamble + truncate(stdout) + trail,
        is_error: killed || code !== 0,
      });
    });
  });
}
