#!/usr/bin/env node
/**
 * mentor-seam (§6) — живой шов наставника. ч.1 buildMentorPrompt: обёртка buildRouterPrompt
 * + проверенный контекст (из A, данные) + журнал переговоров + граф-секция (выход B, данные).
 * НЕ импортирует A/B — секция/контекст ПЕРЕДАЮТСЯ (автономность C; боевая проводка передаёт
 * реальные). Граф = ОТДЕЛЬНАЯ секция (CD-R6-G); контекст = ПРОВЕРЕННЫЙ артефакт (CD-R6-H).
 * FR-2 (финревью 2026-06-11, снял Д-1а-обход): карту районов+staleness рендерит БАЗА
 * buildRouterPrompt в system (W1-канон, CD-R6-G); seam НЕ дублирует. Extras добавляют
 * их в user ТОЛЬКО при пустом/отсутствующем layer0 — маркер ОТСУТСТВИЯ (F-C6) + staleness (✅O16).
 */
import { buildRouterPrompt } from './router-engine.mjs';

/** A8 (нах.F6): ДР-1 гранулярность — поведенческая инъекция в system.
 *  VA-1 (финревью 3/5): ЕДИНЫЙ источник — экспортируется (mentor-verdict импортирует,
 *  дубль-литерал снят). */
export const DR1_LINE = 'ЕДИНИЦА ПЛАНА (ДР-1): растяжка до следующей НЕИЗВЕСТНОСТИ; громкость церемонии по риску — detectHighRisk (atomic/trivial → тихо; прод/многошаг/разрушительное → громко).';

function renderDistricts(graphSection) {
  const layer0 = graphSection && Array.isArray(graphSection.layer0) ? graphSection.layer0 : [];
  // F-C6 (sharp-edges): слой-0 ВСЕГДА (ДР-3) + «сигнал ВСЕГДА» (§5.4) — отсутствие карты
  // НЕ молчит: явный маркер видят и наставник, и владелец (зеркало staleness-философии).
  if (!layer0.length) return '--- ГРАФ ПРОЕКТА: КАРТА РАЙОНОВ ОТСУТСТВУЕТ (слой-0 не передан/пуст — ДР-3 требует ВСЕГДА) ---';
  const lines = layer0.map((d) => {
    const top = Array.isArray(d.topNodes) ? d.topNodes.map((n) => n.id).join(', ') : '';
    return `${d.district} (${d.nodeCount} узлов)${top ? ` — топ: ${top}` : ''}`;
  });
  return ['--- ГРАФ ПРОЕКТА: КАРТА РАЙОНОВ (layer-0, прунённая) ---', ...lines].join('\n');
}

function renderStaleness(graphSection) {
  const st = graphSection && graphSection.staleness;
  if (!st) return '';
  return `--- СВЕЖЕСТЬ ГРАФА (staleness) --- stale=${!!st.stale} commits_behind=${st.commits_behind ?? '?'} uncommitted=${st.uncommitted ?? '?'}`;
}

/** VA-1 (финревью 3/5): ЕДИНЫЙ рендер контекста — экспортируется (mentor-verdict
 *  импортирует; дубль-рендер с дрейфом снят). VA-2 («сигнал-всегда», зеркало F-C6):
 *  пустой список НЕ молчит — наставник видит, что контекста НЕТ (VA-9 ловит на печати,
 *  а круги переговоров идут до неё). */
export function renderVerifiedContext(vc) {
  const list = Array.isArray(vc) ? vc : [];
  if (!list.length) return '--- ПРОВЕРЕННЫЙ КОНТЕКСТ ПУСТ (0 записей — печать заблокирует VA-9) ---';
  const lines = list.map((e) => `[${e.kind || '?'}] ${e.claim || ''} (${e.ref || ''})`);
  return ['--- ПРОВЕРЕННЫЙ КОНТЕКСТ (резолв цитат, НЕ истина — ревью владельца обязателен) ---', ...lines].join('\n');
}

/** VA-1: единый рендер переговоров (export — см. выше). Пустой лог легитимен
 *  (круг 1 без истории) → '' без маркера. */
export function renderNegotiation(log) {
  const list = Array.isArray(log) ? log : [];
  if (!list.length) return '';
  const lines = list.map((e) => `круг ${e.round} [${e.side}] ${e.utterance} — обоснование: ${e.justification || ''}`);
  return ['--- ПЕРЕГОВОРЫ ЗАДАЧИ (история кругов) ---', ...lines].join('\n');
}

