#!/usr/bin/env node
/**
 * C5 observer-coverage-checker (brain governance, observer factor-analysis
 * spec §5.2). Warn-only — always exits 0. Two checks:
 *   1. Coverage — Stop-hook is registered but 0 episodes this month.
 *      Comparing episodes against commit volume is wrong-unit (commits =
 *      work-unit, episodes = turn-unit) and wrong-window (the C5 window
 *      can predate the hook's registration); a freshly-registered hook
 *      vs. 1000 historical commits would flap forever. Driven by hook
 *      registration instead — the only honest expectation source.
 *   2. Registration integrity — observer Stop-hook present in
 *      .claude/settings.json and .git/hooks/post-commit installed.
 * Findings are surfaced in docs/observer/STATUS.md (C4 generator); this
 * controller never blocks a commit.
 *
 * Security Guidance #40: pure fs — no exec/execSync.
 */
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { detectMissedActivations } from './missed-activations.mjs';
import { dedupeEpisodes } from './brain-retro-analyzer.mjs';
import { loadRegistry } from './registry-load.mjs';
import { buildClassificationMap, buildDormancyMap } from './registry-to-classification-map.mjs';

/**
 * @param {number} episodeCount - episodes in the current month JSONL
 * @param {boolean} hookRegistered - whether observer-stop-hook is wired in settings
 * @returns {{ok: boolean, detail: string}}
 */
export function checkCoverage(episodeCount, hookRegistered) {
  if (hookRegistered && episodeCount === 0) {
    return {
      ok: false,
      detail: `Stop-hook registered but 0 episode(s) recorded this month — hook may be silently failing`,
    };
  }
  return { ok: true, detail: `${episodeCount} episode(s) this month` };
}

/** @returns {{ok: boolean, detail: string}} */
export function checkRegistration(settingsJson, postCommitExists) {
  const problems = [];
  const stopHooks = (((settingsJson || {}).hooks || {}).Stop) || [];
  const hasObserverStop = stopHooks.some((entry) =>
    ((entry && entry.hooks) || []).some((h) => String((h && h.command) || '').includes('observer-stop-hook'))
  );
  if (!hasObserverStop) {
    problems.push('observer-stop-hook NOT registered in .claude/settings.json Stop hook');
  }
  if (!postCommitExists) {
    problems.push('.git/hooks/post-commit not installed (run: npx lefthook install --force)');
  }
  return {
    ok: problems.length === 0,
    detail: problems.length ? problems.join('; ') : 'Stop-hook + post-commit OK',
  };
}

function countEpisodes(root) {
  const month = new Date().toISOString().slice(0, 7);
  const file = join(root, 'docs', 'observer', `episodes-${month}.jsonl`);
  if (!existsSync(file)) return 0;
  return readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean).length;
}

function loadEpisodes(root) {
  const month = new Date().toISOString().slice(0, 7);
  const file = join(root, 'docs', 'observer', `episodes-${month}.jsonl`);
  if (!existsSync(file)) return [];
  const out = [];
  for (const line of readFileSync(file, 'utf-8').split('\n')) {
    const t = line.trim();
    if (!t) continue;
    try { out.push(JSON.parse(t)); } catch { /* skip */ }
  }
  return out;
}

function loadClassificationMap(root) {
  try {
    const registry = loadRegistry({
      registryPath: join(root, 'docs', 'registry', 'nodes.yaml'),
      schemaPath: join(root, 'docs', 'registry', 'schema.json'),
      useCache: false,
    });
    return buildClassificationMap(registry);
  } catch { return {}; }
}

function loadDormancy(root) {
  try {
    const registry = loadRegistry({
      registryPath: join(root, 'docs', 'registry', 'nodes.yaml'),
      schemaPath: join(root, 'docs', 'registry', 'schema.json'),
      useCache: false,
    });
    return buildDormancyMap(registry);
  } catch { return {}; }
}

function readSettings(root) {
  try {
    return JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8'));
  } catch {
    return {};
  }
}

function isObserverStopRegistered(settings) {
  const stopHooks = (((settings || {}).hooks || {}).Stop) || [];
  return stopHooks.some((entry) =>
    ((entry && entry.hooks) || []).some((h) =>
      String((h && h.command) || '').includes('observer-stop-hook')
    )
  );
}

export function runCoverageChecker(root = process.cwd()) {
  const settings = readSettings(root);
  const hookRegistered = isObserverStopRegistered(settings);
  const coverage = checkCoverage(countEpisodes(root), hookRegistered);
  const registration = checkRegistration(settings, existsSync(join(root, '.git', 'hooks', 'post-commit')));
  const episodes = loadEpisodes(root).filter((e) => e && e.schema_version === 2 && !e.observer_error);
  const missed = detectMissedActivations(
    dedupeEpisodes(episodes),
    loadClassificationMap(root),
    loadDormancy(root)
  );
  return { coverage, registration, missed };
}

if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/observer-coverage-checker.mjs')) {
  const { coverage, registration, missed } = runCoverageChecker();
  if (!coverage.ok) console.warn(`[observer-coverage-checker] WARN — coverage: ${coverage.detail}`);
  if (!registration.ok) console.warn(`[observer-coverage-checker] WARN — registration: ${registration.detail}`);
  if (missed.totalMissed > 0) {
    console.warn(`[observer-coverage-checker] WARN — missed activations: ${missed.totalMissed} (see /brain-retro)`);
  }
  if (coverage.ok && registration.ok && missed.totalMissed === 0) {
    console.log(`[observer-coverage-checker] OK — ${coverage.detail}; ${registration.detail}`);
  }
  process.exit(0); // warn-only — never blocks a commit
}
