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

/**
 * STRIDE/DREAD threat model validator for ARCHITECT mode agent output.
 *
 * Ported from execution-engine/validators/threat_model.py.
 * Validates LLM-generated JSON threat models for structural correctness:
 * required sections, STRIDE threat type validation, DREAD dimension score
 * ranges (1-10 integers across 5 dimensions), Mermaid DFD syntax, and
 * recommended fields.
 *
 * @module execution-engine/validators/threat-model
 */

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

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

const REQUIRED_SECTIONS = [
  'system_description', 'components', 'trust_boundaries',
  'stride_analysis', 'dread_scores', 'mitigations', 'dfd_mermaid',
];

const RECOMMENDED_FIELDS = [
  'risk_matrix', 'attack_trees', 'residual_risks',
];

// Accept both single-letter abbreviations and full STRIDE names
const VALID_STRIDE_ABBREVIATIONS = new Set(['S', 'T', 'R', 'I', 'D', 'E']);

const VALID_STRIDE_FULL_NAMES = new Set([
  'Spoofing', 'Tampering', 'Repudiation',
  'Information Disclosure', 'Denial of Service', 'Elevation of Privilege',
]);

const STRIDE_FULL_NAMES_LOWER = new Set(
  [...VALID_STRIDE_FULL_NAMES].map(n => n.toLowerCase())
);

const DREAD_DIMENSIONS = [
  'Damage', 'Reproducibility', 'Exploitability', 'Affected Users', 'Discoverability',
];

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

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

  matches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
  if (matches.length > 0) return matches.join('\n');

  return text;
}

/**
 * Parse a JSON threat model 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}`);
  }
}

/**
 * Check if a threat type is a valid STRIDE category.
 *
 * @param {string} threatType
 * @returns {boolean}
 */
function isValidStrideType(threatType) {
  if (VALID_STRIDE_ABBREVIATIONS.has(threatType)) return true;
  if (STRIDE_FULL_NAMES_LOWER.has(threatType.toLowerCase())) return true;
  return false;
}

/**
 * Validate a threat model 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}`);
    }
  }

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

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

  // stride_analysis must be a non-empty list with required keys
  const strideAnalysis = report.stride_analysis;
  if (strideAnalysis !== undefined) {
    if (!Array.isArray(strideAnalysis)) {
      errors.push(`stride_analysis must be a list (got ${typeof strideAnalysis})`);
    } else if (strideAnalysis.length === 0) {
      errors.push('stride_analysis list must not be empty');
    } else {
      const requiredKeys = ['component', 'threat_type', 'description', 'dread_score'];
      for (let i = 0; i < strideAnalysis.length; i++) {
        const item = strideAnalysis[i];
        if (typeof item !== 'object' || item === null || Array.isArray(item)) {
          errors.push(`stride_analysis[${i}] must be a dict (got ${Array.isArray(item) ? 'Array' : typeof item})`);
          continue;
        }
        for (const key of requiredKeys) {
          if (!(key in item)) {
            errors.push(`stride_analysis[${i}] missing required key: ${key}`);
          }
        }
        // Validate threat_type
        const threatType = item.threat_type || '';
        if (threatType && !isValidStrideType(String(threatType))) {
          errors.push(
            `stride_analysis[${i}] invalid threat_type: ` +
            `'${threatType}' (must be one of ${JSON.stringify([...VALID_STRIDE_ABBREVIATIONS].sort())} or a full STRIDE name)`
          );
        }
      }
    }
  }

  // dread_scores must be a non-empty list with all 5 dimensions
  const dreadScores = report.dread_scores;
  if (dreadScores !== undefined) {
    if (!Array.isArray(dreadScores)) {
      errors.push(`dread_scores must be a list (got ${typeof dreadScores})`);
    } else if (dreadScores.length === 0) {
      errors.push('dread_scores list must not be empty');
    } else {
      for (let i = 0; i < dreadScores.length; i++) {
        const item = dreadScores[i];
        if (typeof item !== 'object' || item === null || Array.isArray(item)) {
          errors.push(`dread_scores[${i}] must be a dict (got ${Array.isArray(item) ? 'Array' : typeof item})`);
          continue;
        }
        for (const dim of DREAD_DIMENSIONS) {
          if (!(dim in item)) {
            errors.push(`dread_scores[${i}] missing DREAD dimension: ${dim}`);
          } else {
            const val = item[dim];
            if (!Number.isInteger(val)) {
              errors.push(
                `dread_scores[${i}].${dim} must be an integer (got ${typeof val})`
              );
            } else if (val < 1 || val > 10) {
              errors.push(`dread_scores[${i}].${dim} must be 1-10 (got ${val})`);
            }
          }
        }
      }
    }
  }

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

  // dfd_mermaid must be a non-empty string containing graph or flowchart
  const dfdMermaid = report.dfd_mermaid;
  if (dfdMermaid !== undefined) {
    if (typeof dfdMermaid !== 'string') {
      errors.push(`dfd_mermaid must be a string (got ${typeof dfdMermaid})`);
    } else if (!dfdMermaid.trim()) {
      errors.push('dfd_mermaid must not be empty');
    } else {
      const lower = dfdMermaid.toLowerCase();
      if (!lower.includes('graph') && !lower.includes('flowchart')) {
        errors.push(
          "dfd_mermaid must contain 'graph' or 'flowchart' keyword (invalid Mermaid DFD syntax)"
        );
      }
    }
  }

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

  return { errors, warnings };
}

/**
 * Compute the highest risk level from DREAD score averages.
 *
 * @param {Array<Object>} dreadScores
 * @returns {string}
 */
