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

/**
 * NERF Benchmark — Solver adapters.
 */

import { SolverResult } from './models.js';
import { createInterface } from 'node:readline';

/** Abstract solver base. Subclasses must implement name and solve(). */
export class SolverAdapter {
  get name() { throw new Error('not implemented'); }
  solve(config, targetUrl, expectedFlag) { throw new Error('not implemented'); }
}

/** Testing solver — returns the expected flag immediately. */
export class StubSolver extends SolverAdapter {
  get name() { return 'stub'; }
  solve(config, targetUrl, expectedFlag) {
    const start = performance.now();
    return new SolverResult({
      flagFound: true,
      flagValue: expectedFlag,
      durationS: (performance.now() - start) / 1000,
      steps: ['stub: returned injected flag'],
    });
  }
}

/** Interactive solver — prompts for the flag. */
export class ManualSolver extends SolverAdapter {
  get name() { return 'manual'; }
  solve(config, targetUrl, expectedFlag) {
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    return new Promise((resolve) => {
      console.log(`\n${'='.repeat(60)}`);
      console.log(`  Challenge: ${config.name}`);
      console.log(`  Level: ${config.level}`);
      console.log(`  Tags: ${config.tags.join(', ')}`);
      console.log(`  Description: ${config.description}`);
      console.log(`  Target: ${targetUrl}`);
      console.log(`${'='.repeat(60)}\n`);
      const start = performance.now();
      rl.question('Enter the flag (or "skip"): ', (answer) => {
        rl.close();
        const duration = (performance.now() - start) / 1000;
        if (!answer || answer.toLowerCase() === 'skip') {
          resolve(new SolverResult({ durationS: duration, steps: ['manual: skipped'] }));
        } else {
          resolve(new SolverResult({ flagFound: true, flagValue: answer.trim(), durationS: duration, steps: ['manual: user provided flag'] }));
        }
      });
    });
  }
}

/** AI-powered solver using SecurityAgent in a Docker sandbox. */
export class AutonomousSolver extends SolverAdapter {
  constructor({ backend, maxTurns = 30 } = {}) {
    super();
    this._backend = backend;
    this._maxTurns = maxTurns;
    this._supervisedHook = null;
  }
  get name() { return 'auto'; }
  async solve(config, targetUrl, expectedFlag) {
    const { SecurityAgent } = await import('./agent.js');
    const { makeAgentClient } = await import('./llm.js');
    const { SandboxContainer } = await import('./sandbox.js');
    const { readFileSync } = await import('node:fs');
    const yaml = await import('yaml');

    const start = performance.now();
    const composePath = `${config.path}/docker-compose.yml`;
    const data = yaml.parse(readFileSync(composePath, 'utf8'));
    const services = data.services || {};
    let serviceName = Object.keys(services)[0] || 'web';
    let internalPort = 80;
    for (const [name, svc] of Object.entries(services)) {
      if (svc.ports?.length) {
        serviceName = name;
        const portSpec = String(svc.ports[0]);
        internalPort = parseInt(portSpec.split(':').pop(), 10);
        break;
      }
    }
    const networkName = `nerf-bench-${config.dirName}_default`.toLowerCase();
    const { client, model } = await makeAgentClient({ backendOverride: this._backend });
    const sandbox = new SandboxContainer({ nameSuffix: config.dirName.toLowerCase() });
    try {
      sandbox.create();
      sandbox.connectNetwork(networkName);
      const internalUrl = sandbox.getInternalUrl(serviceName, internalPort);
      const agent = new SecurityAgent({ client, model, sandbox, maxTurns: this._maxTurns, preToolHook: this._supervisedHook });
      const result = await agent.run({ targetUrl: internalUrl, challengeDescription: config.description, tags: config.tags || [], winCondition: config.winCondition || 'flag' });
      return new SolverResult({
        flagFound: result.flagFound, flagValue: result.flagValue,
        durationS: (performance.now() - start) / 1000,
        tokensIn: result.tokensIn, tokensOut: result.tokensOut,
        toolCalls: result.toolCalls, steps: result.steps, error: result.error,
      });
    } catch (e) {
      return new SolverResult({ durationS: (performance.now() - start) / 1000, error: String(e), steps: [`[error] ${e}`] });
    } finally {
      sandbox.cleanup();
    }
  }
}

