/**
 * QUALITY_REPORT.md Generator
 *
 * Standalone script that runs all graph quality tests programmatically
 * and generates a comprehensive QUALITY_REPORT.md in docs/.
 *
 * Usage: npx tsx packages/mcp-server/test/graph-quality/generate-proof.ts
 *    or: npm run test:quality:report
 */

import {
  buildGroundTruthVault,
  stripLinks,
  runSuggestionsOnVault,
  evaluateSuggestions,
  computeGraphHealth,
  loadPrimaryVault,
  loadArchetype,
  loadChaosVault,
  type TempVault,
  type GroundTruthSpec,
  type PrecisionRecallReport,
  type GraphHealthReport,
} from './harness.js';
import type { ScoringLayer, StrictnessMode } from '../../src/core/write/types.js';
import { readFile, writeFile } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

// __dirname equivalent for ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// =============================================================================
// Configuration
// =============================================================================

const STRICTNESS_MODES: StrictnessMode[] = ['conservative', 'balanced', 'aggressive'];

const SCORING_LAYERS: ScoringLayer[] = [
  'length_filter', 'article_filter',
  'exact_match', 'stem_match', 'fuzzy_match',
  'cooccurrence', 'rarity',
  'type_boost', 'context_boost',
  'recency', 'cross_folder',
  'hub_boost', 'feedback', 'semantic',
  'edge_weight',
];

const ARCHETYPE_NAMES = [
  'hub-and-spoke',
  'hierarchical',
  'dense-mesh',
  'sparse-orphan',
  'bridge-network',
  'small-world',
];

/** Graph health target ranges for topology metrics */
const HEALTH_TARGETS: Record<string, { min: number; max: number; label: string }> = {
  linkDensity:            { min: 2.0, max: 10.0,  label: 'Link Density (links/note)' },
  orphanRate:             { min: 0.0, max: 0.15,   label: 'Orphan Rate' },
  entityCoverage:         { min: 0.5, max: 1.0,    label: 'Entity Coverage' },
  connectedness:          { min: 0.7, max: 1.0,    label: 'Connectedness (LCC ratio)' },
  giniCoefficient:        { min: 0.2, max: 0.6,    label: 'Gini Coefficient' },
  // Primary vault is a cross-domain narrative graph; neighbors rarely link to each other.
  // Dense-mesh archetype validates high-clustering detection (0.5+).
  clusteringCoefficient:  { min: 0.09, max: 0.8,   label: 'Clustering Coefficient' },
  avgPathLength:          { min: 1.5, max: 6.0,    label: 'Avg Path Length' },
};

// =============================================================================
// Helpers
// =============================================================================

function pct(n: number): string {
  return `${(n * 100).toFixed(1)}%`;
}

function fmt(n: number, dp = 3): string {
  return n.toFixed(dp);
}

function passIcon(pass: boolean): string {
  return pass ? 'PASS' : 'FAIL';
}

function inRange(value: number, min: number, max: number): boolean {
  return value >= min && value <= max;
}

function layerStatus(delta: number): string {
  if (delta >= 0.05) return 'CORE';
  if (delta >= 0.01) return 'Contributing';
  if (delta > 0) return 'Marginal';
  return 'Dormant';
}

async function getEngineVersion(): Promise<string> {
  try {
    const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
    const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
    return pkg.version || 'unknown';
  } catch {
    return 'unknown';
  }
}

// =============================================================================
// Main Generator
// =============================================================================

