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

/**
 * OverTheWire — Challenge loader for the OverTheWire wargames.
 *
 * OverTheWire hosts SSH-based wargames with progressive difficulty levels.
 * Each level requires solving the current challenge to obtain the password
 * for the next level.
 *
 * Supported wargames:
 * - Bandit (34 levels) — Linux basics, SSH, file operations
 * - Natas (34 levels) — Server-side web security
 * - Leviathan (8 levels) — Binary exploitation basics
 * - Krypton (7 levels) — Classical cryptography
 * - Narnia (10 levels) — Basic exploitation
 *
 * @module benchmark/overthewire
 */

import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { BenchmarkConfig } from './models.js';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

export const OTW_DATA_DIR = join(homedir(), '.nerf', 'benchmarks', 'overthewire');
export const OTW_PROGRESS_FILE = join(OTW_DATA_DIR, 'progress.json');

export const OTW_HOST = 'bandit.labs.overthewire.org';
export const OTW_BASE_DOMAIN = 'overthewire.org';

/** OverTheWire wargame definitions. */
export const OTW_WARGAMES = {
  bandit: {
    name: 'Bandit',
    host: 'bandit.labs.overthewire.org',
    port: 2220,
    levels: 34,
    category: 'general-skills',
    description: 'Linux command line basics — files, permissions, SSH, networking',
    startPassword: 'bandit0',
    levelDescriptions: [
      'Read the readme file in the home directory.',
      'Find the password in a file with dashes in the name.',
      'Find the password in a hidden file.',
      'Find the human-readable file among several files.',
      'Find the only human-readable, non-executable, 1033-byte file.',
      'Find the file owned by a specific user and group.',
      'Find the file somewhere on the server with specific properties.',
      'Find the password in a file among lines of text.',
      'Find the unique line that occurs only once in the file.',
      'Find the base64-encoded password in data.txt.',
      'Decode the ROT13 encoded password.',
      'Decode the hexdump to extract the password.',
      'Decompress a series of compressed files.',
      'Use the SSH key to connect to the next level.',
      'Retrieve the password using the setuid binary with a port argument.',
      'Record the password by submitting the current password to a port.',
      'Read the password from /etc/bandit_pass/ using the setuid binary.',
      'Use the setuid binary with a correct command to read the password.',
      'Find the cron job and trace it to the password.',
      'Analyze the cron job script to find the password.',
      'Analyze the cron job that generates a password based on date.',
      'Use a shell escape in a setuid program.',
      'Use the daemon listening on localhost to get the next password.',
      'Brute-force or script the daemon with port and password.',
      'Crack the encrypted key using SSH agent forwarding.',
      'Use the git repository to find the password.',
      'Navigate git history to find the password.',
      'Explore git tags and branches for the password.',
      'Exploit a setuid script that doesn\'t sanitize input properly.',
      'Read the password with a race condition.',
      'Write a shell script to exploit the setuid binary.',
      'Network service exploitation.',
      'Advanced git exploration.',
      'Final bandit level.',
    ],
  },

  natas: {
    name: 'Natas',
    host: 'natas0.natas.labs.overthewire.org',
    port: 80,
    levels: 34,
    category: 'web-exploitation',
    description: 'Server-side web security — source disclosure, injection, auth bypass',
    startPassword: 'natas0',
    levelDescriptions: [
      'View page source to find the password.',
      'Find the password in the page source.',
      'Find the hidden page listed in robots.txt.',
      'Find the password in the page source of a hidden page.',
      'Bypass server-side access control.',
      'Include a remote/local file to read the password.',
      'Exploit an input validation flaw.',
      'Use directory traversal to read files.',
      'Exploit a weak session or authentication mechanism.',
      'Find secret data in server source code.',
    ],
  },

  leviathan: {
    name: 'Leviathan',
    host: 'leviathan.labs.overthewire.org',
    port: 2223,
    levels: 8,
    category: 'binary-exploitation',
    description: 'Basic exploitation — setuid, ltrace, simple buffer overflows',
    startPassword: '',
    levelDescriptions: [
      'Find the hidden directory and decode the bookmarks file.',
      'Use ltrace to find the password checked by the setuid binary.',
      'Exploit the setuid binary to read the password file.',
      'Trick the binary with a symlink.',
      'Disassemble or trace the binary for the password.',
      'Run the setuid binary correctly to get the password.',
      'Use the binary with a specific trick.',
      'Final leviathan level.',
    ],
  },

  krypton: {
    name: 'Krypton',
    host: 'krypton.labs.overthewire.org',
    port: 2231,
    levels: 7,
    category: 'cryptography',
    description: 'Classical cryptography — Caesar, Vigenère, frequency analysis',
    startPassword: 'KRYPTONISGREAT',
    levelDescriptions: [
      'Decode base64.',
      'Decrypt ROT13 / Caesar cipher.',
      'Break a Caesar cipher with known plaintext.',
      'Break a Vigenère cipher.',
      'Exploit a stream cipher.',
      'Break a more complex cipher.',
      'Final krypton level.',
    ],
  },

  narnia: {
    name: 'Narnia',
    host: 'narnia.labs.overthewire.org',
    port: 2226,
    levels: 10,
    category: 'binary-exploitation',
    description: 'Basic exploitation — buffer overflows, format strings, shellcode',
    startPassword: '',
    levelDescriptions: [
      'Set environment variable to the right value.',
      'Buffer overflow to overwrite a variable.',
      'Buffer overflow to redirect execution.',
      'Buffer overflow with shellcode.',
      'Format string vulnerability.',
      'Stack-based exploitation.',
      'Advanced buffer overflow.',
      'Return-to-libc.',
      'Advanced exploitation.',
      'Final narnia level.',
    ],
  },
};

