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

/**
 * NERF Nuclei Template Manager — pull, version, and manage nuclei templates.
 *
 * Provides template management beyond basic execution:
 * - Pull/update templates from nuclei-templates repository
 * - Custom template generation from scan findings
 * - Template categorization and search
 * - Workflow chain composition
 */

import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { execFileSync } from 'node:child_process';
import { homedir } from 'node:os';

// Default template directory
const DEFAULT_TEMPLATE_DIR =
  process.env.NERF_TEMPLATE_DIR ||
  join(homedir(), '.config', 'nerf', 'nuclei-templates');

// ---------------------------------------------------------------------------
// Data classes
// ---------------------------------------------------------------------------

class TemplateInfo {
  /**
   * @param {object} opts
   * @param {string} opts.id
   * @param {string} opts.name
   * @param {string} [opts.severity]
   * @param {string[]} [opts.tags]
   * @param {string} [opts.author]
   * @param {string} [opts.description]
   * @param {string[]} [opts.reference]
   * @param {string} [opts.filePath]
   * @param {string} [opts.protocol]
   * @param {string} [opts.category]
   */
  constructor(opts = {}) {
    this.id = opts.id || '';
    this.name = opts.name || '';
    this.severity = opts.severity || 'info';
    this.tags = opts.tags || [];
    this.author = opts.author || '';
    this.description = opts.description || '';
    this.reference = opts.reference || [];
    this.filePath = opts.filePath || '';
    this.protocol = opts.protocol || 'http';
    this.category = opts.category || '';
  }

  toDict() {
    return {
      id: this.id,
      name: this.name,
      severity: this.severity,
      tags: this.tags,
      author: this.author,
      description: this.description,
      reference: this.reference,
      file_path: this.filePath,
      protocol: this.protocol,
      category: this.category,
    };
  }
}

class TemplateCollection {
  /**
   * @param {object} opts
   * @param {string} [opts.name]
   * @param {string} [opts.version]
   * @param {TemplateInfo[]} [opts.templates]
   * @param {string} [opts.updatedAt]
   * @param {string} [opts.source]
   */
  constructor(opts = {}) {
    this.name = opts.name || '';
    this.version = opts.version || '';
    this.templates = opts.templates || [];
    this.updatedAt = opts.updatedAt || new Date().toISOString();
    this.source = opts.source || '';
  }

  get count() {
    return this.templates.length;
  }

  bySeverity(severity) {
    return this.templates.filter((t) => t.severity === severity);
  }

  byTag(tag) {
    return this.templates.filter((t) => t.tags.includes(tag));
  }

  byProtocol(protocol) {
    return this.templates.filter((t) => t.protocol === protocol);
  }

  search(query) {
    const q = query.toLowerCase();
    return this.templates.filter(
      (t) =>
        t.id.toLowerCase().includes(q) ||
        t.name.toLowerCase().includes(q) ||
        t.description.toLowerCase().includes(q) ||
        t.tags.some((tag) => tag.toLowerCase().includes(q)),
    );
  }

  toDict() {
    return {
      name: this.name,
      version: this.version,
      count: this.count,
      updated_at: this.updatedAt,
      source: this.source,
      severity_breakdown: {
        critical: this.bySeverity('critical').length,
        high: this.bySeverity('high').length,
        medium: this.bySeverity('medium').length,
        low: this.bySeverity('low').length,
        info: this.bySeverity('info').length,
      },
    };
  }
}

// ---------------------------------------------------------------------------
// NucleiTemplateManager
// ---------------------------------------------------------------------------

/**
 * Resolve the nuclei binary path.
 * @returns {string|null}
 */
function _whichNuclei() {
  try {
    const result = execFileSync('which', ['nuclei'], {
      encoding: 'utf8',
      timeout: 5000,
    });
    return result.trim() || null;
  } catch {
    return null;
  }
}

/**
 * Recursively list all .yaml files under a directory.
 * @param {string} dir
 * @returns {string[]}
 */
function _walkYaml(dir) {
  const results = [];
  if (!existsSync(dir)) return results;

  let entries;
  try {
    entries = readdirSync(dir, { withFileTypes: true });
  } catch {
    return results;
  }

  for (const entry of entries) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(..._walkYaml(fullPath));
    } else if (entry.isFile() && entry.name.endsWith('.yaml')) {
      results.push(fullPath);
    }
  }
  return results;
}

class NucleiTemplateManager {
  /**
   * @param {object} [opts]
   * @param {string} [opts.templateDir]
   */
  constructor(opts = {}) {
    this.templateDir = opts.templateDir || DEFAULT_TEMPLATE_DIR;
    this._nucleiPath = _whichNuclei();
    /** @type {TemplateCollection|null} */
    this._collection = null;
  }

