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

/**
 * NERF Multi-Layer Code Review Engine
 *
 * Orchestrates parallel review layers (Blind Hunter, Edge Case Hunter,
 * Acceptance Auditor) and triages/deduplicates findings into a unified
 * report. Each layer runs independently to avoid anchoring bias.
 *
 * @module review/engine
 */

import { readFile, readdir, stat, realpath } from 'node:fs/promises';
import { join, extname, relative, resolve, isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto';

// ---------------------------------------------------------------------------
// Language detection
// ---------------------------------------------------------------------------

const LANG_MAP = Object.freeze({
  '.js': 'javascript',
  '.mjs': 'javascript',
  '.cjs': 'javascript',
  '.jsx': 'javascript',
  '.ts': 'typescript',
  '.tsx': 'typescript',
  '.py': 'python',
  '.sh': 'shell',
  '.bash': 'shell',
  '.zsh': 'shell',
  '.rb': 'ruby',
  '.go': 'go',
  '.rs': 'rust',
  '.java': 'java',
  '.c': 'c',
  '.h': 'c',
  '.cpp': 'cpp',
  '.cc': 'cpp',
  '.hpp': 'cpp',
  '.cs': 'csharp',
  '.php': 'php',
  '.sql': 'sql',
  '.yml': 'yaml',
  '.yaml': 'yaml',
  '.json': 'json',
  '.xml': 'xml',
  '.html': 'html',
  '.htm': 'html',
  '.css': 'css',
  '.md': 'markdown',
  '.dockerfile': 'dockerfile',
  '.tf': 'terraform',
  '.hcl': 'terraform',
});

/**
 * Detect language from file extension.
 * @param {string} filePath
 * @returns {string}
 */
export function detectLanguage(filePath) {
  const ext = extname(filePath).toLowerCase();
  if (LANG_MAP[ext]) return LANG_MAP[ext];
  // Handle Dockerfile (no extension)
  const base = filePath.split(/[/\\]/).pop()?.toLowerCase() ?? '';
  if (base === 'dockerfile' || base.startsWith('dockerfile.')) return 'dockerfile';
  if (base === 'makefile' || base === 'gnumakefile') return 'makefile';
  return 'unknown';
}

// ---------------------------------------------------------------------------
// ReviewFinding
// ---------------------------------------------------------------------------

/** Severity levels ordered by impact. */
export const Severity = Object.freeze({
  CRITICAL: 'critical',
  HIGH: 'high',
  MEDIUM: 'medium',
  LOW: 'low',
  INFO: 'info',
});

const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };

/**
 * A single code review finding from any layer.
 */
export class ReviewFinding {
  /**
   * @param {object} opts
   * @param {string} [opts.id]          - Unique finding ID (auto-generated)
   * @param {string} opts.title         - Short title
   * @param {string} opts.severity      - critical|high|medium|low|info
   * @param {string} opts.layer         - Which review layer produced this
   * @param {string} [opts.file]        - File path
   * @param {number} [opts.line]        - Line number (1-indexed)
   * @param {number} [opts.column]      - Column number (1-indexed)
   * @param {string} [opts.description] - Detailed explanation
   * @param {string} [opts.proof]       - Code snippet or evidence
   * @param {string} [opts.remediation] - How to fix
   * @param {string[]} [opts.cweIds]    - CWE identifiers
   * @param {string[]} [opts.tags]      - MITRE ATT&CK, OWASP, etc.
   * @param {string} [opts.language]    - Source language
   * @param {object} [opts.meta]        - Layer-specific metadata
   */
  constructor(opts = {}) {
    this.id = opts.id ?? `RF-${randomUUID().slice(0, 8)}`;
    this.title = opts.title ?? '';
    this.severity = opts.severity ?? Severity.INFO;
    this.layer = opts.layer ?? '';
    this.file = opts.file ?? '';
    this.line = opts.line ?? 0;
    this.column = opts.column ?? 0;
    this.description = opts.description ?? '';
    this.proof = opts.proof ?? '';
    this.remediation = opts.remediation ?? '';
    this.cweIds = opts.cweIds ?? [];
    this.tags = opts.tags ?? [];
    this.language = opts.language ?? '';
    this.meta = opts.meta ?? {};
  }

  /** Numeric severity rank for sorting (higher = more severe). */
  get rank() {
    return SEVERITY_RANK[this.severity] ?? 0;
  }

