#!/usr/bin/env node
/**
 * judge-seal-channel (K1 / F4 / F5 / F6, §6) — единственная разрешённая запись судьи:
 * ПЕЧАТЬ на ГО. Печать ставит ТОЛЬКО этот канал одобрения гейта (ключ контроллеру
 * недоступен — owner-provisioned keychain); путь «контроллер сам зовёт freeze» не строится.
 *  - F4: печать связывает хеш РОВНО судимого содержания, атомарно с вердиктом — судья
 *    одобрил X, контроллер подменил на X′ → хеш не сойдётся → печать НЕ ставится (пере-суд).
 *  - Carve-out (§6): деньги (осторожный детерминированный детектор) или 3-круговой тупик
 *    → печать ставит ВЛАДЕЛЕЦ, судья сам не печатает.
 *  - F6: возражение, снятое владельцем (вейвер), на нижних воротах не пере-блокируется.
 * Само-freeze делегируется injected freezeImpl (в проде — freezeArtifact Машины 2);
 * persist в runtime + триггер «одобрил→печать» — оркестрация 4-E.
 */
import { createHash } from 'node:crypto';
import { canonicalJson } from './receipt-sign.mjs';

/** Хеш РОВНО содержания (для F4-сверки «то ли судили»). */
export function contentHash(obj) {
  return createHash('sha256').update(canonicalJson(obj)).digest('hex');
}

// Осторожно-широкий список денежных маркеров: при сомнении → деньги → владелец.
// O11: левая Unicode-граница слова (?<![\p{L}]) режет ложь несбалансированный→баланс,
// recharge/surcharge→charge. ПРАВУЮ границу НЕ ставим — русские стем-суффиксы остаются
// (списан→списание, начислен→начисление). Флаг u нужен для \p{L}.
const MONEY_RE = /(?<![\p{L}])(руб|₽|копе|тариф|списан|биллинг|баланс|оплат|плат[её]ж|выручк|начислен|деньг|payment|charge|refund|invoice|money)/iu;

/** Осторожный детерминированный детектор денег (НЕ «на глаз» модели). */
export function detectMoney(input) {
  const text = typeof input === 'string' ? input : canonicalJson(input);
  return MONEY_RE.test(String(text));
}

/** Печать ставит владелец (а не судья), если задеты деньги ИЛИ вердикт в тупике. */
export function requiresOwnerSeal({ artifact, verdict }) {
  if (verdict && verdict.deadlock) return true;
  return detectMoney(artifact);
}

/**
 * Поставить печать на ГО. Возвращает {sealed, seal?} либо {sealed:false, reason|ownerRequired}.
 * @param {object} a
 * @param {object} a.artifact  - содержание, которое печатаем
 * @param {object} a.verdict   - вердикт судьи: {decision:'GO'|'NO-GO', judged_hash, deadlock?, verdict_id}
 * @param {string} a.key       - ключ печати (контроллеру недоступен)
 * @param {number} [a.nowMs]
 * @param {Function} a.freezeImpl - ({artifact,key,nowMs}) => sealed-object. ОБЯЗАТЕЛЕН;
 *   в проде оркестрация 4-E передаёт freezeArtifact Машины 2 (ESM-импортом, не require).
 */
export function sealOnApproval({ artifact, verdict, key, nowMs, freezeImpl }) {
  if (typeof freezeImpl !== 'function') {
    return { sealed: false, reason: 'freezeImpl не передан — печать-движок Машины 2 не подключён' };
  }
  if (!verdict || verdict.decision !== 'GO') {
    return { sealed: false, reason: 'нет GO судьи — печать не ставится' };
  }
  if (requiresOwnerSeal({ artifact, verdict })) {
    return { sealed: false, ownerRequired: true, reason: 'деньги/тупик — печать ставит владелец (carve-out §6)' };
  }
  const currentHash = contentHash(artifact);
  if (verdict.judged_hash !== currentHash) {
    return { sealed: false, reason: 'содержание изменилось после суда (TOCTOU F4) — нужен пере-суд' };
  }
  const seal = freezeImpl({
    artifact: { ...artifact, judged_by: verdict.verdict_id ?? null, judged_hash: currentHash },
    key, nowMs,
  });
  return { sealed: true, seal };
}

/**
 * F6: возражения, снятые владельцем (вейверы по anchor.ref), исключаются из активных
 * на нижних воротах — судья не поднимает заново то, что владелец уже отвёл.
 */
export function applyWaivers(objections = [], waivers = []) {
  const refs = new Set((waivers || []).map((w) => w && w.ref).filter(Boolean));
  const active = [];
  const waived = [];
  for (const o of objections) {
    const ref = o && o.anchor && o.anchor.ref;
    (ref && refs.has(ref) ? waived : active).push(o);
  }
  return { active, waived };
}