  /** @returns {boolean} */
  get available() {
    return this._nucleiPath !== null;
  }

  /** @returns {boolean} */
  get templatesExist() {
    if (!existsSync(this.templateDir)) return false;
    // Check for at least one .yaml file (shallow check)
    return _walkYaml(this.templateDir).length > 0;
  }

  /**
   * Pull or update nuclei templates from the official repository.
   * @param {boolean} [force=false]
   * @returns {object}
   */
  pullTemplates(force = false) {
    if (this.templatesExist && !force) {
      return {
        status: 'already_exists',
        path: this.templateDir,
        message: 'Templates already present. Use force=true to update.',
      };
    }

    // Try nuclei -update-templates first
    if (this._nucleiPath) {
      try {
        execFileSync(this._nucleiPath, ['-update-templates', '-ud', this.templateDir], {
          encoding: 'utf8',
          timeout: 300000,
          stdio: ['pipe', 'pipe', 'pipe'],
        });
        const count = _walkYaml(this.templateDir).length;
        return {
          status: 'updated',
          path: this.templateDir,
          template_count: count,
          method: 'nuclei-update',
        };
      } catch (e) {
        // Fall through to git
      }
    }

    // Fallback: git clone
    let gitPath;
    try {
      gitPath = execFileSync('which', ['git'], { encoding: 'utf8', timeout: 5000 }).trim();
    } catch {
      gitPath = null;
    }

    if (gitPath) {
      try {
        if (existsSync(join(this.templateDir, '.git'))) {
          execFileSync(gitPath, ['-C', this.templateDir, 'pull', '--depth=1'], {
            encoding: 'utf8',
            timeout: 300000,
            stdio: ['pipe', 'pipe', 'pipe'],
          });
        } else {
          execFileSync(
            gitPath,
            [
              'clone',
              '--depth=1',
              'https://github.com/projectdiscovery/nuclei-templates.git',
              this.templateDir,
            ],
            { encoding: 'utf8', timeout: 300000, stdio: ['pipe', 'pipe', 'pipe'] },
          );
        }
        const count = _walkYaml(this.templateDir).length;
        return {
          status: 'cloned',
          path: this.templateDir,
          template_count: count,
          method: 'git-clone',
        };
      } catch {
        // Fall through
      }
    }

    return {
      status: 'error',
      message: 'Neither nuclei nor git available for template pull',
    };
  }

  /**
   * Scan template directory and build an indexed collection.
   * @returns {TemplateCollection}
   */
  indexTemplates() {
    if (!this.templatesExist) {
      return new TemplateCollection({ name: 'empty', version: '0.0.0' });
    }

    const templates = [];
    const yamlFiles = _walkYaml(this.templateDir).sort();
    for (const yamlFile of yamlFiles) {
      const info = this._parseTemplateFile(yamlFile);
      if (info) templates.push(info);
    }

    this._collection = new TemplateCollection({
      name: 'nuclei-templates',
      version: this._detectVersion(),
      templates,
      source: this.templateDir,
    });
    return this._collection;
  }

  /**
   * Search templates by ID, name, description, or tags.
   * @param {string} query
   * @returns {object[]}
   */
  searchTemplates(query) {
    if (!this._collection) this.indexTemplates();
    if (!this._collection) return [];
    return this._collection.search(query).slice(0, 50).map((t) => t.toDict());
  }

  /**
   * Get template paths suitable for a scan type.
   * @param {string} [scanType='pentest']
   * @param {string} [severity]
   * @param {string[]} [tags]
   * @returns {string[]}
   */
  getTemplatesForScan(scanType = 'pentest', severity, tags) {
    const SCAN_TYPE_TAGS = {
      pentest: ['cve', 'sqli', 'xss', 'rce', 'lfi', 'ssrf', 'injection'],
      recon: ['tech', 'detect', 'fingerprint', 'exposure', 'panel'],
      compliance: ['misconfig', 'default-login', 'exposure', 'unauth'],
      fuzzing: ['fuzz', 'brute', 'spray'],
      cve: ['cve'],
      exposure: ['exposure', 'misconfig', 'default-login', 'unauth', 'panel'],
    };

    const targetTags = [...(SCAN_TYPE_TAGS[scanType] || ['cve'])];
    if (tags) targetTags.push(...tags);

    if (!this._collection) this.indexTemplates();
    if (!this._collection) return [];

    const matched = [];
    for (const t of this._collection.templates) {
      if (severity && t.severity !== severity) continue;
      if (t.tags.some((tag) => targetTags.includes(tag))) {
        if (t.filePath) matched.push(t.filePath);
      }
    }
    return matched.slice(0, 500);
  }

