/**
 * Diff engine for config-audit.
 * Compares two scanner envelopes (baseline vs current) to detect drift.
 * Zero external dependencies.
 */

import { scoreByArea } from './scoring.mjs';
import { gradeFromPassRate } from './severity.mjs';

/**
 * Diff two scanner envelopes.
 * @param {object} baseline - Full envelope from scan-orchestrator
 * @param {object} current - Full envelope from scan-orchestrator
 * @returns {object} Diff result with new, resolved, unchanged, moved findings + score changes
 */
export function diffEnvelopes(baseline, current) {
  const baseFindings = extractFindings(baseline);
  const currFindings = extractFindings(current);

  // Build lookup maps keyed by scanner+title+file
  const baseByKey = groupByKey(baseFindings);
  const currByKey = groupByKey(currFindings);

  // Also build maps by scanner+title (ignoring file) for moved detection
  const baseByScannerTitle = groupByScannerTitle(baseFindings);
  const currByScannerTitle = groupByScannerTitle(currFindings);

  const newFindings = [];
  const resolvedFindings = [];
  const unchangedFindings = [];
  const movedFindings = [];

  const matchedBaseKeys = new Set();
  const matchedCurrKeys = new Set();

  // Pass 1: exact matches (scanner+title+file)
  for (const [key, currList] of currByKey.entries()) {
    const baseList = baseByKey.get(key);
    if (baseList && baseList.length > 0) {
      // Match as many as possible
      const matchCount = Math.min(baseList.length, currList.length);
      for (let i = 0; i < matchCount; i++) {
        unchangedFindings.push(currList[i]);
      }
      // Extra in current = new
      for (let i = matchCount; i < currList.length; i++) {
        newFindings.push(currList[i]);
      }
      matchedBaseKeys.add(key);
      matchedCurrKeys.add(key);
    }
  }

  // Pass 2: find moved findings (same scanner+title, different file)
  const resolvedCandidates = [];
  const newCandidates = [];

  for (const [key, baseList] of baseByKey.entries()) {
    if (!matchedBaseKeys.has(key)) {
      resolvedCandidates.push(...baseList);
    } else {
      // Any extras in baseline beyond matched count
      const currList = currByKey.get(key) || [];
      const matchCount = Math.min(baseList.length, currList.length);
      for (let i = matchCount; i < baseList.length; i++) {
        resolvedCandidates.push(baseList[i]);
      }
    }
  }

  for (const [key, currList] of currByKey.entries()) {
    if (!matchedCurrKeys.has(key)) {
      newCandidates.push(...currList);
    }
  }

  // Try to pair resolved candidates with new candidates as "moved"
  const usedResolved = new Set();
  const usedNew = new Set();

  for (let i = 0; i < newCandidates.length; i++) {
    const curr = newCandidates[i];
    for (let j = 0; j < resolvedCandidates.length; j++) {
      if (usedResolved.has(j)) continue;
      const base = resolvedCandidates[j];
      if (base.scanner === curr.scanner && base.title === curr.title && base.file !== curr.file) {
        movedFindings.push({ from: base, to: curr });
        usedResolved.add(j);
        usedNew.add(i);
        break;
      }
    }
  }

  // Remaining unmatched
  for (let i = 0; i < resolvedCandidates.length; i++) {
    if (!usedResolved.has(i)) resolvedFindings.push(resolvedCandidates[i]);
  }
  for (let i = 0; i < newCandidates.length; i++) {
    if (!usedNew.has(i)) newFindings.push(newCandidates[i]);
  }

  // Score changes
  const baseAreas = scoreByArea(baseline.scanners || []);
  const currAreas = scoreByArea(current.scanners || []);

  const baseAvg = avgScore(baseAreas.areas);
  const currAvg = avgScore(currAreas.areas);

  const scoreChange = {
    before: { score: baseAvg, grade: gradeFromPassRate(baseAvg) },
    after: { score: currAvg, grade: gradeFromPassRate(currAvg) },
    delta: currAvg - baseAvg,
  };

  // Per-area changes
  const areaChanges = buildAreaChanges(baseAreas.areas, currAreas.areas);

  // Summary
  const totalBefore = baseFindings.length;
  const totalAfter = currFindings.length;
  const newCount = newFindings.length;
  const resolvedCount = resolvedFindings.length;

  let trend = 'stable';
  if (resolvedCount > newCount) trend = 'improving';
  else if (newCount > resolvedCount) trend = 'degrading';

  return {
    newFindings,
    resolvedFindings,
    unchangedFindings,
    movedFindings,
    scoreChange,
    areaChanges,
    summary: {
      totalBefore,
      totalAfter,
      newCount,
      resolvedCount,
      trend,
    },
  };
}

