#!/usr/bin/env node
/**
 * node-graph — стабильный граф УЗЛОВ из реестра (3.1/3.2/3.3 + 3.6). Узлы =
 * скилы/MCP/агенты из docs/registry/nodes.yaml; рёбра-ПОДСКАЗКИ (не рецепты):
 * близнецы (общий subcategory) / связи (со-членство в chains) / конфликты
 * (явные attributes.conflicts_with). Резолв скила в узел детерминированный
 * (ОВ-Д2: выдумка → null). Без LLM. Потребляет registry-объект {nodes, chains} —
 * обычно от loadRegistry, но принимает любой совместимый литерал (развязка для
 * тестов/инвариантов).
 */

/** Построить граф из registry ({nodes, chains}). Индексы id/slug/name + субкатегории. */
export function buildNodeGraph(registry) {
  const nodes = (registry && registry.nodes) || [];
  const chains = (registry && registry.chains) || {};
  const byId = new Map(), bySlug = new Map(), byName = new Map();
  const subcategoryIndex = new Map();
  for (const n of nodes) {
    if (n.id) byId.set(String(n.id), n);
    if (n.slug) bySlug.set(String(n.slug).toLowerCase(), n);
    if (n.name) byName.set(String(n.name).toLowerCase(), n);
    if (n.subcategory) {
      if (!subcategoryIndex.has(n.subcategory)) subcategoryIndex.set(n.subcategory, []);
      subcategoryIndex.get(n.subcategory).push(n);
    }
  }
  return { nodes, chains, byId, bySlug, byName, subcategoryIndex };
}

/**
 * ОВ-Д2 — механический резолв ссылки в реальный узел: id (#NN) → slug → name →
 * суффикс после ':' (skill-ref как superpowers:brainstorming) → ПРЕФИКС до ':'
 * как slug/name (фикс-1: суб-скилы superpowers не отдельные узлы → ссылка
 * superpowers:writing-plans заземляется в зонтичный узел superpowers #19).
 * Суффикс пробуется ПЕРВЫМ — если сам суффикс является узлом, он и побеждает
 * (superpowers:architecture-patterns → #38). Не нашли → null (выдумка отклоняется).
 */
export function resolveNode(graph, ref) {
  if (!graph || typeof ref !== 'string') return null;
  const r = ref.trim();
  if (!r) return null;
  if (graph.byId.has(r)) return graph.byId.get(r);
  const low = r.toLowerCase();
  if (graph.bySlug.has(low)) return graph.bySlug.get(low);
  if (graph.byName.has(low)) return graph.byName.get(low);
  if (r.includes(':')) {
    const parts = low.split(':');
    const suf = parts[parts.length - 1];
    if (graph.bySlug.has(suf)) return graph.bySlug.get(suf);
    if (graph.byName.has(suf)) return graph.byName.get(suf);
    const pre = parts[0];
    if (graph.bySlug.has(pre)) return graph.bySlug.get(pre);
    if (graph.byName.has(pre)) return graph.byName.get(pre);
  }
  return null;
}

/** Близнецы — активные узлы той же subcategory (без себя). Роутер ОБЯЗАН сравнить (1.1). */
export function twinsOf(graph, ref) {
  const self = resolveNode(graph, ref);
  if (!self || !self.subcategory) return [];
  return (graph.subcategoryIndex.get(self.subcategory) || [])
    .filter((n) => n !== self && n.status === 'active');
}

/** Связи-подсказки — узлы, со-встречающиеся с этим в любой цепочке (без себя), дедуп. */
export function hintLinksOf(graph, ref) {
  const self = resolveNode(graph, ref);
  if (!self) return [];
  const seen = new Set(), out = [];
  for (const chain of Object.values(graph.chains || {})) {
    const members = (chain.sequence || []).map((s) => resolveNode(graph, s)).filter(Boolean);
    if (!members.includes(self)) continue;
    for (const m of members) {
      if (m === self || seen.has(m.id)) continue;
      seen.add(m.id); out.push(m);
    }
  }
  return out;
}

/** Конфликты — явные attributes.conflicts_with (массив ref'ов), резолвятся в узлы. Free-text не парсим. */
export function conflictsOf(graph, ref) {
  const self = resolveNode(graph, ref);
  if (!self) return [];
  const refs = (self.attributes && self.attributes.conflicts_with) || [];
  return refs.map((r) => resolveNode(graph, r)).filter(Boolean);
}

/**
 * 3.6 — чувство свежести: реестр новее, чем дата сборки графа/каталога → данные
 * могли устареть (роутер предупреждает + пересобирает, не работает уверенно по старью).
 * mtimes инъектируются (чистая функция). Нет builtAt → считаем устаревшим.
 */
export function checkGraphFreshness({ registryMtimeMs, builtAtMs }) {
  if (typeof builtAtMs !== 'number') return { fresh: false, stale: true, reason: 'нет даты сборки графа — пересобрать' };
  if (typeof registryMtimeMs === 'number' && registryMtimeMs > builtAtMs)
    return { fresh: false, stale: true, reason: 'реестр новее графа — данные могли устареть, пересобрать' };
  return { fresh: true, stale: false, reason: 'граф актуален' };
}