  /**
   * Generate a custom nuclei template from a security finding.
   * @param {object} finding
   * @returns {string}
   */
  generateTemplate(finding) {
    const title = (finding.title || 'custom-check').toLowerCase().replace(/\s+/g, '-');
    const desc = finding.description || 'Custom security check';
    const severity = finding.severity || 'medium';
    const method = (finding.method || 'GET').toUpperCase();
    const path = finding.path || '/';
    const matchers = finding.matchers || [];

    let matcherYaml;
    if (matchers.length) {
      matcherYaml = matchers
        .map((m) => `      - type: word\n        words:\n          - "${m}"`)
        .join('\n');
    } else {
      matcherYaml = '      - type: status\n        status:\n          - 200';
    }

    return `id: nerf-${title}

info:
  name: ${finding.title || 'Custom Check'}
  author: nerf-auto
  severity: ${severity}
  description: |
    ${desc}
  tags: custom,nerf-generated
  metadata:
    generated-by: nerf-template-manager
    generated-at: ${new Date().toISOString()}

http:
  - method: ${method}
    path:
      - "{{BaseURL}}${path}"
    matchers:
${matcherYaml}
`;
  }

  /**
   * Generate a nuclei workflow YAML that chains multiple templates.
   * @param {string} name
   * @param {string[]} templateIds
   * @returns {string}
   */
  generateWorkflow(name, templateIds) {
    const subtemplates = templateIds
      .map((tid) => `      - template: ${tid}`)
      .join('\n');
    return `id: nerf-workflow-${name}

info:
  name: NERF Workflow - ${name}
  author: nerf
  severity: info

workflows:
  - template: ${templateIds[0] || 'missing'}
    subtemplates:
${subtemplates}
`;
  }

  /**
   * Return template collection statistics.
   * @returns {object}
   */
  stats() {
    if (!this._collection) this.indexTemplates();
    if (!this._collection) {
      return { status: 'no_templates', path: this.templateDir };
    }
    const protocols = {};
    for (const p of [
      'http', 'dns', 'network', 'file', 'headless', 'javascript', 'code', 'ssl',
    ]) {
      protocols[p] = this._collection.byProtocol(p).length;
    }
    return {
      ...this._collection.toDict(),
      protocols,
    };
  }

  // -- private helpers --

  /**
   * Parse a nuclei template YAML file for metadata (fast string splitting).
   * @param {string} filePath
   * @returns {TemplateInfo|null}
   */
  _parseTemplateFile(filePath) {
    try {
      const content = readFileSync(filePath, 'utf8');
      const info = new TemplateInfo({ filePath });

      const lines = content.split('\n').slice(0, 50);
      for (const rawLine of lines) {
        const line = rawLine.trim();
        if (line.startsWith('id:')) {
          info.id = line.split(':', 2)[1].trim();
        } else if (line.startsWith('name:')) {
          info.name = line.split(':', 2)[1].trim();
        } else if (line.startsWith('severity:')) {
          info.severity = line.split(':', 2)[1].trim();
        } else if (line.startsWith('author:')) {
          info.author = line.split(':', 2)[1].trim();
        } else if (line.startsWith('description:')) {
          info.description = line.split(':', 2)[1].trim();
        } else if (line.startsWith('tags:')) {
          info.tags = line
            .split(':', 2)[1]
            .split(',')
            .map((t) => t.trim())
            .filter(Boolean);
        }
      }

      // Detect protocol and category from path relative to template dir
      try {
        const rel = relative(this.templateDir, filePath);
        const parts = rel.split(/[/\\]/);
        if (parts.length > 0) {
          info.category = parts[0];
          const knownProtocols = [
            'http', 'dns', 'network', 'file', 'headless',
            'javascript', 'code', 'ssl', 'cloud',
          ];
          info.protocol = knownProtocols.includes(parts[0]) ? parts[0] : 'http';
        }
      } catch {
        // relative path may fail if paths are on different roots — ignore
      }

      if (!info.id) return null;
      return info;
    } catch {
      return null;
    }
  }

  /**
   * Detect template version from checksum or date.
   * @returns {string}
   */
  _detectVersion() {
    const checksumFile = join(this.templateDir, 'cves.json-checksum.txt');
    try {
      if (existsSync(checksumFile)) {
        return readFileSync(checksumFile, 'utf8').trim().slice(0, 12);
      }
    } catch {
      // ignore
    }
    return new Date().toISOString().slice(0, 10).replace(/-/g, '');
  }
}

export {
  // Constants
  DEFAULT_TEMPLATE_DIR,
  // Data classes
  TemplateInfo,
  TemplateCollection,
  // Manager
  NucleiTemplateManager,
};
