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

/**
 * Challenge Registry — Unified catalog for multi-suite CTF benchmarks.
 *
 * Aggregates challenges from NYU CTF, PicoCTF, OverTheWire, and
 * any future suites into a single queryable registry.
 *
 * @module benchmark/registry
 */

import { BenchmarkConfig } from './models.js';

// ---------------------------------------------------------------------------
// Suite metadata
// ---------------------------------------------------------------------------

/**
 * Metadata about a registered benchmark suite.
 */
export class SuiteInfo {
  /**
   * @param {object} opts
   * @param {string} opts.id         - Suite identifier (e.g. 'nyu', 'pico', 'otw')
   * @param {string} opts.name       - Display name
   * @param {string} opts.source     - URL or description of data source
   * @param {string} opts.targetType - 'docker' | 'remote-ssh' | 'remote-http' | 'remote-netcat' | 'mixed'
   * @param {string[]} opts.categories - Available categories
   * @param {Function} opts.loader   - Function that returns BenchmarkConfig[]
   * @param {Function} [opts.cloner] - Optional function to clone/download challenge data
   */
  constructor({ id, name, source, targetType, categories = [], loader, cloner }) {
    this.id = id;
    this.name = name;
    this.source = source;
    this.targetType = targetType;
    this.categories = categories;
    this.loader = loader;
    this.cloner = cloner ?? null;
  }
}

// ---------------------------------------------------------------------------
// Registry
// ---------------------------------------------------------------------------

/**
 * Unified challenge registry across all benchmark suites.
 */
export class ChallengeRegistry {
  constructor() {
    /** @type {Map<string, SuiteInfo>} */
    this._suites = new Map();
    /** @type {Map<string, BenchmarkConfig[]>} */
    this._challenges = new Map();
  }

  /**
   * Register a benchmark suite.
   * @param {SuiteInfo} suite
   */
  registerSuite(suite) {
    this._suites.set(suite.id, suite);
  }

  /**
   * Get registered suite info.
   * @param {string} suiteId
   * @returns {SuiteInfo|undefined}
   */
  getSuite(suiteId) {
    return this._suites.get(suiteId);
  }

  /** Get all registered suite IDs. */
  get suiteIds() {
    return [...this._suites.keys()];
  }

  /** Get all registered suites. */
  get suites() {
    return [...this._suites.values()];
  }

  /**
   * Load challenges for a suite (calls the suite's loader function).
   * Results are cached — call refresh() to reload.
   *
   * @param {string} suiteId
   * @returns {BenchmarkConfig[]}
   */
  loadChallenges(suiteId) {
    const suite = this._suites.get(suiteId);
    if (!suite) throw new Error(`Unknown suite: ${suiteId}. Available: ${this.suiteIds.join(', ')}`);

    if (!this._challenges.has(suiteId)) {
      const configs = suite.loader();
      this._challenges.set(suiteId, configs);
    }
    return this._challenges.get(suiteId);
  }

  /**
   * Clear cached challenges for a suite (or all suites).
   * @param {string} [suiteId]
   */
  refresh(suiteId) {
    if (suiteId) {
      this._challenges.delete(suiteId);
    } else {
      this._challenges.clear();
    }
  }

  /**
   * Get all challenges across all loaded suites.
   * @returns {BenchmarkConfig[]}
   */
  allChallenges() {
    const all = [];
    for (const configs of this._challenges.values()) {
      all.push(...configs);
    }
    return all;
  }

  /**
   * Filter challenges by criteria.
   *
   * @param {object} filter
   * @param {string} [filter.suite]     - Suite ID
   * @param {string} [filter.category]  - Category name
   * @param {number} [filter.level]     - Difficulty level (1-5)
   * @param {string[]} [filter.tags]    - Required tags (all must match)
   * @param {string} [filter.nameMatch] - Substring match on name
   * @returns {BenchmarkConfig[]}
   */
  query(filter = {}) {
    let results = filter.suite
      ? this.loadChallenges(filter.suite)
      : this.allChallenges();

    if (filter.category) {
      const cat = filter.category.toLowerCase();
      results = results.filter(c => c.category === cat || c.tags.includes(cat));
    }

    if (filter.level) {
      results = results.filter(c => c.level === filter.level);
    }

    if (filter.tags?.length) {
      results = results.filter(c => filter.tags.every(t => c.tags.includes(t)));
    }

    if (filter.nameMatch) {
      const lc = filter.nameMatch.toLowerCase();
      results = results.filter(c => c.name.toLowerCase().includes(lc));
    }

    return results;
  }

  /**
   * Get statistics for a suite or all suites.
   * @param {string} [suiteId]
   * @returns {object}
   */
  stats(suiteId) {
    const challenges = suiteId
      ? this.loadChallenges(suiteId)
      : this.allChallenges();

    const byCategory = {};
    const byLevel = {};

    for (const c of challenges) {
      byCategory[c.category || 'uncategorized'] = (byCategory[c.category || 'uncategorized'] || 0) + 1;
      byLevel[c.level] = (byLevel[c.level] || 0) + 1;
    }

    return {
      total: challenges.length,
      byCategory,
      byLevel,
      suites: suiteId ? [suiteId] : this.suiteIds,
    };
  }

  /**
   * Clone/download challenge data for a suite.
   * @param {string} suiteId
   * @returns {Promise<boolean>}
   */
  async clone(suiteId) {
    const suite = this._suites.get(suiteId);
    if (!suite) throw new Error(`Unknown suite: ${suiteId}`);
    if (!suite.cloner) throw new Error(`Suite ${suiteId} does not support cloning`);
    return suite.cloner();
  }
}

// ---------------------------------------------------------------------------
// Default registry instance
// ---------------------------------------------------------------------------

let _defaultRegistry = null;

/**
 * Get the default challenge registry with all built-in suites registered.
 * @returns {ChallengeRegistry}
 */
export function getDefaultRegistry() {
  if (_defaultRegistry) return _defaultRegistry;
  _defaultRegistry = new ChallengeRegistry();
  return _defaultRegistry;
}
