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

/**
 * Remote Target — Connection adapters for external CTF challenge servers.
 *
 * Unlike NYU CTF which runs in Docker containers, many CTF platforms
 * host challenges on remote servers accessible via SSH, netcat, or HTTP.
 * This module provides a unified interface for connecting to them.
 *
 * @module benchmark/remote-target
 */

import { spawnSync, spawn } from 'node:child_process';

// ---------------------------------------------------------------------------
// Base class
// ---------------------------------------------------------------------------

/**
 * Abstract remote target connection.
 * Subclasses implement connect(), exec(), and cleanup().
 */
export class RemoteTarget {
  /**
   * @param {object} opts
   * @param {string} opts.type       - 'ssh' | 'netcat' | 'http'
   * @param {string} opts.host       - Hostname or IP
   * @param {number} opts.port       - Port number
   * @param {string} [opts.username] - SSH username
   * @param {string} [opts.password] - SSH password or challenge password
   * @param {object} [opts.metadata] - Additional target-specific info
   */
  constructor({ type, host, port, username, password, metadata = {} }) {
    this.type = type;
    this.host = host;
    this.port = port;
    this.username = username ?? '';
    this.password = password ?? '';
    this.metadata = metadata;
    this._connected = false;
  }

  get connected() { return this._connected; }
  get url() { return `${this.type}://${this.host}:${this.port}`; }

  /** Connect to the remote target. */
  async connect() { throw new Error('Subclass must implement connect()'); }

  /**
   * Execute a command against the remote target.
   * @param {string} command
   * @param {number} [timeout] - Timeout in seconds
   * @returns {Promise<{ exitCode: number, stdout: string, stderr: string }>}
   */
  async exec(command, timeout = 30) { throw new Error('Subclass must implement exec()'); }

  /** Clean up connection resources. */
  async cleanup() { this._connected = false; }
}

// ---------------------------------------------------------------------------
// SSH Target
// ---------------------------------------------------------------------------

/**
 * SSH-based remote target (OverTheWire, SSH-accessible challenges).
 *
 * Uses sshpass + ssh for password-based auth, or ssh with key-based auth.
 * Each exec() call spawns a fresh SSH connection (stateless).
 */
export class SSHTarget extends RemoteTarget {
  constructor({ host, port = 22, username, password, keyPath, metadata = {} }) {
    super({ type: 'ssh', host, port, username, password, metadata });
    this.keyPath = keyPath ?? '';
  }

  async connect() {
    // Verify connectivity with a simple echo
    const result = await this.exec('echo __NERF_CONNECTED__', 10);
    if ((result.stdout || '').includes('__NERF_CONNECTED__')) {
      this._connected = true;
      return true;
    }
    throw new Error(`SSH connection failed to ${this.host}:${this.port}: ${result.stderr}`);
  }

  async exec(command, timeout = 30) {
    const sshArgs = [
      '-o', 'StrictHostKeyChecking=no',
      '-o', 'UserKnownHostsFile=/dev/null',
      '-o', 'LogLevel=ERROR',
      '-o', `ConnectTimeout=${Math.min(timeout, 10)}`,
      '-p', String(this.port),
    ];

    if (this.keyPath) {
      sshArgs.push('-i', this.keyPath);
    }

    sshArgs.push(`${this.username}@${this.host}`, command);

    let args, cmd;
    if (this.password && !this.keyPath) {
      // Use sshpass for password auth
      cmd = 'sshpass';
      args = ['-p', this.password, 'ssh', ...sshArgs];
    } else {
      cmd = 'ssh';
      args = sshArgs;
    }

    const result = spawnSync(cmd, args, {
      timeout: timeout * 1000,
      stdio: 'pipe',
      maxBuffer: 5 * 1024 * 1024,
    });

    return {
      exitCode: result.status ?? -1,
      stdout: (result.stdout || '').toString(),
      stderr: (result.stderr || '').toString(),
    };
  }
}

// ---------------------------------------------------------------------------
// Netcat Target
// ---------------------------------------------------------------------------