  /** Format as NERF finding report. */
  toReport() {
    const lines = [
      `[${this.id}]`,
      `Severity   : ${this.severity.toUpperCase()}`,
    ];
    if (this.cweIds.length) lines.push(`CWE        : ${this.cweIds.join(', ')}`);
    if (this.tags.length) lines.push(`Tags       : ${this.tags.join(', ')}`);
    if (this.file) {
      const loc = this.line ? `${this.file}:${this.line}` : this.file;
      lines.push(`Location   : ${loc}`);
    }
    lines.push(`Layer      : ${this.layer}`);
    if (this.description) lines.push(`Description: ${this.description}`);
    if (this.proof) lines.push(`Proof      : ${this.proof}`);
    if (this.remediation) lines.push(`Remediation: ${this.remediation}`);
    return lines.join('\n');
  }
}

// ---------------------------------------------------------------------------
// Source input types
// ---------------------------------------------------------------------------

/**
 * A single source file prepared for review.
 * @typedef {object} SourceFile
 * @property {string} path     - Relative or absolute file path
 * @property {string} content  - File content
 * @property {string} language - Detected language
 */

// ---------------------------------------------------------------------------
// Input normalization
// ---------------------------------------------------------------------------

const SKIP_DIRS = new Set([
  'node_modules', '.git', 'dist', 'build', 'coverage',
  '__pycache__', '.next', '.nuxt', 'vendor', '.venv', 'venv',
]);

const MAX_FILE_SIZE = 512 * 1024; // 512 KB — skip huge files

/**
 * Resolve input to an array of SourceFile objects.
 *
 * @param {string} input - File path, directory path, or raw code string
 * @param {object} [options]
 * @param {string} [options.language]    - Override language detection
 * @param {string[]} [options.extensions] - Limit to these extensions (e.g. ['.js', '.ts'])
 * @returns {Promise<SourceFile[]>}
 */
export async function resolveInput(input, options = {}) {
  // Determine a safe root for path confinement.
  // Callers may pass options.safeRoot to restrict to a specific directory.
  // When no safeRoot is set, we still block obvious traversal patterns.
  // CWE-22: Path Traversal prevention.
  const safeRoot = options.safeRoot ? resolve(options.safeRoot) : null;

  // Empty or whitespace-only input — treat as raw code string immediately.
  if (!input || !input.trim()) {
    return [{ path: '<inline>', content: input, language: options.language ?? 'unknown' }];
  }

  // Try as file/directory path first
  try {
    const abs = isAbsolute(input) ? input : resolve(process.cwd(), input);

    // Resolve symlinks to get the canonical path
    let canonical;
    try {
      canonical = await realpath(abs);
    } catch {
      canonical = abs; // path may not exist — handled by stat below
    }

    // If an explicit safeRoot is set, enforce confinement.
    // Without a safeRoot, block only explicit '..' traversal in the input string
    // to prevent path traversal from user-controlled relative inputs.
    if (safeRoot) {
      const rel = relative(safeRoot, canonical);
      if (rel.startsWith('..') || isAbsolute(rel)) {
        throw new Error(`Path '${input}' resolves outside the safe root '${safeRoot}'`);
      }
    } else if (!isAbsolute(input) && input.includes('..')) {
      // Block relative traversal (../../etc/passwd) when no safeRoot is set.
      const rel = relative(process.cwd(), canonical);
      if (rel.startsWith('..') || isAbsolute(rel)) {
        throw new Error(`Relative path '${input}' traverses outside the working directory`);
      }
    }

    const st = await stat(canonical);
    if (st.isFile()) {
      const content = await readFile(canonical, 'utf-8');
      const language = options.language ?? detectLanguage(canonical);
      return [{ path: canonical, content, language }];
    }
    if (st.isDirectory()) {
      return collectDir(canonical, options);
    }
  } catch (err) {
    // If it's our own confinement error, re-throw it so the caller knows
    if (err.message?.includes('traverses outside') || err.message?.includes('safe root')) throw err;
    // Otherwise: not a path — treat as raw code string
  }

  // Raw code string
  const language = options.language ?? 'unknown';
  return [{ path: '<inline>', content: input, language }];
}

/**
 * Recursively collect source files from a directory.
 * @param {string} dir
 * @param {object} options
 * @returns {Promise<SourceFile[]>}
 */
async function collectDir(dir, options) {
  const files = [];
  const entries = await readdir(dir, { withFileTypes: true });

  for (const entry of entries) {
    if (SKIP_DIRS.has(entry.name)) continue;
    const full = join(dir, entry.name);

    if (entry.isDirectory()) {
      const sub = await collectDir(full, options);
      files.push(...sub);
    } else if (entry.isFile()) {
      const ext = extname(entry.name).toLowerCase();
      if (options.extensions && !options.extensions.includes(ext)) continue;
      const language = options.language ?? detectLanguage(entry.name);
      if (language === 'unknown') continue; // skip unrecognized files

      try {
        const st = await stat(full);
        if (st.size > MAX_FILE_SIZE) continue;
        const content = await readFile(full, 'utf-8');
        files.push({ path: full, content, language });
      } catch {
        // Skip unreadable files
      }
    }
  }
  return files;
}