/** Резолв ID узла → «#N — имя» через registry.indexById; нет узла/реестра → голый #N (fail-safe). */
function resolveNodeName(id, registry) {
  const node = registry && registry.indexById && typeof registry.indexById.get === 'function'
    ? registry.indexById.get(id) : null;
  const label = node && (node.name || node.slug);
  return label ? `${id} — ${label}` : String(id);
}

/** Рендер скил-контекста для промпта наставника (мерж): объявленные в плане скилы +
 *  рекомендация роутера (ID резолвятся в имена через registry). recommendedChain=null →
 *  классификатор недоступен (не сверять). */
export function renderSkillContext({ declared = [], recommendedChain = null, registry = null } = {}) {
  const dec = declared.length ? declared.join(', ') : '(не объявлены)';
  const rec = recommendedChain === null
    ? '(рекомендация роутера недоступна — НЕ заворачивай за скилы)'
    : (Array.isArray(recommendedChain) && recommendedChain.length
        ? recommendedChain.map((id) => resolveNodeName(id, registry)).join(', ')
        : '(роутер ничего не порекомендовал)');
  return `--- СКИЛЫ ---\nОбъявлены в плане: ${dec}\nРекомендация роутера: ${rec}\n`
    + 'Оцени уместность выбора скилов; неуместный/неполный выбор → decision="NO-GO" + что добавить/убрать.';
}

/**
 * Построить промпт наставника = buildRouterPrompt(graph=граф-секция B; районы+staleness
 * рендерит БАЗА в system — W1-канон, FR-2 финревью 2026-06-11) + проверенный контекст +
 * журнал переговоров — в user (волатильное); system = router-system + ДР-1 (A8).
 * Extras рендерят районы/staleness ТОЛЬКО при пустом/отсутствующем layer0 (база молчит):
 * маркер ОТСУТСТВИЯ карты (F-C6 «сигнал-всегда») + staleness-строка (✅O16). {system,user}.
 */
export function buildMentorPrompt({ prompt = '', plan = null, graphSection = null, verifiedContext = [], negotiationLog = [], catalog = null, contracts = [] } = {}) {
  const base = buildRouterPrompt({ prompt, plan, graph: graphSection, catalog, contracts });
  const baseRendersGraph = graphSection && Array.isArray(graphSection.layer0) && graphSection.layer0.length > 0;
  const extras = [
    baseRendersGraph ? '' : renderDistricts(graphSection),
    baseRendersGraph ? '' : renderStaleness(graphSection),
    renderVerifiedContext(verifiedContext),
    renderNegotiation(negotiationLog),
  ].filter(Boolean).join('\n');
  return {
    system: [base.system, DR1_LINE].join('\n'),
    user: [base.user, extras].filter(Boolean).join('\n'),
  };
}

// Task 5 — runMentorRound + петля: импорт здесь валиден (ESM hoisting).
import { parseRouterResponse, validateTrace, groundTrace, chosenIsCandidate } from './router-engine.mjs';

/** §6.3 SE-7: потолок кругов наставника per-ЗАДАЧЕ (×MENTOR_PROBE_CAP probe в D). */
export const MENTOR_ROUND_CAP = 3;

/** §5.3 F7 (✅O19-зеркало): дозапросов соседнего района на КРУГ — дешёвое чтение
 *  КАРТЫ (districtDetail из B), не файлов. Сверх cap → отказ. */
export const DISTRICT_PROBE_CAP = 2;

/**
 * Один круг наставника: buildMentorPrompt → llmCall (инъект; живой транспорт — владелец) →
 * пайплайн runRouter (validateTrace/groundTrace/chosenIsCandidate/abstain). wired:true только
 * при реальном завершённом вызове (SE-R6-6: сбой/stub → wired:false, НЕ суд).
 * A2 (SE2): groundingGraph = ПОЛНЫЙ нод-граф (заземление groundTrace, CD-R6-G «против
 * полного»); graphSection = прунённая карта районов B (в промпт). НЕ путать — разные
 * формы, своп ломает заземление.
 * F7 (A6/Н-3): трасса с request_district:<name> → конструктивное воздержание с сигналом
 * escalateDistrict (контроллер фетчит districtDetail(name) из B в graphSection следующего
 * круга); districtProbesUsed ≥ DISTRICT_PROBE_CAP → отказ (бюджет круга исчерпан).
 * F-C5 (sharp-edges, secure default): дефолт districtProbesUsed = DISTRICT_PROBE_CAP —
 * ОМИССИЯ счётчика НЕ даёт безлимитные дозапросы (configuration cliff); реальный
 * per-round счётчик ОБЯЗАН вести производитель (C2-W3), передавая фактическое значение.
 */
