#!/usr/bin/env node
/**
 * mentor-verdict (§6.1) — вердикт наставника со СВОИМИ слотами (НЕ судейские
 * validateVerdictSlots/линзы — SE-R7-8). Паттерн-зеркало validateTrace, но собственный
 * набор. substance (SE-R6-6): содержателен = валиден ПО слотам И wired:true (реальный
 * вызов, не stub/degraded). Зеркало: судья {decision:'GO', wired:false} не суд.
 */

/** СОБСТВЕННЫЕ слоты наставника (адресуют план по-пунктам). decision (Р7/мерж) — явная
 *  кнопка GO/NO-GO: содержательное «переделай» обязано заворачивать, не тонуть как GO. */
export const MENTOR_VERDICT_SLOTS = Object.freeze(['plan_points_addressed', 'reasoning', 'recommendation', 'confidence', 'decision']);

/** Валидация вердикта по СВОИМ слотам. plan_points_addressed — непустой массив;
 *  reasoning/recommendation — непустые строки; confidence — конечное число ∈[0,1]
 *  (зеркало F-8: NaN/Infinity/вне диапазона — не «уверен»). */
export function validateMentorVerdict(verdict) {
  const missingSlots = [];
  if (!verdict || typeof verdict !== 'object') return { ok: false, missingSlots: [...MENTOR_VERDICT_SLOTS] };
  // F-C3 (sharp-edges): пункты обязаны быть НЕПУСТЫМИ СТРОКАМИ — [''] / [null] / [{}]
  // формально «непустой массив», но субстанции нет (R2-VA-meta presence ≠ substance).
  if (!Array.isArray(verdict.plan_points_addressed) || verdict.plan_points_addressed.length === 0
      || !verdict.plan_points_addressed.every((p) => typeof p === 'string' && p.trim())) missingSlots.push('plan_points_addressed');
  for (const slot of ['reasoning', 'recommendation']) {
    if (typeof verdict[slot] !== 'string' || !verdict[slot].trim()) missingSlots.push(slot);
  }
  if (typeof verdict.confidence !== 'number' || !Number.isFinite(verdict.confidence) || verdict.confidence < 0 || verdict.confidence > 1) missingSlots.push('confidence');
  // decision (Р7/мерж): явная кнопка GO/NO-GO — только {GO, NO-GO}; иначе вердикт несодержателен.
  if (verdict.decision !== 'GO' && verdict.decision !== 'NO-GO') missingSlots.push('decision');
  return { ok: missingSlots.length === 0, missingSlots };
}

/** Содержателен ли вердикт: валиден ПО слотам И из РЕАЛЬНОГО захода (wired:true). */
export function isMentorVerdictSubstantive(verdict, { wired = false } = {}) {
  return wired === true && validateMentorVerdict(verdict).ok;
}

// Task 3b — производитель вердикта (C-1): импорт здесь валиден (ESM hoisting).
// VA-1 (финревью 3/5): рендеры контекста/переговоров + ДР-1 строка — ЕДИНЫЙ источник
// mentor-seam (дубль-литералы с дрейфом сняты; циклов нет — seam verdict не импортирует).
import { parseRouterResponse } from './router-engine.mjs';
import { DR1_LINE, renderVerifiedContext, renderNegotiation } from './mentor-seam.mjs';

/**
 * Промпт-производитель вердикта (§6.1): зеркало идиомы buildRouterPrompt {system,user},
 * но system просит ИМЕННО verdict-слоты (plan_points_addressed по-пунктам / reasoning /
 * recommendation / confidence), НЕ router-слоты. user — план по-пунктам + проверенный
 * контекст + журнал переговоров (единые рендеры seam, VA-1; пустой контекст НЕ молчит,
 * VA-2) + граф-секция (опора наставника). A8 (нах.F6): system несёт ДР-1 гранулярность.
 */
export function buildMentorVerdictPrompt({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, skillContext = null } = {}) {
  const system = [
    'Ты — НАСТАВНИК. Разбери ПЛАН ПО ПУНКТАМ (не выбирай скил — это другой вызов).',
    DR1_LINE,
    // Smoke 2026-06-12: «статус+замечание» без типа элемента провоцировал массив объектов —
    // валидатор (F-C3/М1-М4: слот = строки) браковал содержательный вердикт. Тип — явно.
    'Переоцени ТЕКУЩИЙ план/спеку: сначала проверь, сняты ли твои ПРОШЛЫЕ претензии из журнала переговоров; снятое НЕ повторяй; учти доводы контроллера; новое возражение — только если оно реально в текущем тексте.',
    'Вынеси РЕШЕНИЕ: decision="GO" если можно реализовывать, decision="NO-GO" если нужна переделка (тогда в recommendation — ЧТО править).',
    'ВЫВОД — строго JSON-вердикт со слотами: plan_points_addressed (массив СТРОК — по КАЖДОМУ пункту плана ровно одна строка вида «пункт N: статус — замечание»; элемент-объект недопустим, не объект а строка), reasoning (строка-разбор), recommendation (строка — что править), confidence (число 0..1), decision ("GO"|"NO-GO"). Пустой слот недопустим.',
  ].join('\n');
  const user = [
    `ПЛАН: ${plan ? JSON.stringify(plan) : '(нет)'}`,
    renderVerifiedContext(verifiedContext),
    renderNegotiation(negotiationLog),
    graphSection ? `--- ГРАФ (карта районов) ---\n${JSON.stringify(graphSection.layer0 || [])}` : '',
    skillContext ? skillContext : '',
  ].filter(Boolean).join('\n');
  return { system, user };
}

