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

/**
 * DPIA (Data Protection Impact Assessment) validator for PRIVACY mode agent output.
 *
 * Ported from execution-engine/validators/privacy.py.
 * Validates LLM-generated JSON DPIA reports for structural correctness:
 * required sections, GDPR article citation format, risk assessment structure,
 * and recommended fields.
 *
 * @module execution-engine/validators/privacy
 */

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

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

const REQUIRED_SECTIONS = [
  'processing_description', 'data_flows', 'legal_basis',
  'risk_assessment', 'mitigations', 'gdpr_articles', 'findings',
];

const RECOMMENDED_FIELDS = [
  'ccpa_mappings', 'hipaa_mappings', 'data_subjects', 'retention_periods',
];

// ---------------------------------------------------------------------------
// 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 DPIA 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}`);
  }
}

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

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

  // legal_basis must be a dict with article and basis keys
  const legalBasis = report.legal_basis;
  if (legalBasis !== undefined) {
    if (typeof legalBasis !== 'object' || legalBasis === null || Array.isArray(legalBasis)) {
      errors.push(`legal_basis must be a dict (got ${Array.isArray(legalBasis) ? 'Array' : typeof legalBasis})`);
    } else {
      if (!('article' in legalBasis)) {
        errors.push('legal_basis missing required key: article');
      }
      if (!('basis' in legalBasis)) {
        errors.push('legal_basis missing required key: basis');
      }
    }
  }

  // risk_assessment must be a non-empty list with required sub-keys
  const riskAssessment = report.risk_assessment;
  if (riskAssessment !== undefined) {
    if (!Array.isArray(riskAssessment)) {
      errors.push(`risk_assessment must be a list (got ${typeof riskAssessment})`);
    } else if (riskAssessment.length === 0) {
      errors.push('risk_assessment list must not be empty');
    } else {
      const requiredRiskKeys = ['threat', 'likelihood', 'severity', 'risk_level'];
      for (let i = 0; i < riskAssessment.length; i++) {
        const item = riskAssessment[i];
        if (typeof item !== 'object' || item === null || Array.isArray(item)) {
          errors.push(`risk_assessment[${i}] must be a dict (got ${Array.isArray(item) ? 'Array' : typeof item})`);
          continue;
        }
        for (const key of requiredRiskKeys) {
          if (!(key in item)) {
            errors.push(`risk_assessment[${i}] missing required key: ${key}`);
          }
        }
      }
    }
  }

  // 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');
    }
  }

  // gdpr_articles must be a non-empty list, each matching Art. \d+
  const gdprArticles = report.gdpr_articles;
  if (gdprArticles !== undefined) {
    if (!Array.isArray(gdprArticles)) {
      errors.push(`gdpr_articles must be a list (got ${typeof gdprArticles})`);
    } else if (gdprArticles.length === 0) {
      errors.push('gdpr_articles list must not be empty');
    } else {
      for (const item of gdprArticles) {
        if (typeof item !== 'string' || !/^Art\.\s*\d+/.test(item)) {
          errors.push(
            `Invalid GDPR article format: '${item}' (must match 'Art. <number>')`
          );
        }
      }
    }
  }

  // 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');
    }
  }

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

  return { errors, warnings };
}

// ---------------------------------------------------------------------------
// DPIAValidator
// ---------------------------------------------------------------------------

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

    // Empty output
    if (!text.trim()) {
      return new ValidationResult({
        valid: false,
        errors: ['No DPIA 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: [`DPIA 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 riskAssessment = parsed.risk_assessment || [];
    const gdprArticles = parsed.gdpr_articles || [];

    const findingsCount = Array.isArray(findings) ? findings.length : 0;
    const riskCount = Array.isArray(riskAssessment) ? riskAssessment.length : 0;
    const gdprArticleCount = Array.isArray(gdprArticles) ? gdprArticles.length : 0;

    // Highest risk level
    let highestRiskLevel = 'none';
    if (Array.isArray(riskAssessment) && riskAssessment.length > 0) {
      const severityOrder = {
        critical: 4, high: 3, medium: 2, low: 1, info: 0,
      };
      const scored = riskAssessment
        .filter(item => item && typeof item === 'object' && item.risk_level)
        .map(item => ({
          score: severityOrder[item.risk_level.toLowerCase()] ?? -1,
          level: item.risk_level,
        }))
        .filter(s => s.score >= 0);
      if (scored.length > 0) {
        highestRiskLevel = scored.reduce((a, b) => a.score > b.score ? a : b).level;
      }
    }

    // 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,
        risk_count: riskCount,
        gdpr_article_count: gdprArticleCount,
        highest_risk_level: highestRiskLevel,
      },
    });
  }
}