function computeHighestRiskLevel(dreadScores) {
  if (!Array.isArray(dreadScores) || dreadScores.length === 0) return 'none';

  let maxAvg = 0;
  for (const item of dreadScores) {
    if (typeof item !== 'object' || item === null || Array.isArray(item)) continue;
    const dimValues = [];
    for (const dim of DREAD_DIMENSIONS) {
      const val = item[dim];
      if (Number.isInteger(val)) dimValues.push(val);
    }
    if (dimValues.length > 0) {
      const avg = dimValues.reduce((a, b) => a + b, 0) / dimValues.length;
      if (avg > maxAvg) maxAvg = avg;
    }
  }

  if (maxAvg <= 0) return 'none';
  if (maxAvg >= 8) return 'Critical';
  if (maxAvg >= 6) return 'High';
  if (maxAvg >= 4) return 'Medium';
  return 'Low';
}

// ---------------------------------------------------------------------------
// ThreatModelValidator
// ---------------------------------------------------------------------------

/**
 * Deterministic validator for JSON STRIDE/DREAD threat model reports.
 *
 * Scoring:
 *   - 1.0: no errors and no warnings (clean)
 *   - 0.5: warnings only (valid but imperfect)
 *   - 0.0: any errors (invalid)
 */
export class ThreatModelValidator {
  /**
   * @param {import('../framework.js').ModeAgentResult} result
   * @returns {import('../framework.js').ValidationResult}
   */
  validate(result) {
    const text = result.outputText || '';
    const emptyMeta = {
      component_count: 0,
      threat_count: 0,
      mitigation_count: 0,
      highest_risk_level: 'none',
    };

    // Empty output
    if (!text.trim()) {
      return new ValidationResult({
        valid: false,
        errors: ['No threat model 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: [`Threat model 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 components = parsed.components || [];
    const strideAnalysis = parsed.stride_analysis || [];
    const mitigations = parsed.mitigations || [];
    const dreadScores = parsed.dread_scores || [];

    const componentCount = Array.isArray(components) ? components.length : 0;
    const threatCount = Array.isArray(strideAnalysis) ? strideAnalysis.length : 0;
    const mitigationCount = Array.isArray(mitigations) ? mitigations.length : 0;
    const highestRiskLevel = computeHighestRiskLevel(dreadScores);

    // 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: {
        component_count: componentCount,
        threat_count: threatCount,
        mitigation_count: mitigationCount,
        highest_risk_level: highestRiskLevel,
      },
    });
  }
}
