#!/usr/bin/env node
/**
 * coverage-machine — машина охвата A/B/C/D (C-14) поверх контрактов 3-A.
 * НЕЗАВИСИМЫЙ механический верификатор полноты плана: set/graph-операции,
 * без LLM («умный не баран»). Рычаг E дисциплины роутера (§6.3).
 */
export function normToken(s) { return String(s ?? '').trim().toLowerCase(); }

/**
 * Покрытие токенов (F-1, аудит M1-M4): точное равенство ИЛИ множество слов одного
 * ⊆ множеству слов другого (границы слов = не-буквы/цифры). Закрывает дыру «мягкого
 * края C», где двусторонняя подстрока считала produces «a» покрывающим запрос
 * «audit-rls-policy» (ложное «всё покрыто»). Подстрока без границы слова больше не
 * покрывает; «csv» ⊆ «csv-export-deals» (по словам) — да.
 */
export function tokensCover(a, b) {
  const na = normToken(a), nb = normToken(b);
  if (!na || !nb) return false;
  if (na === nb) return true;
  const words = (s) => new Set(s.split(/[^\p{L}\p{N}]+/u).filter(Boolean));
  const wa = words(na), wb = words(nb);
  if (wa.size === 0 || wb.size === 0) return false;
  const subset = (x, y) => [...x].every((t) => y.has(t));
  return subset(wa, wb) || subset(wb, wa);
}

/** D — эффективные нужды контракта = needs + constraints + inherent (#1 присущее по природе). */
export function effectiveNeeds(contract) {
  const needs = (contract.needs || []).map((n) => ({ token: n, kind: 'need' }));
  const cons = (contract.constraints || []).map((cN) => ({ token: cN, kind: 'constraint' }));
  const inh = (contract.inherent || []).map((it) => ({ token: it.need, kind: 'inherent' }));
  return [...needs, ...cons, ...inh];
}

/** Множество всего, что производят контракты (нормализованные produces). */
function producedSet(contracts) {
  const s = new Set();
  for (const c of contracts) for (const p of c.produces || []) s.add(normToken(p));
  return s;
}

/** A — граф зависимостей: ребро producer→consumer via need (по needs↔produces). */
export function buildDependencyGraph(contracts) {
  const byProduce = new Map();
  for (const c of contracts) for (const p of c.produces || []) {
    const k = normToken(p);
    if (!byProduce.has(k)) byProduce.set(k, []);
    byProduce.get(k).push(c.skill);
  }
  const edges = [];
  for (const c of contracts) for (const n of c.needs || []) {
    const producers = byProduce.get(normToken(n)) || [];
    for (const p of producers) if (p !== c.skill) edges.push({ from: p, to: c.skill, via: normToken(n) });
  }
  return { nodes: contracts.map((c) => c.skill), edges };
}

/** A — топосортировка (Kahn). Цикл → {order:null, cycle:[оставшиеся скилы]}. */
export function topoOrder(contracts) {
  const { nodes, edges } = buildDependencyGraph(contracts);
  const indeg = new Map(nodes.map((n) => [n, 0]));
  const adj = new Map(nodes.map((n) => [n, []]));
  for (const e of edges) { indeg.set(e.to, (indeg.get(e.to) || 0) + 1); adj.get(e.from).push(e.to); }
  const queue = nodes.filter((n) => (indeg.get(n) || 0) === 0);
  const order = [];
  while (queue.length) {
    const n = queue.shift(); order.push(n);
    for (const m of adj.get(n) || []) { indeg.set(m, indeg.get(m) - 1); if (indeg.get(m) === 0) queue.push(m); }
  }
  if (order.length !== nodes.length) {
    const cycle = nodes.filter((n) => !order.includes(n));
    return { order: null, cycle };
  }
  return { order, cycle: null };
}