/** Multi-agent solver using phased coordinator. */
export class MultiAgentSolver extends SolverAdapter {
  constructor({ backend, reconTurns = 10, exploitTurns = 25 } = {}) {
    super();
    this._backend = backend;
    this._reconTurns = reconTurns;
    this._exploitTurns = exploitTurns;
    this._supervisedHook = null;
  }
  get name() { return 'auto-multi'; }
  async solve(config, targetUrl, expectedFlag) {
    const { Coordinator } = await import('./coordinator.js');
    const { makeAgentClient } = await import('./llm.js');
    const { SandboxContainer } = await import('./sandbox.js');
    const { readFileSync } = await import('node:fs');
    const yaml = await import('yaml');

    const start = performance.now();
    const composePath = `${config.path}/docker-compose.yml`;
    const data = yaml.parse(readFileSync(composePath, 'utf8'));
    const services = data.services || {};
    let serviceName = Object.keys(services)[0] || 'web';
    let internalPort = 80;
    for (const [name, svc] of Object.entries(services)) {
      if (svc.ports?.length) { serviceName = name; internalPort = parseInt(String(svc.ports[0]).split(':').pop(), 10); break; }
    }
    const networkName = `nerf-bench-${config.dirName}_default`.toLowerCase();
    const { client, model } = await makeAgentClient({ backendOverride: this._backend });
    const sandbox = new SandboxContainer({ nameSuffix: config.dirName.toLowerCase() });
    try {
      sandbox.create();
      sandbox.connectNetwork(networkName);
      const internalUrl = sandbox.getInternalUrl(serviceName, internalPort);
      const coordinator = new Coordinator({ client, model, sandbox, reconTurns: this._reconTurns, exploitTurns: this._exploitTurns, preToolHook: this._supervisedHook });
      const result = await coordinator.solve({ targetUrl: internalUrl, challengeDescription: config.description, tags: config.tags || [], winCondition: config.winCondition || 'flag' });
      return new SolverResult({
        flagFound: result.flagFound, flagValue: result.flagValue,
        durationS: (performance.now() - start) / 1000,
        tokensIn: result.tokensIn, tokensOut: result.tokensOut,
        toolCalls: result.toolCalls, steps: result.steps, error: result.error,
      });
    } catch (e) {
      return new SolverResult({ durationS: (performance.now() - start) / 1000, error: String(e), steps: [`[error] ${e}`] });
    } finally {
      sandbox.cleanup();
    }
  }
}

export const SOLVERS = { stub: StubSolver, manual: ManualSolver, auto: AutonomousSolver, 'auto-multi': MultiAgentSolver };

// Lazy-load RemoteSolver to avoid circular imports
export function getSolver(name, opts = {}) {
  if (name === 'remote') {
    return new RemoteSolverProxy(opts);
  }
  if (name === 'claude-code') {
    return new ClaudeCodeSolverProxy(opts);
  }
  const Cls = SOLVERS[name];
  if (!Cls) throw new Error(`Unknown solver: ${name}. Available: ${[...Object.keys(SOLVERS), 'remote', 'claude-code'].join(', ')}`);
  return new Cls(opts);
}

/** Proxy that lazy-loads RemoteSolver on first solve(). */
class RemoteSolverProxy extends SolverAdapter {
  constructor(opts) { super(); this._opts = opts; this._impl = null; }
  get name() { return 'remote'; }
  async solve(config, targetUrl, expectedFlag) {
    if (!this._impl) {
      const { RemoteSolver } = await import('./remote-solver.js');
      this._impl = new RemoteSolver(this._opts);
    }
    return this._impl.solve(config, targetUrl, expectedFlag);
  }
}

/** Proxy that lazy-loads ClaudeCodeSolver on first solve(). */
class ClaudeCodeSolverProxy extends SolverAdapter {
  constructor(opts) { super(); this._opts = opts; this._impl = null; }
  get name() { return 'claude-code'; }
  async solve(config, targetUrl, expectedFlag) {
    if (!this._impl) {
      const { ClaudeCodeSolver } = await import('./claude-code-solver.js');
      this._impl = new ClaudeCodeSolver(this._opts);
    }
    return this._impl.solve(config, targetUrl, expectedFlag);
  }
}