/**
 * Производитель вердикта (§6.1, зеркало runRouter [router-engine.mjs:140]): промпт →
 * llmCall (инъект; живой транспорт — активация владельца) → parse → validateMentorVerdict →
 * при валидном wired:true + verdict с plan_hash (binding нах.F4: freeze-gate сверит
 * verdict.plan_hash === planId(steps) — stale/чужой вердикт не пройдёт). Сбой → wired:false
 * (SE-R6-6: не суд).
 */
export async function runMentorVerdict({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, planHash = null, skillContext = null, llmCall }) {
  let v;
  try {
    v = await llmCall({ buildPrompt: () => buildMentorVerdictPrompt({ plan, verifiedContext, negotiationLog, graphSection, skillContext }) });
  } catch (e) {
    // Smoke 2026-06-12: тихий catch не давал отличить 401 (ключ) от сети — деталь
    // обязана доехать до вердикт-файла/журнала (усечённая; ключ в message не попадает).
    const detail = String((e && e.message) || e).slice(0, 200);
    return { ok: false, wired: false, reason: `сбой вызова наставника-вердикта: ${detail}`, verdict: null };
  }
  if (typeof v === 'string') v = parseRouterResponse(v);
  if (!v) return { ok: false, wired: false, reason: 'пустой/неразборный вердикт', verdict: null };
  const chk = validateMentorVerdict(v);
  if (!chk.ok) return { ok: false, wired: true, reason: `несодержательный вердикт: пустые слоты [${chk.missingSlots.join(', ')}]`, verdict: v };
  return { ok: true, wired: true, verdict: { ...v, plan_hash: planHash } };
}

/**
 * Фаза 3 (отдельный spec-путь, Р6): промпт-производитель вердикта наставника по СПЕКЕ.
 * Зеркало buildMentorVerdictPrompt, но просит разбор СПЕКИ по разделам (не плана по пунктам).
 * Наставник ВИДИТ контекст (verified-context + переговоры — Р6), судья — нет. Те же слоты.
 */
export function buildMentorSpecVerdictPrompt({ specContent = '', verifiedContext = [], negotiationLog = [], graphSection = null } = {}) {
  const system = [
    'Ты — НАСТАВНИК. Разбери СПЕКУ ПО РАЗДЕЛАМ (это спецификация решения, не план; скил не выбираешь).',
    DR1_LINE,
    'Переоцени ТЕКУЩИЙ план/спеку: сначала проверь, сняты ли твои ПРОШЛЫЕ претензии из журнала переговоров; снятое НЕ повторяй; учти доводы контроллера; новое возражение — только если оно реально в текущем тексте.',
    'Вынеси РЕШЕНИЕ: decision="GO" если спеку можно принять, decision="NO-GO" если нужна переделка (тогда в recommendation — ЧТО править).',
    'ВЫВОД — строго JSON-вердикт со слотами: plan_points_addressed (массив СТРОК — по КАЖДОМУ разделу/решению спеки ровно одна строка вида «раздел X: статус — замечание»; элемент-объект недопустим, не объект а строка), reasoning (строка-разбор), recommendation (строка — что править), confidence (число 0..1), decision ("GO"|"NO-GO"). Пустой слот недопустим.',
  ].join('\n');
  const user = [
    `СПЕКА:\n${specContent || '(нет)'}`,
    renderVerifiedContext(verifiedContext),
    renderNegotiation(negotiationLog),
    graphSection ? `--- ГРАФ (карта районов) ---\n${JSON.stringify(graphSection.layer0 || [])}` : '',
  ].filter(Boolean).join('\n');
  return { system, user };
}

/**
 * Фаза 3 (Р6): производитель вердикта наставника по СПЕКЕ. Зеркало runMentorVerdict, но
 * spec-промпт; binding plan_hash = specHash (хеш артефакта спеки — judgedHashOf(sealableArtifact),
 * тот же, чем судья печатает gate1). Сбой → wired:false (SE-R6-6, не суд).
 */
export async function runMentorSpecVerdict({ specContent = '', specHash = null, verifiedContext = [], negotiationLog = [], graphSection = null, llmCall }) {
  let v;
  try {
    v = await llmCall({ buildPrompt: () => buildMentorSpecVerdictPrompt({ specContent, verifiedContext, negotiationLog, graphSection }) });
  } catch (e) {
    const detail = String((e && e.message) || e).slice(0, 200);
    return { ok: false, wired: false, reason: `сбой вызова наставника-вердикта (спека): ${detail}`, verdict: null };
  }
  if (typeof v === 'string') v = parseRouterResponse(v);
  if (!v) return { ok: false, wired: false, reason: 'пустой/неразборный вердикт', verdict: null };
  const chk = validateMentorVerdict(v);
  if (!chk.ok) return { ok: false, wired: true, reason: `несодержательный вердикт: пустые слоты [${chk.missingSlots.join(', ')}]`, verdict: v };
  return { ok: true, wired: true, verdict: { ...v, plan_hash: specHash } };
}
