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

/**
 * Forensic report validator for INCIDENT mode agent output.
 *
 * Ported from execution-engine/validators/forensic.py.
 * Validates LLM-generated JSON forensic reports for structural correctness:
 * required top-level sections, CVE-ID format, non-empty findings and services.
 *
 * @module execution-engine/validators/forensic
 */

import { ValidationResult } from '../framework.js';

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

const REQUIRED_SECTIONS = [
  'summary', 'services', 'connections', 'findings', 'recommendations',
];

const RECOMMENDED_FINDING_FIELDS = ['severity', 'description', 'evidence'];

const RECOMMENDED_TOP_LEVEL_FIELDS = ['pcap_file', 'analysis_timestamp'];

// Matches well-formed CVE IDs: CVE-YYYY-NNNNN (4-7 digit suffix)
const CVE_VALID_RE = /CVE-\d{4}-\d{4,7}/;

// Matches anything that looks like a CVE reference (broader, to catch malformed)
const CVE_LIKE_RE = /CVE-[A-Za-z0-9]+-[A-Za-z0-9]+/g;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Extract JSON content from markdown code fences, or return text as-is.
 * Uses findall+join pattern.
 *
 * @param {string} text
 * @returns {string}
 */
function stripCodeFences(text) {
  const matches = [...text.matchAll(/```(?:json)?\s*\n(.*?)```/gs)].map(m => m[1]);
  if (matches.length > 0) {
    return matches.join('\n');
  }
  return text;
}

/**
 * Parse a JSON forensic report from text.
 *
 * @param {string} text
 * @returns {Object}
 * @throws {Error}
 */
function parseJsonReport(text) {
  const stripped = stripCodeFences(text);
  try {
    return JSON.parse(stripped);
  } catch (e) {
    throw new Error(`JSON parse error: ${e.message}`);
  }
}

/**
 * Recursively collect all string values from a nested structure.
 *
 * @param {*} obj
 * @returns {string[]}
 */
function collectStringValues(obj) {
  const strings = [];
  if (typeof obj === 'string') {
    strings.push(obj);
  } else if (Array.isArray(obj)) {
    for (const item of obj) {
      strings.push(...collectStringValues(item));
    }
  } else if (obj && typeof obj === 'object') {
    for (const v of Object.values(obj)) {
      strings.push(...collectStringValues(v));
    }
  }
  return strings;
}

/**
 * Validate a forensic report dict for structural correctness.
 *
 * @param {Object} report
 * @returns {{ errors: string[], warnings: string[] }}
 */
function validateReport(report) {
  const errors = [];
  const warnings = [];

  // Required top-level sections
  for (const section of REQUIRED_SECTIONS) {
    if (!(section in report)) {
      errors.push(`Missing required section: ${section}`);
    }
  }

  // findings must be a non-empty list
  const findings = report.findings;
  if (findings !== undefined) {
    if (!Array.isArray(findings)) {
      errors.push(`findings must be a list (got ${typeof findings})`);
    } else if (findings.length === 0) {
      errors.push('findings list must not be empty');
    } else {
      for (let i = 0; i < findings.length; i++) {
        if (findings[i] && typeof findings[i] === 'object' && !Array.isArray(findings[i])) {
          for (const fieldName of RECOMMENDED_FINDING_FIELDS) {
            if (!(fieldName in findings[i])) {
              warnings.push(`Finding ${i + 1}: missing recommended field '${fieldName}'`);
            }
          }
        }
      }
    }
  }

  // services must be a non-empty list
  const services = report.services;
  if (services !== undefined) {
    if (!Array.isArray(services)) {
      errors.push(`services must be a list (got ${typeof services})`);
    } else if (services.length === 0) {
      errors.push('services list must not be empty');
    }
  }

  // CVE format validation in findings
  if (Array.isArray(findings)) {
    const allStrings = collectStringValues(findings);
    for (const s of allStrings) {
      // Reset global regex
      const cveLikeRe = /CVE-[A-Za-z0-9]+-[A-Za-z0-9]+/g;
      let match;
      while ((match = cveLikeRe.exec(s)) !== null) {
        const cveRef = match[0];
        if (!CVE_VALID_RE.test(cveRef)) {
          errors.push(
            `Malformed CVE ID: ${cveRef} (must match CVE-YYYY-NNNN[N{0,3}])`
          );
        }
      }
    }
  }

  // Recommended top-level fields
  for (const fieldName of RECOMMENDED_TOP_LEVEL_FIELDS) {
    if (!(fieldName in report)) {
      warnings.push(`Missing recommended top-level field: ${fieldName}`);
    }
  }

  return { errors, warnings };
}

// ---------------------------------------------------------------------------
// ForensicValidator
// ---------------------------------------------------------------------------

/**
 * Deterministic validator for JSON forensic reports.
 *
 * Scoring:
 *   - 1.0: no errors and no warnings (clean)
 *   - 0.5: warnings only (valid but imperfect)
 *   - 0.0: any errors (invalid)
 */
export class ForensicValidator {
  /**
   * @param {import('../framework.js').ModeAgentResult} result
   * @returns {import('../framework.js').ValidationResult}
   */
  validate(result) {
    const text = result.outputText || '';
    const emptyMeta = { findings_count: 0, services_count: 0, cve_count: 0 };

    // Empty output
    if (!text.trim()) {
      return new ValidationResult({
        valid: false,
        errors: ['No forensic report found in output (empty)'],
        warnings: [],
        score: 0.0,
        metadata: { ...emptyMeta },
      });
    }

    // Parse JSON
    let parsed;
    try {
      parsed = parseJsonReport(text);
    } catch (e) {
      return new ValidationResult({
        valid: false,
        errors: [e.message],
        warnings: [],
        score: 0.0,
        metadata: { ...emptyMeta },
      });
    }

    // Must be an object
    if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
      return new ValidationResult({
        valid: false,
        errors: [`Forensic report must be a JSON object (got ${Array.isArray(parsed) ? 'Array' : typeof parsed})`],
        warnings: [],
        score: 0.0,
        metadata: { ...emptyMeta },
      });
    }

    // Structural validation
    const { errors, warnings } = validateReport(parsed);

    // Compute metadata
    const findings = parsed.findings || [];
    const services = parsed.services || [];
    const findingsCount = Array.isArray(findings) ? findings.length : 0;
    const servicesCount = Array.isArray(services) ? services.length : 0;

    // Count valid CVE references
    let cveCount = 0;
    if (Array.isArray(findings)) {
      const allStrings = collectStringValues(findings);
      for (const s of allStrings) {
        const cveValidRe = /CVE-\d{4}-\d{4,7}/g;
        const matches = s.match(cveValidRe);
        if (matches) cveCount += matches.length;
      }
    }

    // Scoring
    let score;
    if (errors.length > 0) {
      score = 0.0;
    } else if (warnings.length > 0) {
      score = 0.5;
    } else {
      score = 1.0;
    }

    return new ValidationResult({
      valid: errors.length === 0,
      errors,
      warnings,
      score,
      metadata: {
        findings_count: findingsCount,
        services_count: servicesCount,
        cve_count: cveCount,
      },
    });
  }
}
