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

/**
 * NERF Compliance Engine — automated compliance report generation.
 *
 * Maps security findings and scan results to regulatory frameworks,
 * generates gap analyses, compares reports, and exports CSV/JSON/Markdown.
 */

import { randomUUID } from 'node:crypto';
import { CONTROL_MAPS, CATEGORY_KEYWORDS, SEVERITY_WEIGHTS } from './controls.js';

// ---------------------------------------------------------------------------
// Enums (frozen objects with string constants)
// ---------------------------------------------------------------------------

/** Supported compliance frameworks — 39 values. */
export const ComplianceFramework = Object.freeze({
  SOC2: 'SOC2',
  PCI_DSS: 'PCI_DSS',
  HIPAA: 'HIPAA',
  NIST_800_53: 'NIST_800_53',
  CIS: 'CIS',
  ISO_27001: 'ISO_27001',
  GDPR: 'GDPR',
  CMMC: 'CMMC',
  NIST_CSF: 'NIST_CSF',
  NIST_AI_RMF: 'NIST_AI_RMF',
  FEDRAMP: 'FEDRAMP',
  OWASP_TOP_10: 'OWASP_TOP_10',
  MITRE_ATTACK: 'MITRE_ATTACK',
  IEC_62443: 'IEC_62443',
  NIS2: 'NIS2',
  DORA: 'DORA',
  CISA_KEV: 'CISA_KEV',
  EU_AI_ACT: 'EU_AI_ACT',
  SWIFT_CSP: 'SWIFT_CSP',
  NIST_800_171: 'NIST_800_171',
  ISO_27017: 'ISO_27017',
  ISO_27018: 'ISO_27018',
  ISO_27701: 'ISO_27701',
  CSA_CCM: 'CSA_CCM',
  NERC_CIP: 'NERC_CIP',
  HITRUST_CSF: 'HITRUST_CSF',
  CIS_CLOUD: 'CIS_CLOUD',
  NIST_800_82: 'NIST_800_82',
  OWASP_MASVS: 'OWASP_MASVS',
  SANS_TOP_25: 'SANS_TOP_25',
  PTES: 'PTES',
  OSSTMM: 'OSSTMM',
  SOX_IT: 'SOX_IT',
  COBIT: 'COBIT',
  ITIL_SECURITY: 'ITIL_SECURITY',
  SSDF: 'SSDF',
  OWASP_ASVS: 'OWASP_ASVS',
  NIST_PRIVACY: 'NIST_PRIVACY',
  CCPA: 'CCPA',
});

/** Assessment outcome for a single control — 5 values. */
export const ControlStatus = Object.freeze({
  PASS: 'PASS',
  FAIL: 'FAIL',
  PARTIAL: 'PARTIAL',
  NOT_APPLICABLE: 'NOT_APPLICABLE',
  NOT_TESTED: 'NOT_TESTED',
});

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

/** Result of evaluating one control against collected evidence. */
export class ControlAssessment {
  /**
   * @param {object} opts
   * @param {string} opts.controlId
   * @param {string} opts.controlName
   * @param {string} opts.framework
   * @param {string} opts.status
   * @param {string[]} [opts.evidence]
   * @param {string[]} [opts.gaps]
   * @param {string[]} [opts.recommendations]
   * @param {string} [opts.severity]
   * @param {string} [opts.testedAt]
   */
  constructor({
    controlId,
    controlName,
    framework,
    status,
    evidence = [],
    gaps = [],
    recommendations = [],
    severity = 'info',
    testedAt = '',
  }) {
    this.controlId = controlId;
    this.controlName = controlName;
    this.framework = framework;
    this.status = status;
    this.evidence = evidence;
    this.gaps = gaps;
    this.recommendations = recommendations;
    this.severity = severity;
    this.testedAt = testedAt || new Date().toISOString();
  }
}