/** A+D — дыры: нужда/ограничение, которую никто не производит и нет в initialInputs. */
export function findHoles(contracts, { initialInputs = [], includeConstraints = true } = {}) {
  const produced = producedSet(contracts);
  const inputs = new Set(initialInputs.map(normToken));
  const holes = [];
  for (const c of contracts) {
    const items = includeConstraints ? effectiveNeeds(c) : (c.needs || []).map((n) => ({ token: n, kind: 'need' }));
    for (const { token, kind } of items) {
      const k = normToken(token);
      if (!produced.has(k) && !inputs.has(k)) holes.push({ need: token, neededBy: c.skill, kind });
    }
  }
  return holes;
}

/** A — связные группы (неориентированные компоненты по рёбрам needs↔produces). */
export function decompositionGroups(contracts) {
  const { nodes, edges } = buildDependencyGraph(contracts);
  const parent = new Map(nodes.map((n) => [n, n]));
  const find = (x) => { while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); } return x; };
  const union = (a, b) => { parent.set(find(a), find(b)); };
  for (const e of edges) union(e.from, e.to);
  const groups = new Map();
  for (const n of nodes) { const r = find(n); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(n); }
  return [...groups.values()];
}

/** Найти контракт, чей produces покрывает запрос (равенство нормализованных ИЛИ подстрока — мягкий край C). */
function coveringSkill(contracts, request) {
  const r = normToken(request);
  if (!r) return null; // F3: пустой/пробельный запрос не «покрывается» (иначе ''.includes('') = ложное всё-покрыто)
  for (const c of contracts) for (const p of c.produces || []) {
    const pp = normToken(p);
    if (!pp) continue;
    if (tokensCover(pp, r)) return c.skill; // F-1: по словам, не по подстроке
  }
  return null;
}

/** C — чек-лист «просьбы цели → план»: каждая просьба сверяется с produces плана. */
export function requestsChecklist(requests, contracts) {
  return (requests || []).map((req) => {
    const coveredBy = coveringSkill(contracts, req);
    return { request: req, coveredBy, ok: coveredBy !== null };
  });
}

/** B — двусторонний реестр: дыры (findHoles) + сироты (produces никому не нужен и не покрывает просьбу). */
export function coverageRegistry(contracts, { requests = [], initialInputs = [] } = {}) {
  const holes = findHoles(contracts, { initialInputs });
  const allNeeds = new Set();
  for (const c of contracts) for (const n of c.needs || []) allNeeds.add(normToken(n));
  const reqTokens = (requests || []).map(normToken).filter(Boolean); // F3: пустые токены не покрывают
  const orphans = [];
  for (const c of contracts) {
    const produces = (c.produces || []).map(normToken);
    const neededBySomeone = produces.some((p) => p && allNeeds.has(p));
    const coversRequest = produces.some((p) => p && reqTokens.some((r) => tokensCover(p, r))); // F-1: по словам, не по подстроке
    if (produces.length > 0 && !neededBySomeone && !coversRequest)
      orphans.push({ skill: c.skill, reason: 'produces никому не нужен и не покрывает просьбу цели (scope creep?)' });
  }
  return { holes, orphans };
}

/**
 * Хребет машины охвата — буквальный чек-лист готовности плана (галочки + указатели §).
 * Объединяет A (дыры/циклы), B (сироты), C (просьбы). ready = все пункты ok.
 */
export function readinessChecklist({ contracts = [], requests = [], initialInputs = [] }) {
  const { holes, orphans } = coverageRegistry(contracts, { requests, initialInputs });
  const topo = topoOrder(contracts);
  const reqList = requestsChecklist(requests, contracts);
  const items = [
    { label: 'Все нужды/ограничения покрыты (нет дыр)', ok: holes.length === 0, pointer: '§A findHoles', detail: holes },
    { label: 'Нет циклов зависимостей', ok: topo.cycle === null, pointer: '§A topoOrder', detail: topo.cycle },
    { label: 'Нет сирот-скоупкрипа', ok: orphans.length === 0, pointer: '§B coverageRegistry', detail: orphans },
    { label: 'Все просьбы цели покрыты', ok: reqList.every((r) => r.ok), pointer: '§C requestsChecklist', detail: reqList.filter((r) => !r.ok) },
  ];
  return { ready: items.every((i) => i.ok), items };
}
