#!/usr/bin/env node
/**
 * on-plan-write (W3, A0 — нах.F5/C-1) — оркестратор записи плана: при (пере)записи
 * плана присваивает persisted first-plan-anchor task-id (✅O17), производит mentor-вердикт
 * (runMentorVerdict, binding plan_hash нах.F4) и best-effort пишет журнал переговоров
 * (SE10: сбой журнала НЕ крашит круг). Чистое ядро — I/O/llmCall/имплы инъектируются;
 * РЕГИСТРАЦИЯ как PostToolUse-хука на запись плана — шаг владельца (вне scope).
 * Контракты инъекций — план C2 W7 (planHash ОБЯЗАТЕЛЕН для freeze-gate F-C4;
 * journalKey — ключ подписи головы цепи).
 */
import { planId } from './plan-lock.mjs';
import { deriveTaskId } from './router-task-id.mjs';
import { runMentorVerdict, runMentorSpecVerdict } from './mentor-verdict.mjs';
import { appendNegotiation, roundCount } from './mentor-journal.mjs';
import { renderSkillContext } from './mentor-seam.mjs';

/**
 * Устойчивый маппинг результата classify() в список рекомендованных скилов. LLM-схема даёт
 * recommended_chain; prefilter/regex-выход несёт поле node (или recommended_node). 'direct' =
 * «скил не нужен» → пустой список. Битый/пустой вход → пустой список (не теряем молча).
 */
export function recommendedChainOf(c) {
  if (!c || typeof c !== 'object') return [];
  if (Array.isArray(c.recommended_chain) && c.recommended_chain.length) return [...c.recommended_chain];
  const node = c.recommended_node ?? c.node ?? null;
  return node && node !== 'direct' ? [node] : [];
}

/**
 * @returns {{taskId, taskIdPersisted, ok, wired, verdict, reason?, journal, journalOk}}
 *  verdict.plan_hash === planId(planSteps) (нах.F4) — caller отдаёт его freeze-gate.
 */
export async function onPlanWrite({
  planSteps = [],
  existingTaskId = null,
  persistTaskIdImpl = null,
  llmCall,
  runMentorVerdictImpl = runMentorVerdict,
  appendNegotiationImpl = appendNegotiation,
  journalEntries = [],
  journalKey = null,
  nowMs = null,
  verifiedContext = [],
  negotiationLog = [],
  graphSection = null,
  classifyImpl = null,
  registry = null,
  declaredSkills = [],
  planGoal = '',
} = {}) {
  const planHash = planId(planSteps);
  // ✅O17: существующий task-id побеждает (re-issue не сбрасывает); первый план — якорь.
  const taskId = deriveTaskId({ existingTaskId, firstPlanHash: planHash });
  let taskIdPersisted = false;
  if (taskId && typeof persistTaskIdImpl === 'function') {
    try { persistTaskIdImpl(taskId); taskIdPersisted = true; } catch { taskIdPersisted = false; }
  }
  // Мерж роутер↔наставник: зовём classify() как функцию (мозг роутера цел). Сбой/недоступен →
  // recommendedChain=null → наставник судит план БЕЗ скил-сверки (fail-safe §5, не ложный NO-GO).
  let recommendedChain = null;
  if (typeof classifyImpl === 'function') {
    try {
      const c = await classifyImpl(planGoal, registry);
      recommendedChain = recommendedChainOf(c);
    } catch { recommendedChain = null; }
  }
  const skillContext = renderSkillContext({ declared: declaredSkills, recommendedChain, registry });
  // Производитель вердикта (C T5b): сбой → ok:false/wired:false (SE-R6-6, не суд).
  const r = await runMentorVerdictImpl({
    plan: { steps: planSteps }, planHash, verifiedContext, negotiationLog, graphSection, skillContext, llmCall,
  });
  // SE10 (A4): журнал — best-effort; throw ловится, круг не падает. Обоснование непустое
  // всегда (F-C2/ДР-6): из вердикта либо из reason сбоя.
  const round = roundCount(journalEntries, taskId) + 1;
  let journal = null;
  let journalOk = false;
  try {
    journal = appendNegotiationImpl(journalEntries, {
      taskId,
      round,
      side: 'mentor',
      utterance: (r.verdict && r.verdict.recommendation) || 'вердикт не произведён',
      justification: (r.verdict && r.verdict.reasoning) || r.reason || 'сбой производителя вердикта',
    }, { key: journalKey, nowMs });
    journalOk = true;
  } catch { journal = null; journalOk = false; }
  return { taskId, taskIdPersisted, ok: r.ok, wired: r.wired, verdict: r.verdict ?? null, reason: r.reason, journal, journalOk, router: { recommendedChain, declared: declaredSkills }, round };
}

/**
 * Фаза 3 (отдельный spec-путь, Р6) — оркестратор записи СПЕКИ. Зеркало onPlanWrite, но
 * вердикт по спеке (runMentorSpecVerdict, binding specHash — хеш артефакта спеки). task-id
 * анкорится specHash (спека первая в стэке спека+план → план переиспользует тот же task-id).
 * Журнал переговоров — best-effort (SE10). I/O/llmCall инъектируются.
 */
export async function onSpecWrite({
  specContent = '',
  specHash = null,
  existingTaskId = null,
  persistTaskIdImpl = null,
  llmCall,
  runMentorSpecVerdictImpl = runMentorSpecVerdict,
  appendNegotiationImpl = appendNegotiation,
  journalEntries = [],
  journalKey = null,
  nowMs = null,
  verifiedContext = [],
  negotiationLog = [],
  graphSection = null,
} = {}) {
  // ✅O17: существующий task-id побеждает; спека анкорит стэк (спека+план) по specHash.
  const taskId = deriveTaskId({ existingTaskId, firstPlanHash: specHash });
  let taskIdPersisted = false;
  if (taskId && typeof persistTaskIdImpl === 'function') {
    try { persistTaskIdImpl(taskId); taskIdPersisted = true; } catch { taskIdPersisted = false; }
  }
  const r = await runMentorSpecVerdictImpl({ specContent, specHash, verifiedContext, negotiationLog, graphSection, llmCall });
  let journal = null;
  let journalOk = false;
  try {
    const round = roundCount(journalEntries, taskId) + 1;
    journal = appendNegotiationImpl(journalEntries, {
      taskId,
      round,
      side: 'mentor',
      utterance: (r.verdict && r.verdict.recommendation) || 'вердикт не произведён',
      justification: (r.verdict && r.verdict.reasoning) || r.reason || 'сбой производителя вердикта',
    }, { key: journalKey, nowMs });
    journalOk = true;
  } catch { journal = null; journalOk = false; }
  return { taskId, taskIdPersisted, ok: r.ok, wired: r.wired, verdict: r.verdict ?? null, reason: r.reason, journal, journalOk };
}