// ---------------------------------------------------------------------------
// Level-to-difficulty mapping
// ---------------------------------------------------------------------------

/**
 * Map a wargame level number to difficulty 1-5.
 * Earlier levels are easier, later levels are harder.
 *
 * @param {number} level - Level number (0-indexed)
 * @param {number} totalLevels - Total levels in the wargame
 * @returns {number} Difficulty 1-5
 */
function levelToDifficulty(level, totalLevels) {
  const pct = level / (totalLevels - 1 || 1);
  if (pct <= 0.2) return 1;
  if (pct <= 0.4) return 2;
  if (pct <= 0.6) return 3;
  if (pct <= 0.8) return 4;
  return 5;
}

// ---------------------------------------------------------------------------
// Loader
// ---------------------------------------------------------------------------

/**
 * Generate BenchmarkConfig entries for a single wargame.
 *
 * @param {string} wargameId - Key in OTW_WARGAMES
 * @returns {BenchmarkConfig[]}
 */
export function loadOtwWargame(wargameId) {
  const wg = OTW_WARGAMES[wargameId];
  if (!wg) throw new Error(`Unknown wargame: ${wargameId}. Available: ${Object.keys(OTW_WARGAMES).join(', ')}`);

  const configs = [];
  for (let i = 0; i < wg.levels; i++) {
    const username = `${wargameId}${i}`;
    const description = wg.levelDescriptions?.[i] || `Level ${i} of ${wg.name}`;

    configs.push(new BenchmarkConfig({
      name: `${wg.name} Level ${i}`,
      description,
      level: levelToDifficulty(i, wg.levels),
      tags: [wg.category, 'overthewire', wargameId],
      winCondition: 'flag', // The "flag" is the next level's password
      files: [],
      path: OTW_DATA_DIR,
      category: wg.category,
    }));
  }

  return configs;
}

/**
 * Load all OverTheWire challenges across all wargames.
 * @returns {BenchmarkConfig[]}
 */
export function enumerateOtwChallenges() {
  const all = [];
  for (const id of Object.keys(OTW_WARGAMES)) {
    all.push(...loadOtwWargame(id));
  }
  return all;
}

/**
 * Get SSH connection info for an OverTheWire level.
 *
 * @param {string} wargameId
 * @param {number} level
 * @returns {{ host: string, port: number, username: string, password: string|null }}
 */
export function getOtwConnectionInfo(wargameId, level) {
  const wg = OTW_WARGAMES[wargameId];
  if (!wg) throw new Error(`Unknown wargame: ${wargameId}`);

  const username = `${wargameId}${level}`;
  const progress = loadProgress();
  const password = level === 0
    ? wg.startPassword
    : progress[wargameId]?.[level] ?? null;

  // Natas uses per-level hostnames
  let host = wg.host;
  if (wargameId === 'natas') {
    host = `natas${level}.natas.labs.overthewire.org`;
  }

  return { host, port: wg.port, username, password };
}

// ---------------------------------------------------------------------------
// Progress tracking
// ---------------------------------------------------------------------------

/**
 * Load saved progress (discovered passwords per level).
 * @returns {object}
 */
export function loadProgress() {
  if (!existsSync(OTW_PROGRESS_FILE)) return {};
  try {
    return JSON.parse(readFileSync(OTW_PROGRESS_FILE, 'utf8'));
  } catch {
    return {};
  }
}

/**
 * Save a discovered password for a level.
 *
 * @param {string} wargameId
 * @param {number} level
 * @param {string} password
 */
export function saveProgress(wargameId, level, password) {
  mkdirSync(OTW_DATA_DIR, { recursive: true });
  const progress = loadProgress();
  if (!progress[wargameId]) progress[wargameId] = {};
  progress[wargameId][level] = password;
  // Atomic write — write to tmp then rename (BENCH-009)
  const tmpFile = `${OTW_PROGRESS_FILE}.tmp`;
  writeFileSync(tmpFile, JSON.stringify(progress, null, 2));
  renameSync(tmpFile, OTW_PROGRESS_FILE);
}

/**
 * Get progress stats across all wargames.
 * @returns {object}
 */
export function getProgressStats() {
  const progress = loadProgress();
  const stats = {};

  for (const [id, wg] of Object.entries(OTW_WARGAMES)) {
    const solved = progress[id] ? Object.keys(progress[id]).length : 0;
    stats[id] = {
      name: wg.name,
      total: wg.levels,
      solved,
      pct: wg.levels > 0 ? Math.round((solved / wg.levels) * 100) : 0,
      category: wg.category,
    };
  }

  return stats;
}

/**
 * Initialize OverTheWire data directory.
 * @returns {boolean}
 */
export function cloneOtw() {
  mkdirSync(OTW_DATA_DIR, { recursive: true });
  // Save initial progress file if it doesn't exist
  if (!existsSync(OTW_PROGRESS_FILE)) {
    writeFileSync(OTW_PROGRESS_FILE, '{}');
  }
  return true;
}

/**
 * Get catalog stats for all OTW wargames.
 * @returns {object}
 */
export function getOtwCatalogStats() {
  const challenges = enumerateOtwChallenges();
  const byCategory = {};
  const byLevel = {};
  const byWargame = {};

  for (const c of challenges) {
    byCategory[c.category] = (byCategory[c.category] || 0) + 1;
    byLevel[c.level] = (byLevel[c.level] || 0) + 1;
    const wg = c.tags.find(t => OTW_WARGAMES[t]);
    if (wg) byWargame[wg] = (byWargame[wg] || 0) + 1;
  }

  return { total: challenges.length, byCategory, byLevel, byWargame };
}
