// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

/**
 * NERF Benchmark — Docker sandbox for auto agent execution.
 *
 * Uses child_process to exec commands in a Docker container instead of
 * dockerode, keeping the dependency footprint minimal.
 */

import { execSync, spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';

const DEFAULT_IMAGE = 'python:3.12-slim';
const CONTAINER_PREFIX = 'nerf-sandbox';

export class SandboxError extends Error {
  constructor(msg) { super(msg); this.name = 'SandboxError'; }
}

export class SandboxContainer {
  constructor({ image = DEFAULT_IMAGE, nameSuffix = '', network = 'none', memory = '512m', cpus = '0.5', pidsLimit = 256, readOnly = true } = {}) {
    this._image = image;
    this._containerId = null;
    this._connectedNetworks = [];
    this._nameSuffix = nameSuffix;
    this._setupDone = false;
    this._network = network;
    this._memory = memory;
    this._cpus = cpus;
    this._pidsLimit = pidsLimit;
    this._readOnly = readOnly;
  }

  get containerId() { return this._containerId; }

  create() {
    const name = this._nameSuffix
      ? `${CONTAINER_PREFIX}-${this._nameSuffix}`
      : `${CONTAINER_PREFIX}-${randomBytes(4).toString('hex')}`;

    const dockerArgs = [
      'run', '-d', '--name', name,
      `--memory=${this._memory}`, `--cpus=${this._cpus}`,
      `--pids-limit=${this._pidsLimit}`,
      `--network=${this._network}`,
      // Drop all Linux capabilities — CTF containers don't need any.
      // CWE-250: Execution with Unnecessary Privileges prevention.
      '--cap-drop', 'ALL',
      // Run as unprivileged nobody (uid 65534) — not root.
      '--user', '65534:65534',
      // Prevent privilege escalation via setuid/setgid binaries.
      '--security-opt', 'no-new-privileges',
    ];
    if (this._readOnly) {
      dockerArgs.push('--read-only', '--tmpfs', '/tmp:rw,noexec,nosuid,size=256m');
    }
    dockerArgs.push(this._image, 'sleep', 'infinity');

    const result = spawnSync('docker', dockerArgs, { timeout: 30000, stdio: 'pipe' });

    if (result.status !== 0) {
      throw new SandboxError(`Failed to create sandbox: ${(result.stderr || '').toString().slice(0, 300)}`);
    }
    this._containerId = (result.stdout || '').toString().trim().slice(0, 12);
    this._installBaseTools();
  }

  _installBaseTools() {
    if (this._setupDone) return;
    this.execTool('apt-get update -qq && apt-get install -y -qq curl nmap > /dev/null 2>&1', 120);
    this._setupDone = true;
  }

  execTool(command, timeout = 60) {
    if (!this._containerId) throw new SandboxError('Container not created. Call create() first.');
    const result = spawnSync('docker', ['exec', this._containerId, '/bin/sh', '-c', command], {
      timeout: timeout * 1000,
      stdio: 'pipe',
      maxBuffer: 10 * 1024 * 1024,
    });
    return {
      exitCode: result.status ?? -1,
      stdout: (result.stdout || '').toString(),
      stderr: (result.stderr || '').toString(),
    };
  }

  connectNetwork(networkName) {
    if (!this._containerId) throw new SandboxError('Container not created.');
    const result = spawnSync('docker', ['network', 'connect', networkName, this._containerId], { timeout: 10000, stdio: 'pipe' });
    if (result.status !== 0) {
      throw new SandboxError(`Failed to connect to network ${networkName}: ${(result.stderr || '').toString().slice(0, 200)}`);
    }
    this._connectedNetworks.push(networkName);
  }

  /**
   * Copy a file from the host into the sandbox container.
   * @param {string} hostPath - Absolute path on the host
   * @param {string} containerPath - Path inside the container (default: /tmp/<filename>)
   */
  copyFileIn(hostPath, containerPath) {
    if (!this._containerId) throw new SandboxError('Container not created.');
    const result = spawnSync('docker', ['cp', hostPath, `${this._containerId}:${containerPath}`], {
      timeout: 30000,
      stdio: 'pipe',
    });
    if (result.status !== 0) {
      throw new SandboxError(`Failed to copy ${hostPath} to container: ${(result.stderr || '').toString().slice(0, 200)}`);
    }
  }

  /**
   * Parse a docker-compose.yml and return all service URLs.
   * @param {object} services - Parsed docker-compose services object
   * @param {string} networkName - Docker network the sandbox is connected to
   * @returns {Array<{ name: string, url: string, port: number }>}
   */
  static resolveServiceUrls(services, networkName) {
    const urls = [];
    for (const [name, svc] of Object.entries(services)) {
      let port = 80;
      if (svc.ports?.length) {
        port = parseInt(String(svc.ports[0]).split(':').pop(), 10);
      }
      const protocol = [443, 8443].includes(port) ? 'https' : 'http';
      urls.push({ name, url: `${protocol}://${name}:${port}`, port });
    }
    return urls;
  }

  getInternalUrl(serviceName, port = 80) {
    const protocol = [443, 8443].includes(port) ? 'https' : 'http';
    return `${protocol}://${serviceName}:${port}`;
  }

  cleanup() {
    if (!this._containerId) return;
    for (const net of this._connectedNetworks) {
      try { spawnSync('docker', ['network', 'disconnect', '-f', net, this._containerId], { timeout: 5000, stdio: 'pipe' }); }
      catch { /* ignore */ }
    }
    this._connectedNetworks = [];
    try { spawnSync('docker', ['rm', '-f', this._containerId], { timeout: 10000, stdio: 'pipe' }); }
    catch { /* ignore */ }
    this._containerId = null;
  }
}