/**
 * Format a diff result into a human-readable terminal report.
 * @param {object} diff - Output from diffEnvelopes()
 * @returns {string}
 */
export function formatDiffReport(diff) {
  const lines = [];
  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  lines.push(' Config-Audit Drift Report');
  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
  lines.push('');

  // Trend
  const trendIcon = diff.summary.trend === 'improving' ? '↑'
    : diff.summary.trend === 'degrading' ? '↓' : '→';
  const trendLabel = diff.summary.trend.charAt(0).toUpperCase() + diff.summary.trend.slice(1);
  lines.push(` Trend: ${trendIcon} ${trendLabel}`);
  lines.push('');

  // Score
  const sc = diff.scoreChange;
  const deltaSign = sc.delta > 0 ? '+' : '';
  lines.push(` Score: ${sc.before.grade} (${sc.before.score}) → ${sc.after.grade} (${sc.after.score})  ${trendIcon} ${deltaSign}${sc.delta} points`);
  lines.push('');

  // New findings
  if (diff.newFindings.length > 0) {
    lines.push(` New findings (${diff.newFindings.length}):`);
    for (const f of diff.newFindings) {
      const fileInfo = f.file ? ` (${f.file})` : '';
      lines.push(`  - [${f.severity}] ${f.title}${fileInfo}`);
    }
    lines.push('');
  }

  // Resolved
  if (diff.resolvedFindings.length > 0) {
    lines.push(` Resolved (${diff.resolvedFindings.length}):`);
    for (const f of diff.resolvedFindings) {
      lines.push(`  - [${f.severity}] ${f.title}`);
    }
    lines.push('');
  }

  // Moved
  if (diff.movedFindings.length > 0) {
    lines.push(` Moved (${diff.movedFindings.length}):`);
    for (const m of diff.movedFindings) {
      lines.push(`  - [${m.from.severity}] ${m.from.title}: ${m.from.file} → ${m.to.file}`);
    }
    lines.push('');
  }

  // Area changes (only show areas with delta != 0)
  const changedAreas = diff.areaChanges.filter(a => a.delta !== 0);
  if (changedAreas.length > 0) {
    lines.push(' Area changes:');
    for (const a of changedAreas) {
      const sign = a.delta > 0 ? '↑' : '↓';
      const deltaStr = a.delta > 0 ? `+${a.delta}` : `${a.delta}`;
      const padding = '.'.repeat(Math.max(1, 20 - a.name.length));
      lines.push(`  ${a.name} ${padding} ${a.before.grade} (${a.before.score}) → ${a.after.grade} (${a.after.score})  ${sign} ${deltaStr}`);
    }
    lines.push('');
  }

  // Unchanged summary
  if (diff.unchangedFindings.length > 0) {
    lines.push(` Unchanged: ${diff.unchangedFindings.length} finding(s)`);
    lines.push('');
  }

  lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

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

// --- Internal helpers ---

function extractFindings(envelope) {
  const findings = [];
  for (const scanner of (envelope.scanners || [])) {
    for (const f of (scanner.findings || [])) {
      findings.push(f);
    }
  }
  return findings;
}

function findingKey(f) {
  return `${f.scanner}::${f.title}::${f.file || ''}`;
}

function scannerTitleKey(f) {
  return `${f.scanner}::${f.title}`;
}

function groupByKey(findings) {
  const map = new Map();
  for (const f of findings) {
    const key = findingKey(f);
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(f);
  }
  return map;
}

function groupByScannerTitle(findings) {
  const map = new Map();
  for (const f of findings) {
    const key = scannerTitleKey(f);
    if (!map.has(key)) map.set(key, []);
    map.get(key).push(f);
  }
  return map;
}

function avgScore(areas) {
  if (areas.length === 0) return 0;
  return Math.round(areas.reduce((s, a) => s + a.score, 0) / areas.length);
}

function buildAreaChanges(baseAreas, currAreas) {
  const baseMap = new Map(baseAreas.map(a => [a.name, a]));
  const currMap = new Map(currAreas.map(a => [a.name, a]));

  const allNames = new Set([...baseMap.keys(), ...currMap.keys()]);
  const changes = [];

  for (const name of allNames) {
    const before = baseMap.get(name) || { score: 0, grade: 'F' };
    const after = currMap.get(name) || { score: 0, grade: 'F' };
    changes.push({
      name,
      before: { score: before.score, grade: before.grade },
      after: { score: after.score, grade: after.grade },
      delta: after.score - before.score,
    });
  }

  return changes;
}