/** Full compliance report for a single framework assessment. */
export class ComplianceReport {
  /**
   * @param {object} opts
   * @param {string} opts.reportId
   * @param {string} opts.framework
   * @param {string} opts.scope
   * @param {string} [opts.assessor]
   * @param {number} [opts.controlsTotal]
   * @param {number} [opts.controlsPass]
   * @param {number} [opts.controlsFail]
   * @param {number} [opts.controlsPartial]
   * @param {number} [opts.controlsNa]
   * @param {number} [opts.overallScore]
   * @param {ControlAssessment[]} [opts.assessments]
   * @param {string} [opts.executiveSummary]
   * @param {string} [opts.createdAt]
   */
  constructor({
    reportId,
    framework,
    scope,
    assessor = 'NERF Automated Assessment',
    controlsTotal = 0,
    controlsPass = 0,
    controlsFail = 0,
    controlsPartial = 0,
    controlsNa = 0,
    overallScore = 0,
    assessments = [],
    executiveSummary = '',
    createdAt = '',
  }) {
    this.reportId = reportId;
    this.framework = framework;
    this.scope = scope;
    this.assessor = assessor;
    this.controlsTotal = controlsTotal;
    this.controlsPass = controlsPass;
    this.controlsFail = controlsFail;
    this.controlsPartial = controlsPartial;
    this.controlsNa = controlsNa;
    this.overallScore = overallScore;
    this.assessments = assessments;
    this.executiveSummary = executiveSummary;
    this.createdAt = createdAt || new Date().toISOString();
  }

  /** Serialise the report to a plain object. */
  toDict() {
    return {
      report_id: this.reportId,
      framework: this.framework,
      scope: this.scope,
      assessor: this.assessor,
      controls_total: this.controlsTotal,
      controls_pass: this.controlsPass,
      controls_fail: this.controlsFail,
      controls_partial: this.controlsPartial,
      controls_na: this.controlsNa,
      overall_score: Math.round(this.overallScore * 100) / 100,
      executive_summary: this.executiveSummary,
      created_at: this.createdAt,
      assessments: this.assessments.map((a) => ({
        control_id: a.controlId,
        control_name: a.controlName,
        framework: a.framework,
        status: a.status,
        evidence: a.evidence,
        gaps: a.gaps,
        recommendations: a.recommendations,
        severity: a.severity,
        tested_at: a.testedAt,
      })),
    };
  }

  /** Serialise the report to a JSON string. */
  toJson() {
    return JSON.stringify(this.toDict(), null, 2);
  }