export async function runMentorRound({ prompt, plan = null, groundingGraph, graphSection = null, verifiedContext = [], negotiationLog = [], catalog = null, contracts = [], llmCall, confidenceThreshold = 0.5, districtProbesUsed = DISTRICT_PROBE_CAP }) {
  let trace;
  try {
    trace = await llmCall({ buildPrompt: () => buildMentorPrompt({ prompt, plan, graphSection, verifiedContext, negotiationLog, catalog, contracts }) });
  } catch {
    return { ok: false, wired: false, abstain: false, escalateDistrict: null, reason: 'сбой вызова наставника', trace: null };
  }
  if (typeof trace === 'string') trace = parseRouterResponse(trace);
  if (!trace) return { ok: false, wired: false, abstain: false, escalateDistrict: null, reason: 'пустой/неразборный ответ наставника', trace: null };
  const reqDistrict = typeof trace.request_district === 'string' ? trace.request_district.trim() : '';
  if (reqDistrict) {
    if (Number(districtProbesUsed) >= DISTRICT_PROBE_CAP) {
      return { ok: false, wired: true, abstain: false, escalateDistrict: null, reason: `дозапрос района сверх DISTRICT_PROBE_CAP=${DISTRICT_PROBE_CAP} на круг — отказ (F7)`, trace };
    }
    // VA-3 (финревью 3/5): при непустой карте (layer0) имя района проверяется — опечатка/
    // выдумка наставника НЕ эскалируется молча к пустой детали districtDetail. Пустая/
    // отсутствующая карта → валидировать нечем, старое поведение (контроллер разрулит).
    const known = (graphSection && Array.isArray(graphSection.layer0)) ? graphSection.layer0 : [];
    if (known.length && !known.some((d) => d && d.district === reqDistrict)) {
      return { ok: false, wired: true, abstain: false, escalateDistrict: null, reason: `запрошенный район «${reqDistrict}» отсутствует в карте районов — отказ (VA-3)`, trace };
    }
    return { ok: true, wired: true, abstain: true, escalateDistrict: reqDistrict, reason: 'дозапрос соседнего района — конструктивное воздержание (F7, ДР-P1)', trace };
  }
  const v = validateTrace(trace);
  if (!v.ok) return { ok: false, wired: true, abstain: false, escalateDistrict: null, reason: `невалидная трасса: [${v.missingSlots.join(', ')}]`, trace };
  const g = groundTrace(trace, groundingGraph);
  if (!g.grounded) return { ok: false, wired: true, abstain: false, escalateDistrict: null, reason: `выдуманные узлы: [${g.invented.join(', ')}]`, trace };
  if (!chosenIsCandidate(trace)) return { ok: false, wired: true, abstain: false, escalateDistrict: null, reason: 'выбор не среди кандидатов (§6.3)', trace };
  if (trace.confidence < confidenceThreshold) return { ok: true, wired: true, abstain: true, escalateDistrict: null, reason: 'низкая уверенность — воздержание (5.2): переспрос/эскалация', trace };
  return { ok: true, wired: true, abstain: false, escalateDistrict: null, trace };
}

/**
 * Петля кругов (§4/6.4): крутит runRoundImpl пока не согласовано ЛИБО потолок кругов
 * (→ эскалация владельцу, симметрия ДР-6 в ОБА направления). resolved = ok && !abstain.
 * Каждый круг — забота вызывающего записать в mentor-journal (вне чистой петли). nowRound —
 * текущий счётчик кругов задачи (из roundCount). @returns {{resolved, escalate, round, last}}
 */
export async function mentorLoop({ runRoundImpl, nowRound = 0, cap = MENTOR_ROUND_CAP }) {
  let round = nowRound;
  let last = null;
  while (round < cap) {
    round += 1;
    last = await runRoundImpl(round);
    if (last && last.ok && !last.abstain) return { resolved: true, escalate: false, round, last };
  }
  return { resolved: false, escalate: true, round, last, reason: `${cap} круга без согласия → к владельцу (ДР-6)` };
}