/**
 * Netcat-based remote target (interactive TCP challenges).
 *
 * Many CTF challenges expose a service on a TCP port that you interact with
 * by sending/receiving text. This wraps netcat for that purpose.
 */
export class NetcatTarget extends RemoteTarget {
  constructor({ host, port, metadata = {} }) {
    super({ type: 'netcat', host, port, metadata });
  }

  async connect() {
    // Verify port is reachable
    const result = spawnSync('nc', ['-z', '-w', '5', this.host, String(this.port)], {
      timeout: 10000, stdio: 'pipe',
    });
    this._connected = result.status === 0;
    if (!this._connected) {
      throw new Error(`Netcat connection failed to ${this.host}:${this.port}`);
    }
    return true;
  }

  /**
   * Send input to a netcat service and capture output.
   * @param {string} input - Text to send to the service
   * @param {number} [timeout] - Timeout in seconds
   */
  async exec(input, timeout = 15) {
    const result = spawnSync('nc', ['-w', String(timeout), this.host, String(this.port)], {
      input: input + '\n',
      timeout: timeout * 1000,
      stdio: ['pipe', 'pipe', 'pipe'],
      maxBuffer: 1 * 1024 * 1024,
    });

    return {
      exitCode: result.status ?? -1,
      stdout: (result.stdout || '').toString(),
      stderr: (result.stderr || '').toString(),
    };
  }
}

// ---------------------------------------------------------------------------
// HTTP Target
// ---------------------------------------------------------------------------

/**
 * HTTP-based remote target (web exploitation challenges).
 *
 * Wraps curl for HTTP interactions. Supports GET, POST, custom headers.
 */
export class HTTPTarget extends RemoteTarget {
  constructor({ host, port = 80, protocol = 'http', metadata = {} }) {
    super({ type: 'http', host, port, metadata });
    this.protocol = protocol;
  }

  get url() { return `${this.protocol}://${this.host}:${this.port}`; }

  async connect() {
    const result = spawnSync('curl', [
      '-s', '-o', '/dev/null', '-w', '%{http_code}',
      '--connect-timeout', '10', '--max-time', '15',
      this.url,
    ], { timeout: 20000, stdio: 'pipe' });

    const statusCode = parseInt((result.stdout || '').toString().trim(), 10);
    this._connected = statusCode > 0 && statusCode < 600;
    if (!this._connected) {
      throw new Error(`HTTP connection failed to ${this.url}`);
    }
    return true;
  }

  /**
   * Execute an HTTP request.
   * @param {string} command - curl-style command args or a URL path
   * @param {number} [timeout] - Timeout in seconds
   */
  async exec(command, timeout = 30) {
    // If command starts with '/' treat as a path on this target
    let curlArgs;
    if (command.startsWith('/') || command.startsWith('http')) {
      const targetUrl = command.startsWith('http') ? command : `${this.url}${command}`;
      curlArgs = ['-s', '--max-time', String(timeout), targetUrl];
    } else {
      // Raw curl args
      curlArgs = ['-s', '--max-time', String(timeout), ...command.split(/\s+/)];
    }

    const result = spawnSync('curl', curlArgs, {
      timeout: timeout * 1000,
      stdio: 'pipe',
      maxBuffer: 5 * 1024 * 1024,
    });

    return {
      exitCode: result.status ?? -1,
      stdout: (result.stdout || '').toString(),
      stderr: (result.stderr || '').toString(),
    };
  }
}

// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------

/**
 * Create a RemoteTarget from a challenge config.
 *
 * @param {object} opts
 * @param {string} opts.type - 'ssh' | 'netcat' | 'http'
 * @param {string} opts.host
 * @param {number} opts.port
 * @param {string} [opts.username]
 * @param {string} [opts.password]
 * @param {string} [opts.keyPath]
 * @param {string} [opts.protocol]
 * @param {object} [opts.metadata]
 * @returns {RemoteTarget}
 */
export function createRemoteTarget(opts) {
  switch (opts.type) {
    case 'ssh':    return new SSHTarget(opts);
    case 'netcat': return new NetcatTarget(opts);
    case 'http':   return new HTTPTarget(opts);
    default: throw new Error(`Unknown target type: ${opts.type}`);
  }
}