  /** Render the report as a Markdown document. */
  toMarkdown() {
    const statusIcon = {
      [ControlStatus.PASS]: '✅',
      [ControlStatus.FAIL]: '❌',
      [ControlStatus.PARTIAL]: '⚠️',
      [ControlStatus.NOT_APPLICABLE]: '➖',
      [ControlStatus.NOT_TESTED]: '❓',
    };

    const lines = [
      `# Compliance Report — ${this.framework}`,
      '',
      `**Report ID:** ${this.reportId}  `,
      `**Scope:** ${this.scope}  `,
      `**Assessor:** ${this.assessor}  `,
      `**Created:** ${this.createdAt}  `,
      '',
      '## Executive Summary',
      '',
      this.executiveSummary || '_No summary generated._',
      '',
      '## Score Overview',
      '',
      '| Metric | Value |',
      '|--------|-------|',
      `| Overall Score | ${this.overallScore.toFixed(1)}% |`,
      `| Total Controls | ${this.controlsTotal} |`,
      `| Pass | ${this.controlsPass} |`,
      `| Fail | ${this.controlsFail} |`,
      `| Partial | ${this.controlsPartial} |`,
      `| N/A | ${this.controlsNa} |`,
      '',
      '## Control Assessments',
      '',
    ];

    for (const a of this.assessments) {
      const icon = statusIcon[a.status] || '';
      lines.push(`### ${icon} ${a.controlId} — ${a.controlName}`);
      lines.push('');
      lines.push(`**Status:** ${a.status} | **Severity:** ${a.severity}`);
      lines.push('');
      if (a.evidence.length) {
        lines.push('**Evidence:**');
        for (const e of a.evidence) lines.push(`- ${e}`);
        lines.push('');
      }
      if (a.gaps.length) {
        lines.push('**Gaps:**');
        for (const g of a.gaps) lines.push(`- ${g}`);
        lines.push('');
      }
      if (a.recommendations.length) {
        lines.push('**Recommendations:**');
        for (const r of a.recommendations) lines.push(`- ${r}`);
        lines.push('');
      }
    }

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

// ---------------------------------------------------------------------------
// Engine
// ---------------------------------------------------------------------------

/** Map security findings to compliance controls and generate reports. */
export class ComplianceEngine {
  constructor() {
    // Build category → control ID index per framework
    /** @type {Map<string, Map<string, string[]>>} */
    this._categoryToControls = new Map();
    for (const [fw, controls] of Object.entries(CONTROL_MAPS)) {
      const catMap = new Map();
      for (const ctrl of controls) {
        const existing = catMap.get(ctrl.category) || [];
        existing.push(ctrl.id);
        catMap.set(ctrl.category, existing);
      }
      this._categoryToControls.set(fw, catMap);
    }
  }

  // -- public API -----------------------------------------------------------

  /**
   * Map a list of security findings to framework controls.
   *
   * Each finding dict should have at least:
   *   - title (string)
   *   - severity (string): critical/high/medium/low/info
   * Optional keys: description, category, remediation, evidence, location
   *
   * @param {object[]} findings
   * @param {string} framework - ComplianceFramework value
   * @param {string} [scope='']
   * @returns {ComplianceReport}
   */
  assessFromFindings(findings, framework, scope = '') {
    const controls = CONTROL_MAPS[framework];
    if (!controls) {
      throw new Error(`Unknown compliance framework: ${framework}`);
    }
    const now = new Date().toISOString();

    // Build map: controlId → matched findings
    const ctrlFindings = new Map();
    for (const ctrl of controls) {
      ctrlFindings.set(ctrl.id, []);
    }

    for (const finding of findings) {
      if (!finding || typeof finding !== 'object') continue;
      const matchedIds = this._mapFindingToControls(finding, framework);
      for (const cid of matchedIds) {
        const arr = ctrlFindings.get(cid);
        if (arr) arr.push(finding);
      }
    }

    const assessments = [];
    for (const ctrl of controls) {
      const matched = ctrlFindings.get(ctrl.id);
      if (!matched || matched.length === 0) {
        assessments.push(
          new ControlAssessment({
            controlId: ctrl.id,
            controlName: ctrl.name,
            framework,
            status: ControlStatus.NOT_TESTED,
            severity: 'info',
            testedAt: now,
          }),
        );
        continue;
      }

      // Determine status from findings
      const severities = matched.map((f) => (f.severity || 'info').toLowerCase());
      const worst = ComplianceEngine._worstSeverity(severities);
      const hasCritical = severities.includes('critical') || severities.includes('high');
      const hasMedium = severities.includes('medium');

      let status;
      if (hasCritical) {
        status = ControlStatus.FAIL;
      } else if (hasMedium) {
        status = ControlStatus.PARTIAL;
      } else {
        status = ControlStatus.PASS;
      }

      const evidence = matched.map((f) => f.title || 'untitled');
      const gaps = matched
        .filter((f) => ['critical', 'high', 'medium'].includes((f.severity || '').toLowerCase()))
        .map((f) => f.description || f.title || '');
      const recs = matched.filter((f) => f.remediation).map((f) => f.remediation);

      assessments.push(
        new ControlAssessment({
          controlId: ctrl.id,
          controlName: ctrl.name,
          framework,
          status,
          evidence,
          gaps,
          recommendations: recs,
          severity: worst,
          testedAt: now,
        }),
      );
    }

    return this._buildReport(assessments, framework, scope);
  }

  /**
   * Map scan results to compliance controls.
   *
   * @param {object} scanResults - { scope, findings, metadata }
   * @param {string} framework - ComplianceFramework value
   * @returns {ComplianceReport}
   */
  assessFromScan(scanResults, framework) {
    const findings = scanResults?.findings || [];
    const scope = scanResults?.scope || 'scan target';
    return this.assessFromFindings(findings, framework, scope);
  }

  /**
   * Return a prioritised remediation plan from failing controls.
   * @param {ComplianceReport} report
   * @returns {object}
   */
  generateGapAnalysis(report) {
    const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
    const failing = report.assessments.filter(
      (a) => a.status === ControlStatus.FAIL || a.status === ControlStatus.PARTIAL,
    );
    failing.sort((a, b) => {
      const idxA = severityOrder.indexOf(a.severity);
      const idxB = severityOrder.indexOf(b.severity);
      return (idxA === -1 ? 99 : idxA) - (idxB === -1 ? 99 : idxB);
    });

    const remediationItems = failing.map((a, idx) => ({
      priority: idx + 1,
      control_id: a.controlId,
      control_name: a.controlName,
      status: a.status,
      severity: a.severity,
      gaps: a.gaps,
      recommendations: a.recommendations,
    }));

    const notTested = report.assessments.filter((a) => a.status === ControlStatus.NOT_TESTED);

    return {
      framework: report.framework,
      report_id: report.reportId,
      overall_score: Math.round(report.overallScore * 100) / 100,
      total_gaps: failing.length,
      remediation_plan: remediationItems,
      untested_controls: notTested.map((a) => ({
        control_id: a.controlId,
        control_name: a.controlName,
      })),
      summary:
        `${failing.length} controls require remediation; ` +
        `${notTested.length} controls have not been tested. ` +
        `Current score: ${report.overallScore.toFixed(1)}%.`,
    };
  }

  /**
   * Diff two reports of the same framework to show progress over time.
   * @param {ComplianceReport} reportA
   * @param {ComplianceReport} reportB
   * @returns {object}
   */
  compareReports(reportA, reportB) {
    const mapA = new Map(reportA.assessments.map((a) => [a.controlId, a]));
    const mapB = new Map(reportB.assessments.map((a) => [a.controlId, a]));

    const allIds = [...new Set([...mapA.keys(), ...mapB.keys()])].sort();

    const statusRank = {
      [ControlStatus.FAIL]: 0,
      [ControlStatus.PARTIAL]: 1,
      [ControlStatus.NOT_TESTED]: 2,
      [ControlStatus.NOT_APPLICABLE]: 3,
      [ControlStatus.PASS]: 4,
    };

    const improved = [];
    const regressed = [];
    const unchanged = [];
    const newControls = [];
    const removedControls = [];

    for (const cid of allIds) {
      const aCtrl = mapA.get(cid);
      const bCtrl = mapB.get(cid);

      if (aCtrl && !bCtrl) {
        removedControls.push(cid);
      } else if (bCtrl && !aCtrl) {
        newControls.push(cid);
      } else if (aCtrl && bCtrl) {
        const rankA = statusRank[aCtrl.status] ?? -1;
        const rankB = statusRank[bCtrl.status] ?? -1;
        if (rankB > rankA) {
          improved.push({ control_id: cid, from: aCtrl.status, to: bCtrl.status });
        } else if (rankB < rankA) {
          regressed.push({ control_id: cid, from: aCtrl.status, to: bCtrl.status });
        } else {
          unchanged.push(cid);
        }
      }
    }

    const scoreDelta = reportB.overallScore - reportA.overallScore;

    return {
      report_a_id: reportA.reportId,
      report_b_id: reportB.reportId,
      framework: reportA.framework,
      score_a: Math.round(reportA.overallScore * 100) / 100,
      score_b: Math.round(reportB.overallScore * 100) / 100,
      score_delta: Math.round(scoreDelta * 100) / 100,
      improved,
      regressed,
      unchanged_count: unchanged.length,
      new_controls: newControls,
      removed_controls: removedControls,
      summary:
        `Score changed from ${reportA.overallScore.toFixed(1)}% to ` +
        `${reportB.overallScore.toFixed(1)}% (Δ ${scoreDelta >= 0 ? '+' : ''}${scoreDelta.toFixed(1)}%). ` +
        `${improved.length} improved, ${regressed.length} regressed.`,
    };
  }

  /**
   * Export report as CSV for import into GRC tools.
   * @param {ComplianceReport} report
   * @returns {string}
   */
  exportCsv(report) {
    const escape = (val) => {
      const s = String(val);
      if (s.includes(',') || s.includes('"') || s.includes('\n')) {
        return `"${s.replace(/"/g, '""')}"`;
      }
      return s;
    };

    const headers = [
      'Report ID',
      'Framework',
      'Control ID',
      'Control Name',
      'Status',
      'Severity',
      'Evidence',
      'Gaps',
      'Recommendations',
      'Tested At',
    ];

    const rows = [headers.map(escape).join(',')];
    for (const a of report.assessments) {
      rows.push(
        [
          report.reportId,
          report.framework,
          a.controlId,
          a.controlName,
          a.status,
          a.severity,
          a.evidence.join('; '),
          a.gaps.join('; '),
          a.recommendations.join('; '),
          a.testedAt,
        ]
          .map(escape)
          .join(','),
      );
    }
    return rows.join('\n') + '\n';
  }

  // -- private helpers ------------------------------------------------------

  /**
   * Match a finding to relevant control IDs based on category keywords.
   * @param {object} finding
   * @param {string} framework
   * @returns {string[]}
   */
  _mapFindingToControls(finding, framework) {
    const text = [
      String(finding.title || ''),
      String(finding.description || ''),
      String(finding.category || ''),
    ]
      .join(' ')
      .toLowerCase();

    const matchedCategories = new Set();
    for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
      for (const kw of keywords) {
        if (text.includes(kw)) {
          matchedCategories.add(category);
          break;
        }
      }
    }

    // Fallback: try explicit category field
    if (matchedCategories.size === 0) {
      const explicit = (finding.category || '').toLowerCase();
      if (CATEGORY_KEYWORDS[explicit]) {
        matchedCategories.add(explicit);
      }
    }

    const catMap = this._categoryToControls.get(framework);
    if (!catMap) return [];

    const controlIds = [];
    const seen = new Set();
    for (const cat of matchedCategories) {
      for (const cid of catMap.get(cat) || []) {
        if (!seen.has(cid)) {
          seen.add(cid);
          controlIds.push(cid);
        }
      }
    }

    return controlIds;
  }

  /**
   * Weighted compliance score (0-100).
   *
   * PASS = full weight, PARTIAL = 50%, FAIL/NOT_TESTED = 0, NA excluded.
   * @param {ControlAssessment[]} assessments
   * @returns {number}
   */
  _calculateScore(assessments) {
    let totalWeight = 0;
    let earnedWeight = 0;

    for (const a of assessments) {
      if (a.status === ControlStatus.NOT_APPLICABLE) continue;
      const weight = SEVERITY_WEIGHTS[a.severity] ?? 0.5;
      totalWeight += weight;
      if (a.status === ControlStatus.PASS) {
        earnedWeight += weight;
      } else if (a.status === ControlStatus.PARTIAL) {
        earnedWeight += weight * 0.5;
      }
    }

    if (totalWeight === 0) return 0;
    return (earnedWeight / totalWeight) * 100;
  }

  /**
   * Return the most severe level from a list.
   * @param {string[]} severities
   * @returns {string}
   */
  static _worstSeverity(severities) {
    const order = ['critical', 'high', 'medium', 'low', 'info'];
    for (const level of order) {
      if (severities.includes(level)) return level;
    }
    return 'info';
  }

  /**
   * Assemble a ComplianceReport from a list of assessments.
   * @param {ControlAssessment[]} assessments
   * @param {string} framework
   * @param {string} scope
   * @returns {ComplianceReport}
   */
  _buildReport(assessments, framework, scope) {
    const score = this._calculateScore(assessments);
    const counts = {
      [ControlStatus.PASS]: 0,
      [ControlStatus.FAIL]: 0,
      [ControlStatus.PARTIAL]: 0,
      [ControlStatus.NOT_APPLICABLE]: 0,
      [ControlStatus.NOT_TESTED]: 0,
    };
    for (const a of assessments) {
      counts[a.status] = (counts[a.status] || 0) + 1;
    }

    const total = assessments.length;
    const passCount = counts[ControlStatus.PASS];
    const failCount = counts[ControlStatus.FAIL];
    const partialCount = counts[ControlStatus.PARTIAL];
    const naCount = counts[ControlStatus.NOT_APPLICABLE];

    const summaryParts = [
      `Assessment of ${total} ${framework} controls completed.`,
      `${passCount} passed, ${failCount} failed, ${partialCount} partial, ${naCount} N/A.`,
      `Overall compliance score: ${score.toFixed(1)}%.`,
    ];
    if (failCount) {
      const criticalFails = assessments.filter(
        (a) => a.status === ControlStatus.FAIL && a.severity === 'critical',
      );
      if (criticalFails.length) {
        summaryParts.push(
          `CRITICAL: ${criticalFails.length} controls have critical failures requiring immediate remediation.`,
        );
      }
    }

    return new ComplianceReport({
      reportId: randomUUID(),
      framework,
      scope: scope || 'Full environment',
      controlsTotal: total,
      controlsPass: passCount,
      controlsFail: failCount,
      controlsPartial: partialCount,
      controlsNa: naCount,
      overallScore: Math.round(score * 100) / 100,
      assessments,
      executiveSummary: summaryParts.join(' '),
    });
  }
}
