// output.mjs — Finding and result builders, JSON envelope
// Zero dependencies (uses severity.mjs).

import { riskScore, verdict, riskBand, owaspCategorize } from './severity.mjs';

let findingCounter = 0;

/**
 * Reset the global finding counter.
 * Called between scanner runs in the orchestrator and before each test.
 */
export function resetCounter() {
  findingCounter = 0;
}

/**
 * Create a finding object.
 * @param {object} opts
 * @param {string} opts.scanner - Scanner prefix (UNI, ENT, PRM, DEP, TNT, GIT, NET)
 * @param {string} opts.severity - From SEVERITY constants
 * @param {string} opts.title - Short finding title
 * @param {string} opts.description - Detailed description
 * @param {string} [opts.file] - Affected file path (relative)
 * @param {number} [opts.line] - Line number
 * @param {string} [opts.evidence] - Redacted evidence string
 * @param {string} [opts.owasp] - OWASP reference (e.g. "LLM01")
 * @param {string} [opts.recommendation] - Fix suggestion
 * @returns {object}
 */
export function finding(opts) {
  findingCounter++;
  const id = `DS-${opts.scanner}-${String(findingCounter).padStart(3, '0')}`;
  return {
    id,
    scanner: opts.scanner,
    severity: opts.severity,
    title: opts.title,
    description: opts.description,
    file: opts.file || null,
    line: opts.line || null,
    evidence: opts.evidence || null,
    owasp: opts.owasp || null,
    recommendation: opts.recommendation || null,
  };
}

/**
 * Create a scanner result envelope.
 * @param {string} scannerName
 * @param {'ok'|'error'|'skipped'} status
 * @param {object[]} findings
 * @param {number} filesScanned
 * @param {number} durationMs
 * @param {string} [errorMsg]
 * @returns {object}
 */
export function scannerResult(scannerName, status, findings, filesScanned, durationMs, errorMsg) {
  const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
  for (const f of findings) {
    counts[f.severity] = (counts[f.severity] || 0) + 1;
  }
  const result = {
    scanner: scannerName,
    status,
    files_scanned: filesScanned,
    duration_ms: durationMs,
    findings,
    counts,
  };
  if (errorMsg) result.error = errorMsg;
  return result;
}

/**
 * Create a fix result object for the auto-cleaner.
 * @param {object} opts
 * @param {string} opts.finding_id - Original finding ID (e.g. "DS-UNI-001")
 * @param {string} opts.file - Affected file path (relative)
 * @param {string} opts.operation - Fix operation name (e.g. "strip_zero_width")
 * @param {'applied'|'skipped'|'failed'} opts.status
 * @param {string} opts.description - What was done
 * @param {string} [opts.error] - Error message if failed
 * @returns {object}
 */
export function fixResult(opts) {
  const result = {
    finding_id: opts.finding_id,
    file: opts.file,
    operation: opts.operation,
    status: opts.status,
    description: opts.description,
  };
  if (opts.error) result.error = opts.error;
  return result;
}

/**
 * Build the top-level output envelope for the auto-cleaner.
 * @param {string} targetPath
 * @param {boolean} dryRun
 * @param {object[]} fixes - Array of fixResult objects
 * @param {object[]} errors - Array of error objects
 * @param {number} durationMs
 * @returns {object}
 */
export function cleanEnvelope(targetPath, dryRun, fixes, errors, durationMs) {
  const applied = fixes.filter(f => f.status === 'applied').length;
  const skipped = fixes.filter(f => f.status === 'skipped').length;
  const failed = fixes.filter(f => f.status === 'failed').length;
  const filesModified = new Set(fixes.filter(f => f.status === 'applied').map(f => f.file)).size;

  return {
    meta: {
      target: targetPath,
      timestamp: new Date().toISOString(),
      dry_run: dryRun,
      duration_ms: durationMs,
    },
    summary: {
      findings_received: fixes.length + errors.length,
      fixes_applied: applied,
      fixes_skipped: skipped,
      fixes_failed: failed,
      files_modified: filesModified,
    },
    fixes,
    errors,
  };
}

/**
 * Build the top-level output envelope from all scanner results.
 * @param {string} targetPath
 * @param {Record<string, object>} scannerResults - keyed by scanner short name
 * @param {number} totalDurationMs
 * @returns {object}
 */
export function envelope(targetPath, scannerResults, totalDurationMs) {
  const aggCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
  const allFindings = [];
  let totalFindings = 0;
  let scannersOk = 0;
  let scannersError = 0;
  let scannersSkipped = 0;

  for (const r of Object.values(scannerResults)) {
    for (const sev of Object.keys(aggCounts)) {
      aggCounts[sev] += r.counts[sev] || 0;
    }
    totalFindings += r.findings.length;
    allFindings.push(...r.findings);
    if (r.status === 'ok') scannersOk++;
    else if (r.status === 'error') scannersError++;
    else if (r.status === 'skipped') scannersSkipped++;
  }

  return {
    meta: {
      target: targetPath,
      timestamp: new Date().toISOString(),
      node_version: process.version,
      total_duration_ms: totalDurationMs,
    },
    scanners: scannerResults,
    aggregate: {
      total_findings: totalFindings,
      counts: aggCounts,
      risk_score: riskScore(aggCounts),
      risk_band: riskBand(riskScore(aggCounts)),
      verdict: verdict(aggCounts),
      owasp_breakdown: owaspCategorize(allFindings),
      scanners_ok: scannersOk,
      scanners_error: scannersError,
      scanners_skipped: scannersSkipped,
    },
  };
}
