/**
 * Validates `@rule BR-XXX-YYY-NNN` citations in source code against the
 * business-rules index. Reports:
 *
 *  - Orphan citations: code references a rule ID that does not exist in the catalog.
 *  - Zombie rules: an `activa` rule older than 30 days without any code citation.
 *
 * Citations are OPTIONAL. This script reports findings; it does not fail builds.
 *
 * Invoked via `pnpm br:validate:citations`.
 */
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
import fastGlob from 'fast-glob';
import { findRepoRoot } from './parse-catalog';
import type { CatalogIndex } from './types';

const CITATION_PATTERN = /@rule\s+(BR-[A-Z0-9-]+)/g;
const STALE_AGE_DAYS = 30;

interface Citation {
  ruleId: string;
  filePath: string;
  line: number;
}

async function findCitations(repoRoot: string): Promise<Citation[]> {
  const patterns = ['backend/src/**/*.{ts,tsx}', 'frontend/src/**/*.{ts,tsx,html}'];
  const files = await fastGlob(patterns, {
    cwd: repoRoot,
    absolute: true,
    ignore: ['**/node_modules/**', '**/dist/**', '**/.angular/**'],
  });
  const citations: Citation[] = [];
  for (const filePath of files) {
    let content: string;
    try {
      content = readFileSync(filePath, 'utf-8');
    } catch {
      continue;
    }
    if (!content.includes('@rule')) continue;
    const lines = content.split('\n');
    for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
      const line = lines[lineIdx];
      for (const m of line.matchAll(CITATION_PATTERN)) {
        citations.push({
          ruleId: m[1],
          filePath,
          line: lineIdx + 1,
        });
      }
    }
  }
  return citations;
}

function loadIndex(repoRoot: string): CatalogIndex | null {
  const path = resolve(repoRoot, 'cliter/business-rules-index.json');
  if (!existsSync(path)) return null;
  return JSON.parse(readFileSync(path, 'utf-8')) as CatalogIndex;
}

function daysSince(isoDate: string): number {
  const then = new Date(isoDate).getTime();
  const now = Date.now();
  return Math.floor((now - then) / (1000 * 60 * 60 * 24));
}

async function main(): Promise<void> {
  const repoRoot = findRepoRoot();
  const index = loadIndex(repoRoot);
  if (!index) {
    console.error(
      '✗ cliter/business-rules-index.json no existe. Ejecuta `pnpm br:generate` primero.',
    );
    process.exit(1);
  }

  const citations = await findCitations(repoRoot);
  const orphans: Citation[] = [];
  const cited = new Set<string>();
  for (const c of citations) {
    if (!index.byId[c.ruleId]) orphans.push(c);
    else cited.add(c.ruleId);
  }

  const zombies: Array<{ ruleId: string; ageDays: number }> = [];
  for (const rule of Object.values(index.byId)) {
    if (rule.state !== 'active') continue;
    if (cited.has(rule.id)) continue;
    const age = daysSince(rule.last_updated);
    if (age >= STALE_AGE_DAYS) {
      zombies.push({ ruleId: rule.id, ageDays: age });
    }
  }

  console.log('Citaciones encontradas:', citations.length);
  console.log('Reglas con al menos una cita:', cited.size);

  if (orphans.length > 0) {
    console.log(`\n⚠ Citaciones huérfanas (${orphans.length}):`);
    for (const o of orphans) {
      console.log(`  - ${o.ruleId} en ${o.filePath}:${o.line}`);
    }
  } else {
    console.log('\n✓ Sin citaciones huérfanas.');
  }

  if (zombies.length > 0) {
    console.log(`\n⚠ Reglas zombi candidatas (${zombies.length}):`);
    console.log(`  Reglas activas con > ${STALE_AGE_DAYS} días sin citas en código:`);
    for (const z of zombies) {
      console.log(`  - ${z.ruleId} (${z.ageDays} días)`);
    }
  } else {
    console.log('\n✓ Sin reglas zombi.');
  }
}

main().catch((err) => {
  console.error('✗ validate-citations falló:');
  console.error(err.message ?? err);
  process.exit(1);
});