// ---------------------------------------------------------------------------
// CodeReviewEngine
// ---------------------------------------------------------------------------

/**
 * Multi-layer code review engine.
 *
 * Runs registered review layers in parallel, collects findings,
 * deduplicates, and produces a unified report.
 */
export class CodeReviewEngine {
  constructor() {
    /** @type {Array<{name: string, review: function}>} */
    this._layers = [];
  }

  /**
   * Register a review layer.
   * @param {string} name      - Layer identifier (e.g. 'blind-hunter')
   * @param {function} reviewFn - async (sources: SourceFile[], options) => ReviewFinding[]
   */
  addLayer(name, reviewFn) {
    this._layers.push({ name, review: reviewFn });
  }

  /**
   * Run all layers against the input and return unified results.
   *
   * @param {string} input       - File path, directory, or raw code
   * @param {object} [options]
   * @param {string} [options.language]     - Override language
   * @param {string[]} [options.extensions] - Limit file extensions
   * @param {string} [options.minSeverity]  - Filter findings at or above this level
   * @returns {Promise<ReviewResult>}
   */
  async review(input, options = {}) {
    const t0 = Date.now();

    // 1. Resolve input to source files
    const sources = await resolveInput(input, options);
    if (!sources.length) {
      return new ReviewResult({
        findings: [],
        filesReviewed: 0,
        layerTimings: {},
        totalTime: Date.now() - t0,
      });
    }

    // 2. Run all layers in parallel
    const layerTimings = {};
    const layerResults = await Promise.allSettled(
      this._layers.map(async (layer) => {
        const lt0 = Date.now();
        try {
          const findings = await layer.review(sources, options);
          layerTimings[layer.name] = Date.now() - lt0;
          return { name: layer.name, findings };
        } catch (err) {
          layerTimings[layer.name] = Date.now() - lt0;
          // Layer failure is non-fatal — report as info finding
          return {
            name: layer.name,
            findings: [
              new ReviewFinding({
                title: `Review layer "${layer.name}" failed`,
                severity: Severity.INFO,
                layer: layer.name,
                description: err.message,
                tags: ['engine-error'],
              }),
            ],
          };
        }
      }),
    );

    // 3. Collect all findings
    const allFindings = [];
    for (const result of layerResults) {
      if (result.status === 'fulfilled') {
        allFindings.push(...result.value.findings);
      }
      // 'rejected' shouldn't happen since we catch above, but guard anyway
    }

    // 4. Deduplicate
    const deduped = this._deduplicate(allFindings);

    // 5. Filter by severity if requested
    const minRank = options.minSeverity
      ? (SEVERITY_RANK[options.minSeverity] ?? 0)
      : 0;
    const filtered = deduped.filter((f) => f.rank >= minRank);

    // 6. Sort by severity (highest first), then by file+line
    filtered.sort((a, b) => {
      if (b.rank !== a.rank) return b.rank - a.rank;
      if (a.file !== b.file) return a.file.localeCompare(b.file);
      return a.line - b.line;
    });

    return new ReviewResult({
      findings: filtered,
      filesReviewed: sources.length,
      layerTimings,
      totalTime: Date.now() - t0,
    });
  }

  /**
   * Deduplicate findings that overlap in location and pattern.
   * When two findings cover the same file:line and similar CWE/title,
   * keep the one with higher severity and merge tags.
   *
   * @param {ReviewFinding[]} findings
   * @returns {ReviewFinding[]}
   */
  _deduplicate(findings) {
    /** @type {Map<string, ReviewFinding>} */
    const seen = new Map();

    for (const f of findings) {
      // Key: file + line + normalized title stem
      const titleStem = f.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 30);
      const key = `${f.file}:${f.line}:${titleStem}`;

      const existing = seen.get(key);
      if (!existing) {
        seen.set(key, f);
        continue;
      }

      // Keep higher severity, merge metadata
      if (f.rank > existing.rank) {
        // Merge tags and CWEs from existing into the new winner
        f.tags = [...new Set([...f.tags, ...existing.tags])];
        f.cweIds = [...new Set([...f.cweIds, ...existing.cweIds])];
        f.layer = `${f.layer}+${existing.layer}`;
        seen.set(key, f);
      } else {
        existing.tags = [...new Set([...existing.tags, ...f.tags])];
        existing.cweIds = [...new Set([...existing.cweIds, ...f.cweIds])];
        if (!existing.layer.includes(f.layer)) {
          existing.layer = `${existing.layer}+${f.layer}`;
        }
      }
    }

