#!/usr/bin/env node
/**
 * enforce-mentor-on-plan-write — активационная обёртка наставника (runbook Этап 1, W3/W7).
 * PostToolUse на запись плана (PLAN_PATH_RE + Write — канон судьи): производит вердикт
 * наставника (onPlanWrite: task-id ✅O17 + binding нах.F4 + журнал ДР-6) и персистит
 * вердикт/журнал/task-id. ПРОИЗВОДИТЕЛЬ, НЕ ГЕЙТ: exit всегда allow — печать блокирует
 * freeze-gate в пути судьи (T6). Рубильник mentorSeamActive: без флага+ключа — $0 no-op.
 */
import { readStdin, parseEventJson, exitDecision, runtimeDir } from './enforce-hook-helpers.mjs';
import { mentorSeamActive, resolveMentorLlmKey } from './mentor-gate-config.mjs';
import { PLAN_PATH_RE, SPEC_PATH_RE } from './enforce-judge-gate.mjs';
import { sealablePlan, sealableArtifact, judgedHashOf } from './seal-orchestration.mjs';
import { planId } from './plan-lock.mjs';
import { onPlanWrite, onSpecWrite } from './on-plan-write.mjs';
import { parseVerifiedContext } from './plan-verified-context.mjs';
// Мерж роутер↔наставник (Р8): наставник зовёт мозг роутера classify() как функцию + грузит
// граф (loadRegistry). parsePlanSkills/extractPlanGoal — объявленные скилы + цель из плана.
import { parsePlanSkills, extractPlanGoal } from './plan-skills.mjs';
import { loadRegistry } from './registry-load.mjs';
import { classify } from './router-classifier.mjs';
import { loadMentorJournal, persistMentorJournal, persistMentorVerdict } from './mentor-journal-store.mjs';
import { loadTaskId, saveTaskId, deriveTaskId } from './router-task-id.mjs';
import { callAnthropicAPI } from './router-classifier.mjs';
import { CLASSIFIER_MODEL, HEAVY_LLM_TIMEOUT_MS } from './router-config.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
import { resolveSessionId } from './enforce-supreme-gate.mjs';
// Волна 7 (двухуровневые переговоры §6): наставник — surface + счётчик + эскалация → карточка.
import { buildArbitrationCard } from './arbitration-card.mjs';
import { formatMentorObjection } from './objection-format.mjs';
import { buildObjectionFeedback, buildDegradedFeedback } from './objection-delivery.mjs';
import { parseNegotiationSection } from './negotiation-section.mjs';
import { bumpMentorNoGo, MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs';
// Способ B (Фаза 2): наставник НЕ печатает — на GO лишь записывает подписанное одобрение
// (mentor-GO, привязка к plan_hash). Печать делает судья (хук ПОСЛЕ наставника) при валидном mentor-GO.
import { buildMentorGo, persistMentorGo } from './mentor-go-store.mjs';

/**
 * Волна 7 (§6): сообщение арбитража при 3 NO-GO наставника — дословное замечание +
 * позиция контроллера (раздел «Переговоры» плана) + 3 выбора + аффорданс. Чистая.
 */
export function buildMentorArbitrationMessage(res, planContent, n) {
  const neg = parseNegotiationSection(planContent);
  const position = neg.length ? neg[neg.length - 1].position : '(позиция не указана в разделе «Переговоры» плана)';
  const card = buildArbitrationCard({
    side: 'mentor', level: 'L1', round: n,
    objectionVerbatim: formatMentorObjection(res) || '(нет текста замечания)',
    controllerPositionVerbatim: position,
  });
  const opts = card.options.map((o) => `• ${o.label}: ${o.whatChanges}`).join('\n');
  return [
    '[mentor] ' + card.title,
    `Замечание наставника:\n${card.objection}`,
    `Позиция контроллера:\n${card.position}`,
    `Что меняет выбор:\n${opts}`,
    'Скажи «объясни подробнее». Решение — через escape/вейвер владельца.',
  ].join('\n\n');
}


/**
 * Фаза 1 (канал замечаний, Р2): чистое решение «что отдать контроллеру» по результату
 * наставника. Только настоящий NO-GO (wired && !ok) → block:true с ПОЛНЫМ текстом замечания
 * (через рабочий exit-2 канал); на 3-м заходе — карточка арбитража. GO/degraded → block:false.
 */
export function decideMentorObjection({ res, planContent, n } = {}) {
  // degraded (наставник не дозвонился, спека §9): block:true с «не смог дозвониться»,
  // одобрения нет (recordMentorGo:false), это НЕ NO-GO (escalation не растёт).
  if (res && res.ran && res.wired === false) {
    return {
      block: true, degraded: true, recordMentorGo: false,
      message: buildDegradedFeedback({ side: 'mentor', reason: res.reason || 'транспорт недоступен' }),
    };
  }
  // Р7/мерж: содержательное «переделай» обязано заворачивать — явная кнопка decision='NO-GO'
  // блокирует наряду со сломанным вердиктом (ok!==true). Раньше блокировал только сломанный →
  // содержательный NO-GO тонул как GO (круг L1 не работал).
  const decision = res && res.verdict && res.verdict.decision;
  const blocked = !!(res && res.wired === true && (decision === 'NO-GO' || res.ok !== true));
  if (!blocked) {
    // GO (wired && ok && decision==='GO'): наставник одобрил ЭТОТ план → записать mentor-GO.
    const recordMentorGo = !!(res && res.wired === true && res.ok === true && decision === 'GO');
    return { block: false, recordMentorGo };
  }
  const message = n >= MENTOR_ESCALATE_AFTER
    ? buildMentorArbitrationMessage(res, String(planContent ?? ''), n)
    : buildObjectionFeedback({ side: 'mentor', text: formatMentorObjection(res) });
  return { block: true, recordMentorGo: false, message };
}

/** Адаптер llmCall (паттерн судьи [enforce-judge-gate.mjs:167-177]): throw НЕ глотаем —
 *  его ловит runMentorVerdict → wired:false (SE-R6-6, не суд). */
export function buildLlmCall({ apiKey, model = CLASSIFIER_MODEL, transport = callAnthropicAPI, perAttemptTimeoutMs = HEAVY_LLM_TIMEOUT_MS }) {
  // perAttemptTimeoutMs: вердикт наставника тяжёлый (~25-32с) — дефолт 30с давал
  // таймаут→degraded. 90с укладывает с большим запасом (systematic-debugging 2026-06-14).
  return async ({ buildPrompt }) => transport(buildPrompt(), { apiKey, model, perAttemptTimeoutMs });
}

/**
 * Чистый производитель: inert → {ran:false}; не план-Write → {ran:false}; план без
 * steps-json → {ran:false, reason} (вердикт НЕ фабрикуется — печать всё равно fail-CLOSE
 * у судьи [enforce-judge-gate.mjs:79]); иначе onPlanWrite + персист. ВСЕ deps инъектируются.
 */
export async function runMentorOnPlanWrite(event, {
  mentorActiveImpl, llmCall, loadJournalImpl, persistJournalImpl, persistVerdictImpl,
  loadTaskIdImpl, persistTaskIdImpl, journalKey, graphSectionImpl, nowMs = null,
  classifyImpl = null, registryImpl = null,
} = {}) {
  if (!mentorActiveImpl()) return { ran: false, reason: 'mentor inert ($0)' };
  const tool = event && event.tool_name;
  const filePath = String((event && event.tool_input && event.tool_input.file_path) || '');
  const content = String((event && event.tool_input && event.tool_input.content) ?? '');
  // Фаза 3 (Р6, отдельный spec-путь): запись СПЕКИ тоже будит наставника. Вердикт по спеке
  // (видит контекст: verified-context + переговоры), binding к хешу артефакта спеки (тот же,
  // чем судья печатает gate1). Один сценарий со спекой и планом — наставник одобряет оба.
  if (tool === 'Write' && SPEC_PATH_RE.test(filePath)) {
    let specHash;
    try { specHash = judgedHashOf(sealableArtifact(content)); } catch { specHash = null; }
    if (!specHash) return { ran: false, reason: 'спека без артефакт-хеша — вердикт не фабрикуется' };
    const journalS = loadJournalImpl();
    const taskIdForPromptS = deriveTaskId({ existingTaskId: loadTaskIdImpl(), firstPlanHash: specHash });
    const negotiationLogS = (journalS.entries || [])
      .map((e) => e && e.payload)
      .filter((p) => p && p.task_id === taskIdForPromptS);
    let graphSectionS = null;
    try { graphSectionS = graphSectionImpl(); } catch { graphSectionS = null; }
    const verifiedContextS = parseVerifiedContext(content);
    const rs = await onSpecWrite({
      specContent: content,
      specHash,
      existingTaskId: loadTaskIdImpl(),
      persistTaskIdImpl,
      llmCall,
      journalEntries: journalS.entries,
      journalKey,
      nowMs,
      verifiedContext: verifiedContextS,
      negotiationLog: negotiationLogS,
      graphSection: graphSectionS,
    });
    try { persistVerdictImpl({ ok: rs.ok, wired: rs.wired, reason: rs.reason ?? null, planHash: specHash, verdict: rs.verdict }); } catch { /* best-effort */ }
    if (rs.journalOk && rs.journal) { try { persistJournalImpl(rs.journal); } catch { /* best-effort (SE10) */ } }
    return { ran: true, ok: rs.ok, wired: rs.wired, reason: rs.reason, taskId: rs.taskId, planHash: specHash, verdict: rs.verdict };
  }
  if (tool !== 'Write' || !PLAN_PATH_RE.test(filePath)) return { ran: false, reason: 'не запись плана/спеки' };
  let steps;
  try { steps = sealablePlan(content).steps; } catch { steps = null; }
  if (!Array.isArray(steps) || steps.length === 0) {
    return { ran: false, reason: 'план без steps-json блока — вердикт не фабрикуется (печать fail-CLOSE у судьи)' };
  }
  const journal0 = loadJournalImpl(); // F-C2-4: загруженная цепь, не []
  // W-3 (sharp-edges 2026-06-12): в промпт — переговоры ТОЛЬКО текущей задачи (тот же
  // deriveTaskId, что внутри onPlanWrite ✅O17: существующий побеждает, иначе якорь плана).
  const taskIdForPrompt = deriveTaskId({ existingTaskId: loadTaskIdImpl(), firstPlanHash: planId(steps) });
  const negotiationLog = (journal0.entries || [])
    .map((e) => e && e.payload)
    .filter((p) => p && p.task_id === taskIdForPrompt);
  try {
    const cn = await import('./controller-negotiation.mjs');
    negotiationLog.push(...cn.controllerNegotiationEntries(content));
  } catch { /* best-effort */ }
  let graphSection = null;
  try { graphSection = graphSectionImpl(); } catch { graphSection = null; } // F-C6: null → маркер ОТСУТСТВИЯ
  const verifiedContext = parseVerifiedContext(content);
  // Мерж (Р8): объявленные в плане скилы + цель → onPlanWrite зовёт classify() (мозг роутера),
  // кладёт «рекомендация vs объявлено» в вердикт. registryImpl?.() — граф/карточки nodes.yaml.
  const declaredSkills = parsePlanSkills(content);
  const planGoal = extractPlanGoal(content);
  let registry = null;
  try { registry = typeof registryImpl === 'function' ? registryImpl() : null; } catch { registry = null; }
  const r = await onPlanWrite({
    planSteps: steps,
    existingTaskId: loadTaskIdImpl(),
    persistTaskIdImpl,
    llmCall,
    journalEntries: journal0.entries,
    journalKey,
    nowMs,
    verifiedContext,
    negotiationLog,
    graphSection,
    classifyImpl,
    registry,
    declaredSkills,
    planGoal,
  });
  const planHash = planId(steps);
  try { persistVerdictImpl({ ok: r.ok, wired: r.wired, reason: r.reason ?? null, planHash, verdict: r.verdict }); } catch { /* best-effort */ }
  if (r.journalOk && r.journal) { try { persistJournalImpl(r.journal); } catch { /* best-effort (SE10) */ } }
  return { ran: true, ok: r.ok, wired: r.wired, reason: r.reason, taskId: r.taskId, planHash, verdict: r.verdict, router: r.router, round: r.round };
}

async function main() {
  try {
    const event = parseEventJson(await readStdin());
    const fs = (await import('node:fs')).default;
    const dir = runtimeDir();
    const sess = resolveSessionId(event);
    const res = await runMentorOnPlanWrite(event, {
      mentorActiveImpl: () => mentorSeamActive(),
      // «Оба строго» (2026-06-12): СВОЙ ключ наставника, общий ROUTER_LLM_KEY не фолбэк.
      llmCall: buildLlmCall({ apiKey: resolveMentorLlmKey() }),
      loadJournalImpl: () => loadMentorJournal({ sessionId: sess, runtimeDir: dir }),
      persistJournalImpl: (j) => persistMentorJournal({ journal: j, sessionId: sess, runtimeDir: dir }),
      persistVerdictImpl: (rec) => persistMentorVerdict({ record: rec, sessionId: sess, runtimeDir: dir }),
      loadTaskIdImpl: () => loadTaskId({ sessionId: sess, runtimeDir: dir, fsImpl: fs }),
      persistTaskIdImpl: (id) => saveTaskId({ taskId: id, sessionId: sess, runtimeDir: dir, fsImpl: fs }),
      journalKey: resolveReceiptKey(),
      // Боевой граф B — следующий шаг после обкатки (runbook-нота T7): null → промпт
      // наставника несёт явный маркер «КАРТА РАЙОНОВ ОТСУТСТВУЕТ» (F-C6, не тихо).
      graphSectionImpl: () => null,
      // Мерж (Р8): мозг роутера classify() как функция (3 слоя + граф/карточки nodes.yaml,
      // код classify/registry-load НЕ тронут — новый вызыватель). classify сам берёт свой
      // ключ/транспорт; сбой ловит onPlanWrite → fail-safe (план без скил-сверки, §5).
      classifyImpl: async (goal, registry) => classify(goal, registry, { skipPrefilter: true }),
      registryImpl: () => { try { return loadRegistry({ useCache: false }); } catch { return null; } },
    });
    if (res && res.ran) {
      // Р7/§3.4: счётчик L1 растёт на NO-GO = содержательный decision='NO-GO' ИЛИ сломанный
      // вердикт (ok!==true). degraded (wired:false) не считается (escalation L1 не растёт).
      const verdictDecision = res.verdict && res.verdict.decision;
      const blocked = res.wired === true && (verdictDecision === 'NO-GO' || res.ok !== true);
      // Фаза 4: счётчик на СТЭК (спека+план) одной задачи — ключ task-id (sess — fallback).
      const n = bumpMentorNoGo({ taskId: res.taskId, sessionId: sess, blocked });
      // Фаза 1 (Р2): на NO-GO/degraded — ПОЛНЫЙ текст доходит до контроллера через рабочий
      // exit-2 канал (подтверждён Фазой 0). На 3-м NO-GO — карточка арбитража.
      const planContent = String((event.tool_input && event.tool_input.content) ?? '');
      const decision = decideMentorObjection({ res, planContent, n });
      try {
        const m1 = await import('./verdict-surface-store.mjs');
        const det = await import('./verdict-surface-detail.mjs');
        const kindObj = decision.degraded ? { kind: 'degraded', reason: res.reason || 'транспорт недоступен' }
          : decision.block ? { kind: 'nogo', text: formatMentorObjection(res) }
          : decision.recordMentorGo ? { kind: 'go' }
          : { kind: 'skip', reason: res.reason || 'не судится' };
        const routerLine = det.buildRouterSurface(res.router, { round: res.round });
        const mentorLine = det.buildMentorSurface(kindObj, { round: res.round, verdict: res.verdict });
        for (const line of [routerLine, mentorLine]) {
          if (line) m1.appendVerdictOutcome({ sessionId: sess, line, nowMs: Date.now() });
        }
      } catch { /* best-effort */ }
      // Способ B (Task 2.2): наставник НЕ печатает. На GO — записывает подписанное одобрение
      // (mentor-GO, binding plan_hash); печать сделает судья (хук ПОСЛЕ) при валидном mentor-GO.
      if (decision.recordMentorGo && res.planHash) {
        try {
          persistMentorGo({
            record: buildMentorGo({ planHash: res.planHash, key: resolveReceiptKey() }),
            sessionId: sess, runtimeDir: dir,
          });
        } catch { /* best-effort: нет записи → судья просто не запечатает (fail-safe) */ }
      }
      if (decision.block) {
        exitDecision(decision); // exit 2 со stderr-сообщением (замечание/degraded)
        return;
      }
    }
  } catch { /* производитель никогда не блокирует */ }
  exitDecision({ block: false });
}

import { fileURLToPath } from 'node:url';
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();
