#!/usr/bin/env node
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';

const NORMATIVE_FILES = {
  Pravila: 'docs/Pravila_raboty_Claude_v1_1.md',
  CLAUDE: 'CLAUDE.md',
  Tooling: 'docs/Tooling_v8_3.md',
  PSR_v1: 'docs/Plugin_stack_rules_v1.md',
  MEMORY: 'MEMORY.md',
};

const PATH_TO_NAME = {
  'docs/Pravila_raboty_Claude_v1_1.md': 'Pravila',
  'docs/Tooling_v8_3.md': 'Tooling',
  'docs/Plugin_stack_rules_v1.md': 'PSR_v1',
  'CLAUDE.md': 'CLAUDE',
  'MEMORY.md': 'MEMORY',
};

const VERSION_RE = /v(\d+\.\d+(?:\.\d+)?)/;
const VERSIYA_RE = /\*\*Версия:\*\*\s*v?(\d+\.\d+(?:\.\d+)?)/;

// Primary: markdown-link to normative file followed by first **vN.M** (bold
// may contain preceding words like "Прил. Н"). Anchored on link → precision.
const LINK_REF_RE =
  /\[[^\]]+\]\((docs\/Pravila_raboty_Claude_v1_1\.md|docs\/Tooling_v8_3\.md|docs\/Plugin_stack_rules_v1\.md|CLAUDE\.md|MEMORY\.md)\)(?:[^\n*]{0,200}?)\*\*[^*\n]*?v(\d+\.\d+(?:\.\d+)?)/g;

// Fallback: plain "Name vX.Y" mention. `\b` after the version forbids partial
// captures via backtracking (so "v1.30" never collapses to "v1.3"). Negative
// lookahead skips the left side of transitions ("v1.30→v1.31") since that is
// the FROM-side, not a current cross-ref.
const CROSS_REF_RE =
  /\b(Pravila|CLAUDE|Tooling|PSR_v1|MEMORY|PSR|Plugin_stack_rules|Pravila_raboty_Claude)\s+v(\d+\.\d+(?:\.\d+)?)\b(?!\s*→)/g;

// History-block marker. Normative shapki list past releases after the current
// one; the fallback scan must stop before the first such block, otherwise it
// picks up stale "наследие" cross-refs. Three shapes are used across the files:
//   "**v2.16 наследие:**"                   — CLAUDE.md / Tooling
//   "**Что изменилось в v1.29 относительно**" — Pravila
//   "**v3.15** — ..."                       — PSR_v1 / changelog entries
const HISTORY_MARKER_RE =
  /\*\*(?:v\d+\.\d+(?:\.\d+)?\s+наследие|Что изменилось в v\d+\.\d+(?:\.\d+)?\s+относительно|v\d+\.\d+(?:\.\d+)?[^*\n]{0,80}?\*\*\s+—)/;

export function extractVersion(text) {
  const head = text.split('\n').slice(0, 30);
  for (const line of head) {
    const m = line.match(VERSIYA_RE);
    if (m) return m[1];
  }
  for (const line of head.slice(0, 10)) {
    const m = line.match(VERSION_RE);
    if (m) return m[1];
  }
  return null;
}

function normalizeName(raw) {
  if (raw === 'PSR' || raw === 'Plugin_stack_rules') return 'PSR_v1';
  if (raw === 'Pravila_raboty_Claude') return 'Pravila';
  return raw;
}

// Truncate the text before the first history block. Applied to the fallback
// scan only — link-based detection runs on the whole file (a markdown-link to
// a normative file is precise enough that history blocks cannot pollute it).
function scopeBeforeHistory(text) {
  const m = text.match(HISTORY_MARKER_RE);
  return m ? text.slice(0, m.index) : text;
}

export function extractCrossRefs(text) {
  const refs = [];
  const seen = new Set();
  for (const match of text.matchAll(LINK_REF_RE)) {
    const name = PATH_TO_NAME[match[1]];
    if (!name || seen.has(name)) continue;
    seen.add(name);
    refs.push({ name, version: match[2] });
  }
  const fallbackScope = scopeBeforeHistory(text);
  for (const match of fallbackScope.matchAll(CROSS_REF_RE)) {
    const name = normalizeName(match[1]);
    if (seen.has(name)) continue;
    seen.add(name);
    refs.push({ name, version: match[2] });
  }
  return refs;
}

export function detectMismatches(files) {
  const headerVersions = {};
  for (const [shortName, path] of Object.entries(NORMATIVE_FILES)) {
    const entry = Object.entries(files).find(([k]) => k === path || k.endsWith(path));
    if (entry) headerVersions[shortName] = extractVersion(entry[1]);
  }
  const mismatches = [];
  for (const [path, text] of Object.entries(files)) {
    const refs = extractCrossRefs(text);
    for (const r of refs) {
      const expected = headerVersions[r.name];
      if (expected && expected !== r.version) {
        mismatches.push({ from: path, to: r.name, expected, found: r.version });
      }
    }
  }
  return mismatches;
}

function loadFiles(root = process.cwd()) {
  const out = {};
  for (const [, path] of Object.entries(NORMATIVE_FILES)) {
    const abs = join(root, path);
    if (existsSync(abs)) out[path] = readFileSync(abs, 'utf-8');
  }
  return out;
}

if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/cross-ref-checker.mjs')) {
  const files = loadFiles();
  const m = detectMismatches(files);
  if (m.length === 0) {
    console.log(`[cross-ref-checker] OK — 0 drift in ${Object.keys(files).length} files`);
    process.exit(0);
  }
  console.error(`[cross-ref-checker] FAIL — version drift detected:`);
  for (const x of m) {
    console.error(`  ${x.from} references ${x.to} v${x.found}, but ${x.to} header is v${x.expected}`);
  }
  console.error(`Update cross-refs in offending files.`);
  process.exit(1);
}