    return [...seen.values()];
  }
}

// ---------------------------------------------------------------------------
// ReviewResult
// ---------------------------------------------------------------------------

/**
 * Unified review result with findings and metadata.
 */
export class ReviewResult {
  /**
   * @param {object} opts
   * @param {ReviewFinding[]} opts.findings
   * @param {number} opts.filesReviewed
   * @param {object} opts.layerTimings
   * @param {number} opts.totalTime
   */
  constructor({ findings = [], filesReviewed = 0, layerTimings = {}, totalTime = 0 } = {}) {
    this.findings = findings;
    this.filesReviewed = filesReviewed;
    this.layerTimings = layerTimings;
    this.totalTime = totalTime;
  }

  /** Count of findings by severity. */
  get severityCounts() {
    const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
    for (const f of this.findings) {
      counts[f.severity] = (counts[f.severity] ?? 0) + 1;
    }
    return counts;
  }

  /** Human-readable summary line. */
  get summary() {
    const c = this.severityCounts;
    const parts = [];
    if (c.critical) parts.push(`${c.critical} critical`);
    if (c.high) parts.push(`${c.high} high`);
    if (c.medium) parts.push(`${c.medium} medium`);
    if (c.low) parts.push(`${c.low} low`);
    if (c.info) parts.push(`${c.info} info`);
    const total = this.findings.length;
    const detail = parts.length ? ` (${parts.join(', ')})` : '';
    return `${total} finding${total !== 1 ? 's' : ''}${detail} across ${this.filesReviewed} file${this.filesReviewed !== 1 ? 's' : ''} in ${this.totalTime}ms`;
  }

  /** Full report as formatted text. */
  toReport() {
    const lines = [
      '═══════════════════════════════════════════════════════',
      '  NERF Code Review Report',
      '═══════════════════════════════════════════════════════',
      '',
      `Summary: ${this.summary}`,
      '',
    ];

    if (this.findings.length === 0) {
      lines.push('No findings.');
    } else {
      for (const f of this.findings) {
        lines.push(f.toReport());
        lines.push('');
      }
    }

    // Layer timing
    lines.push('───────────────────────────────────────────────────────');
    lines.push('Layer Timings:');
    for (const [name, ms] of Object.entries(this.layerTimings)) {
      lines.push(`  ${name}: ${ms}ms`);
    }

    return lines.join('\n');
  }

  /** Structured JSON output. */
  toJSON() {
    return {
      summary: this.summary,
      severityCounts: this.severityCounts,
      filesReviewed: this.filesReviewed,
      totalTime: this.totalTime,
      layerTimings: this.layerTimings,
      findings: this.findings.map((f) => ({
        id: f.id,
        title: f.title,
        severity: f.severity,
        layer: f.layer,
        file: f.file,
        line: f.line,
        column: f.column,
        description: f.description,
        proof: f.proof,
        remediation: f.remediation,
        cweIds: f.cweIds,
        tags: f.tags,
        language: f.language,
      })),
    };
  }
}

// ---------------------------------------------------------------------------
// Factory — create engine with all standard layers
// ---------------------------------------------------------------------------

/**
 * Create a CodeReviewEngine with all standard review layers loaded.
 * Layers are imported lazily to keep the module lightweight.
 *
 * @returns {Promise<CodeReviewEngine>}
 */
export async function createReviewEngine() {
  const engine = new CodeReviewEngine();

  // Layer 1: Blind Hunter — pattern-based vulnerability detection
  const { blindHunterReview } = await import('./layers/blind-hunter.js');
  engine.addLayer('blind-hunter', blindHunterReview);

  // Layer 2: Edge Case Hunter — boundary condition analysis
  const { edgeCaseReview } = await import('./layers/edge-case-hunter.js');
  engine.addLayer('edge-case-hunter', edgeCaseReview);

  // Layer 3: Acceptance Auditor — security architecture review
  const { acceptanceAuditReview } = await import('./layers/acceptance-auditor.js');
  engine.addLayer('acceptance-auditor', acceptanceAuditReview);

  // Layer 4: Defense-in-Depth — single-layer validation gaps
  const { defenseInDepthReview } = await import('./layers/defense-in-depth.js');
  engine.addLayer('defense-in-depth', defenseInDepthReview);

  return engine;
}
