import { spawn } from 'node:child_process';
import { computerError } from './errors.js';

export interface RunOptions {
  readonly timeoutMs?: number;
  readonly maxStdoutBytes?: number;
  readonly maxStderrBytes?: number;
  readonly env?: NodeJS.ProcessEnv;
}

export interface RunResult {
  readonly exitCode: number | null;
  readonly stdout: Buffer;
  readonly stderr: Buffer;
  readonly timedOut: boolean;
}

export interface CommandRunner {
  run(command: string, args: readonly string[], options?: RunOptions): Promise<RunResult>;
}

export class SpawnCommandRunner implements CommandRunner {
  async run(command: string, args: readonly string[], options: RunOptions = {}): Promise<RunResult> {
    const timeoutMs = options.timeoutMs ?? 30_000;
    const maxStdoutBytes = options.maxStdoutBytes ?? 16 * 1024 * 1024;
    const maxStderrBytes = options.maxStderrBytes ?? 1024 * 1024;

    return new Promise<RunResult>((resolve, reject) => {
      const child = spawn(command, [...args], {
        shell: false,
        stdio: ['ignore', 'pipe', 'pipe'],
        env: { ...process.env, ...options.env },
      });
      const stdout: Buffer[] = [];
      const stderr: Buffer[] = [];
      let stdoutBytes = 0;
      let stderrBytes = 0;
      let timedOut = false;
      let settled = false;

      const finishError = (error: Error): void => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        reject(error);
      };

      child.stdout.on('data', (chunk: Buffer) => {
        stdoutBytes += chunk.byteLength;
        if (stdoutBytes > maxStdoutBytes) {
          child.kill('SIGTERM');
          finishError(computerError('OUTPUT_LIMIT', command, 'Command stdout exceeded configured limit.', { maximum: maxStdoutBytes }));
          return;
        }
        stdout.push(chunk);
      });
      child.stderr.on('data', (chunk: Buffer) => {
        stderrBytes += chunk.byteLength;
        if (stderrBytes > maxStderrBytes) {
          child.kill('SIGTERM');
          finishError(computerError('OUTPUT_LIMIT', command, 'Command stderr exceeded configured limit.', { maximum: maxStderrBytes }));
          return;
        }
        stderr.push(chunk);
      });
      child.once('error', error => {
        const code = (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'DEPENDENCY_MISSING' : 'OS_ERROR';
        finishError(computerError(code, command, `Could not execute required host command: ${command}.`));
      });
      child.once('close', exitCode => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        resolve({ exitCode, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), timedOut });
      });

      const timer = setTimeout(() => {
        timedOut = true;
        child.kill('SIGTERM');
      }, timeoutMs);
      timer.unref();
    });
  }
}