async function main() {
  const output: string[] = [];
  const emit = (line: string) => output.push(line);
  const emitBlank = () => output.push('');

  const version = await getEngineVersion();
  const dateStr = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');

  console.log(`QUALITY_REPORT.md generator v${version}`);
  console.log('='.repeat(50));

  // =========================================================================
  // Header
  // =========================================================================
  emit('# PROVE-IT: Graph Quality Evidence');
  emitBlank();
  emit(`> Auto-generated by \`npm run test:quality:report\` on ${dateStr}`);
  emit(`> Engine version: ${version}`);
  emitBlank();
  emit('- [1. Vault Configuration](#1-vault-configuration)');
  emit('- [2. Precision/Recall by Strictness Mode](#2-precisionrecall-by-strictness-mode)');
  emit('- [3. Ranking Metrics](#3-ranking-metrics)');
  emit('- [4. By-Tier Recall (Balanced Mode)](#4-by-tier-recall-balanced-mode)');
  emit('- [5. By-Category F1 (Balanced Mode)](#5-by-category-f1-balanced-mode)');
  emit('- [6. Layer Ablation (Balanced Mode)](#6-layer-ablation-balanced-mode)');
  emit('- [7. Cross-Archetype Comparison](#7-cross-archetype-comparison)');
  emit('- [8. Chaos Vault Resilience](#8-chaos-vault-resilience)');
  emit('- [9. Graph Health (Primary Vault)](#9-graph-health-primary-vault)');
  emit('- [10. Topology Metrics](#10-topology-metrics)');
  emit('- [11. Methodology](#11-methodology)');
  emitBlank();

  // =========================================================================
  // Section 1: Primary Vault
  // =========================================================================
  let spec: GroundTruthSpec;
  let vault: TempVault | null = null;
  let balancedReport: PrecisionRecallReport | null = null;
  let strictnessReports: Map<StrictnessMode, PrecisionRecallReport> = new Map();

  try {
    console.log('\n[1/9] Loading primary vault fixture...');
    spec = await loadPrimaryVault();

    const tier1 = spec.groundTruth.filter(g => g.tier === 1).length;
    const tier2 = spec.groundTruth.filter(g => g.tier === 2).length;
    const tier3 = spec.groundTruth.filter(g => g.tier === 3).length;
    const totalGt = spec.groundTruth.length;

    emit('## 1. Vault Configuration');
    emitBlank();
    emit(`- Primary vault: **${spec.notes.length}** notes, **${spec.entities.length}** entities, seed: \`${spec.seed}\``);
    emit(`- Ground truth: **${totalGt}** links (${pct(tier1 / totalGt)} easy, ${pct(tier2 / totalGt)} medium, ${pct(tier3 / totalGt)} hard)`);
    emit(`- Description: ${spec.description}`);
    emitBlank();

    // Build vault and strip links
    console.log('  Building vault and stripping ground truth links...');
    vault = await buildGroundTruthVault(spec);
    await stripLinks(vault, spec.groundTruth);
    console.log('  Vault ready.');

    // =========================================================================
    // Section 2: Precision/Recall by Strictness Mode
    // =========================================================================
    console.log('\n[2/9] Running suggestions across strictness modes...');

    emit('## 2. Precision/Recall by Strictness Mode');
    emitBlank();
    emit('| Mode | Precision | Recall | F1 | FP Rate | Suggestions |');
    emit('|---|---|---|---|---|---|');

    for (const mode of STRICTNESS_MODES) {
      console.log(`  Running ${mode} mode...`);
      const runs = await runSuggestionsOnVault(vault, { strictness: mode });
      const report = evaluateSuggestions(runs, spec.groundTruth, spec.entities);
      strictnessReports.set(mode, report);
      if (mode === 'balanced') balancedReport = report;

      emit(`| ${mode} | ${pct(report.precision)} | ${pct(report.recall)} | ${pct(report.f1)} | ${pct(report.fpRate)} | ${report.totalSuggestions} |`);
    }
    emitBlank();

    // =========================================================================
    // Section 3: Ranking Metrics
    // =========================================================================
    console.log('\n[3/9] Computing ranking metrics...');

    emit('## 3. Ranking Metrics');
    emitBlank();

    if (balancedReport) {
      emit('| Metric | Value |');
      emit('|---|---|');
      emit(`| MRR | ${fmt(balancedReport.mrr)} |`);
      emit(`| Hits@3 | ${pct(balancedReport.hitsAt3)} |`);
      emit(`| Precision@3 | ${pct(balancedReport.precisionAtK)} |`);
      emitBlank();
    } else {
      emit('*Ranking metrics unavailable (balanced mode did not run).*');
      emitBlank();
    }

    // =========================================================================
    // Section 4: By-Tier Recall (Balanced Mode)
    // =========================================================================
    console.log('\n[4/9] By-tier breakdown...');

    emit('## 4. By-Tier Recall (Balanced Mode)');
    emitBlank();

    if (balancedReport) {
      emit('| Tier | Count | Precision | Recall | F1 |');
      emit('|---|---|---|---|---|');

      const tierLabels = { 1: '1 (Easy)', 2: '2 (Medium)', 3: '3 (Hard)' } as const;
      for (const tier of [1, 2, 3] as const) {
        const t = balancedReport.byTier[tier];
        emit(`| ${tierLabels[tier]} | ${t.count} | ${pct(t.precision)} | ${pct(t.recall)} | ${pct(t.f1)} |`);
      }
      emitBlank();
    } else {
      emit('*Tier breakdown unavailable.*');
      emitBlank();
    }

    // =========================================================================
    // Section 5: By-Category F1 (Balanced Mode)
    // =========================================================================
    console.log('\n[5/9] By-category breakdown...');

    emit('## 5. By-Category F1 (Balanced Mode)');
    emitBlank();

    if (balancedReport && Object.keys(balancedReport.byCategory).length > 0) {
      emit('| Category | Count | Precision | Recall | F1 |');
      emit('|---|---|---|---|---|');

      const sortedCats = Object.entries(balancedReport.byCategory)
        .sort(([, a], [, b]) => b.count - a.count);

      for (const [cat, stats] of sortedCats) {
        emit(`| ${cat} | ${stats.count} | ${pct(stats.precision)} | ${pct(stats.recall)} | ${pct(stats.f1)} |`);
      }
      emitBlank();
    } else {
      emit('*Category breakdown unavailable.*');
      emitBlank();
    }

  } catch (err) {
    console.error('  ERROR in primary vault sections:', err);
    emit('');
    emit(`> **Error** in primary vault evaluation: ${err instanceof Error ? err.message : String(err)}`);
    emitBlank();
  }

  // =========================================================================
  // Section 6: Layer Ablation (Balanced Mode)
  // =========================================================================
  try {
    console.log('\n[6/9] Running layer ablation study...');

    emit('## 6. Layer Ablation (Balanced Mode)');
    emitBlank();

    if (vault && spec!) {
      // Get baseline F1 (balanced with all layers)
      const baselineReport = balancedReport || (() => {
        // Should already have it but just in case
        return null;
      })();

      if (baselineReport) {
        const baselineF1 = baselineReport.f1;

        emit('| Layer | F1 (all) | F1 (ablated) | Delta F1 | Status |');
        emit('|---|---|---|---|---|');

        for (const layer of SCORING_LAYERS) {
          console.log(`  Ablating layer: ${layer}...`);

          try {
            const runs = await runSuggestionsOnVault(vault, {
              strictness: 'balanced',
              disabledLayers: [layer],
            });
            const report = evaluateSuggestions(runs, spec!.groundTruth, spec!.entities);
            const delta = baselineF1 - report.f1;
            const status = layerStatus(delta);

            emit(`| ${layer} | ${pct(baselineF1)} | ${pct(report.f1)} | ${delta >= 0 ? '+' : ''}${pct(delta)} | ${status} |`);
          } catch (err) {
            emit(`| ${layer} | ${pct(baselineF1)} | ERROR | - | - |`);
            console.error(`    Error ablating ${layer}:`, err);
          }
        }
        emitBlank();
      } else {
        emit('*Layer ablation unavailable (no baseline report).*');
        emitBlank();
      }
    } else {
      emit('*Layer ablation unavailable (vault not built).*');
      emitBlank();
    }
  } catch (err) {
    console.error('  ERROR in layer ablation:', err);
    emit('');
    emit(`> **Error** in layer ablation: ${err instanceof Error ? err.message : String(err)}`);
    emitBlank();
  }

  // =========================================================================
  // Section 7: Cross-Archetype Comparison
  // =========================================================================
  try {
    console.log('\n[7/9] Testing archetype vaults...');

    emit('## 7. Cross-Archetype Comparison');
    emitBlank();
    emit('| Archetype | Precision | Recall | F1 | Notes |');
    emit('|---|---|---|---|---|');

    for (const archName of ARCHETYPE_NAMES) {
      let archVault: TempVault | null = null;
      try {
        console.log(`  Loading archetype: ${archName}...`);
        const archSpec = await loadArchetype(archName);
        archVault = await buildGroundTruthVault(archSpec);
        await stripLinks(archVault, archSpec.groundTruth);

        const runs = await runSuggestionsOnVault(archVault, { strictness: 'balanced' });
        const report = evaluateSuggestions(runs, archSpec.groundTruth, archSpec.entities);

        const noteStr = archSpec.description || `${archSpec.notes.length} notes, ${archSpec.entities.length} entities`;
        emit(`| ${archName} | ${pct(report.precision)} | ${pct(report.recall)} | ${pct(report.f1)} | ${noteStr} |`);
        console.log(`    F1: ${pct(report.f1)}`);
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg.includes('ENOENT') || msg.includes('no such file')) {
          emit(`| ${archName} | - | - | - | *Fixture not found* |`);
          console.log(`    Skipped (fixture not found)`);
        } else {
          emit(`| ${archName} | - | - | - | *Error: ${msg.slice(0, 60)}* |`);
          console.error(`    Error:`, err);
        }
      } finally {
        if (archVault) {
          try { await archVault.cleanup(); } catch { /* ignore cleanup errors */ }
        }
      }
    }
    emitBlank();
  } catch (err) {
    console.error('  ERROR in archetype section:', err);
    emit('');
    emit(`> **Error** in archetype comparison: ${err instanceof Error ? err.message : String(err)}`);
    emitBlank();
  }

  // =========================================================================
  // Section 8: Chaos Vault Resilience
  // =========================================================================
  try {
    console.log('\n[8/9] Testing chaos vault...');

    emit('## 8. Chaos Vault Resilience');
    emitBlank();

    let chaosVault: TempVault | null = null;
    try {
      const chaosSpec = await loadChaosVault();
      chaosVault = await buildGroundTruthVault(chaosSpec);
      await stripLinks(chaosVault, chaosSpec.groundTruth);

      const chaosRuns = await runSuggestionsOnVault(chaosVault, { strictness: 'balanced' });
      const chaosReport = evaluateSuggestions(chaosRuns, chaosSpec.groundTruth, chaosSpec.entities);

      emit('| Metric | Clean (primary) | Chaos | Delta |');
      emit('|---|---|---|---|');

      if (balancedReport) {
        const metrics: Array<{ label: string; clean: number; chaos: number }> = [
          { label: 'Precision', clean: balancedReport.precision, chaos: chaosReport.precision },
          { label: 'Recall', clean: balancedReport.recall, chaos: chaosReport.recall },
          { label: 'F1', clean: balancedReport.f1, chaos: chaosReport.f1 },
          { label: 'FP Rate', clean: balancedReport.fpRate, chaos: chaosReport.fpRate },
          { label: 'MRR', clean: balancedReport.mrr, chaos: chaosReport.mrr },
        ];

        for (const m of metrics) {
          const delta = m.chaos - m.clean;
          emit(`| ${m.label} | ${pct(m.clean)} | ${pct(m.chaos)} | ${delta >= 0 ? '+' : ''}${pct(delta)} |`);
        }
      } else {
        emit(`| Precision | - | ${pct(chaosReport.precision)} | - |`);
        emit(`| Recall | - | ${pct(chaosReport.recall)} | - |`);
        emit(`| F1 | - | ${pct(chaosReport.f1)} | - |`);
        emit(`| FP Rate | - | ${pct(chaosReport.fpRate)} | - |`);
        emit(`| MRR | - | ${pct(chaosReport.mrr)} | - |`);
      }
      emitBlank();
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (msg.includes('ENOENT') || msg.includes('no such file')) {
        emit('*Chaos vault fixture not found. Skipped.*');
        console.log('  Skipped (fixture not found)');
      } else {
        emit(`> **Error** in chaos vault: ${msg}`);
        console.error('  Error:', err);
      }
      emitBlank();
    } finally {
      if (chaosVault) {
        try { await chaosVault.cleanup(); } catch { /* ignore */ }
      }
    }
  } catch (err) {
    console.error('  ERROR in chaos vault section:', err);
    emit('');
    emit(`> **Error** in chaos vault: ${err instanceof Error ? err.message : String(err)}`);
    emitBlank();
  }

  // =========================================================================
  // Section 9: Graph Health (Primary Vault)
  // =========================================================================
  let healthBefore: GraphHealthReport | null = null;
  let healthAfter: GraphHealthReport | null = null;

  try {
    console.log('\n[9/9] Computing graph health metrics...');

    emit('## 9. Graph Health (Primary Vault)');
    emitBlank();

    if (vault) {
      // "Before" = vault with links stripped (current state)
      console.log('  Computing health before applying suggestions...');
      healthBefore = await computeGraphHealth(vault.vaultPath);

      // "After" = re-apply suggestion links to vault (simulate ideal state)
      // For the "after" state we rebuild the vault without stripping
      let afterVault: TempVault | null = null;
      try {
        afterVault = await buildGroundTruthVault(spec!);
        console.log('  Computing health after (original links intact)...');
        healthAfter = await computeGraphHealth(afterVault.vaultPath);
      } finally {
        if (afterVault) {
          try { await afterVault.cleanup(); } catch { /* ignore */ }
        }
      }

      if (healthBefore && healthAfter) {
        emit('| Metric | Before (stripped) | After (linked) | Target | Pass |');
        emit('|---|---|---|---|---|');

        for (const [key, target] of Object.entries(HEALTH_TARGETS)) {
          const before = (healthBefore as Record<string, unknown>)[key] as number;
          const after = (healthAfter as Record<string, unknown>)[key] as number;
          if (before !== undefined && after !== undefined) {
            const pass = inRange(after, target.min, target.max);
            emit(`| ${target.label} | ${fmt(before)} | ${fmt(after)} | ${fmt(target.min)}-${fmt(target.max)} | ${passIcon(pass)} |`);
          }
        }
        emitBlank();
      }
    } else {
      emit('*Graph health unavailable (vault not built).*');
      emitBlank();
    }
  } catch (err) {
    console.error('  ERROR in graph health section:', err);
    emit('');
    emit(`> **Error** in graph health: ${err instanceof Error ? err.message : String(err)}`);
    emitBlank();
  }

  // =========================================================================
  // Section 10: Topology Metrics
  // =========================================================================
  try {
    emit('## 10. Topology Metrics');
    emitBlank();

    const health = healthAfter || healthBefore;
    if (health) {
      emit('| Metric | Value | Target Range | Pass |');
      emit('|---|---|---|---|');

      const topologyMetrics: Array<{
        label: string;
        value: number;
        min: number;
        max: number;
      }> = [
        { label: 'Clustering Coefficient', value: health.clusteringCoefficient, min: 0.09, max: 0.8 },
        { label: 'Avg Path Length', value: health.avgPathLength, min: 1.5, max: 6.0 },
        { label: 'Gini Coefficient', value: health.giniCoefficient, min: 0.2, max: 0.6 },
        { label: 'Connectedness (LCC)', value: health.connectedness, min: 0.7, max: 1.0 },
        { label: 'Top-5% Betweenness Share', value: health.betweennessCentrality.top5PctShare, min: 0.1, max: 0.6 },
        { label: 'Degree Centrality StdDev', value: health.degreeCentralityStdDev, min: 0.5, max: 5.0 },
        { label: 'Cluster Count', value: health.clusterCount, min: 1, max: 5 },
      ];

      for (const m of topologyMetrics) {
        const pass = inRange(m.value, m.min, m.max);
        emit(`| ${m.label} | ${fmt(m.value)} | ${fmt(m.min)}-${fmt(m.max)} | ${passIcon(pass)} |`);
      }
      emitBlank();
    } else {
      emit('*Topology metrics unavailable (no health data).*');
      emitBlank();
    }
  } catch (err) {
    console.error('  ERROR in topology section:', err);
    emit('');
    emit(`> **Error** in topology metrics: ${err instanceof Error ? err.message : String(err)}`);
    emitBlank();
  }

  // =========================================================================
  // Section 11: Methodology
  // =========================================================================
  emit('## 11. Methodology');
  emitBlank();
  emit('- All tests deterministic (fixed seeds)');
  emit('- Ground truth links are stripped from notes before testing; the engine must rediscover them');
  emit('- Precision = true positives / total suggestions');
  emit('- Recall = true positives / total ground truth');
  emit('- F1 = harmonic mean of precision and recall');
  emit('- MRR = mean reciprocal rank of first correct suggestion per note');
  emit('- Layer ablation: each scoring layer disabled individually; F1 delta measures contribution');
  emit(`- Scoring layers (${SCORING_LAYERS.length}): ${SCORING_LAYERS.join(', ')}`);
  emit(`- Run: \`npm run test:quality:report\``);
  emit(`- Engine: flywheel-memory v${version}`);
  emitBlank();

  // =========================================================================
  // Cleanup and Write
  // =========================================================================
  if (vault) {
    try {
      console.log('\nCleaning up primary vault...');
      await vault.cleanup();
    } catch {
      /* ignore cleanup errors */
    }
  }

  const repoRoot = path.resolve(__dirname, '..', '..', '..', '..');
  const outPath = path.join(repoRoot, 'docs', 'QUALITY_REPORT.md');
  await writeFile(outPath, output.join('\n'), 'utf-8');

  console.log('\n' + '='.repeat(50));
  console.log(`Generated ${outPath}`);
  console.log(`${output.length} lines, ${output.join('\n').length} bytes`);
}

main().catch((err) => {
  console.error('Fatal error:', err);
  process.exit(1);
});
