/**
 * PreToolUse(mcp__*) wrapper for tools/mcp-tool-classifier.mjs.
 * Router-gate v4 spec §5.3 + v4.1 G1/G12.
 *
 * Classifier categorises MCP tool calls; default-deny on unknown.
 * 'ask' decision is treated as block (controller must seek explicit approval).
 * Fail-CLOSE on internal error.
 */
import { fileURLToPath } from 'url';
import {
  readStdin,
  parseEventJson,
  exitDecision,
} from './enforce-hook-helpers.mjs';
import { classifyMcpTool } from './mcp-tool-classifier.mjs';
import { scanSecrets } from './secret-scan.mjs';
import { canonicalAction, escapeGrantOpen, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
import { logGuardBlock } from './guard-block-log.mjs';

// 7.3 (Блок 4.3) exfil-схемы в исходящих: base64-данные / локальный файл / IP-литерал URL
// (обход DNS/allowlist). Узко-таргетированы (data:...;base64, и file://) — не ловят обычный текст.
const DATA_URI_RE = /\bdata:[^\s;,]*;base64,/i;
const FILE_URI_RE = /\bfile:\/\//i;
const IP_URL_RE = /\bhttps?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/i;

function serializeArgs(toolInput) {
  try { return JSON.stringify(toolInput); } catch { return String(toolInput); }
}

/**
 * 7.3: скан ИСХОДЯЩЕГО MCP-payload на exfil. Секреты (общий secret-scan) / data:base64 / file:// /
 * IP-литерал URL / раздутый payload → block. fail-close на egress: даже разрешённый классификатором
 * вызов не должен выносить наружу секрет или использовать exfil-схему.
 */
export function scanEgress(toolInput, { maxBytes = 100000 } = {}) {
  const text = serializeArgs(toolInput);
  const sec = scanSecrets(text);
  if (sec.found) return { block: true, reason: `секрет в исходящих аргументах (${sec.hits.join(', ')})` };
  if (DATA_URI_RE.test(text)) return { block: true, reason: 'data:base64 URI в исходящих (exfil-вектор)' };
  if (FILE_URI_RE.test(text)) return { block: true, reason: 'file:// схема в исходящих' };
  if (IP_URL_RE.test(text)) return { block: true, reason: 'IP-литерал URL в исходящих (обход DNS/allowlist)' };
  if (text.length > maxBytes) return { block: true, reason: `раздутый исходящий payload (${text.length} > ${maxBytes} байт)` };
  return { block: false };
}

export function decide({ toolName, toolInput, escapeGrants = [], escapeConsumed = [], now = Date.now() }) {
  const name = String(toolName || '');
  if (!name.startsWith('mcp__')) return { block: false, reason: null };
  const verdict = classifyMcpTool(name, toolInput || {}, {});
  // М6: сквозной аварийный выход владельца — снимает verdict/egress-блок при точном совпадении канон-строки.
  const action = canonicalAction(name, toolInput || {});
  const escaped = () => escapeGrantOpen(action, escapeGrants, escapeConsumed, now);
  if (!verdict) return { block: false, reason: null };
  if (verdict.decision === 'block' || verdict.decision === 'ask') {
    if (escaped()) return { block: false, reason: 'mcp: разрешено аварийным выходом (floor_escape)' };
    // G-5: блок-сообщение несёт точную канон-строку — владелец вставляет её дословно в AskUser как FLOOR-ESCAPE.
    return { block: true, reason: `${verdict.reason || `${name} requires approval (decision=${verdict.decision})`}; FLOOR-ESCAPE: ${action}` };
  }
  // 7.3: классификатор разрешил — но проверяем, что наружу не уходит секрет/exfil-схема/раздутый payload.
  const egress = scanEgress(toolInput || {});
  if (egress.block) {
    if (escaped()) return { block: false, reason: 'egress: разрешено аварийным выходом (floor_escape)' };
    return { block: true, reason: `egress: ${egress.reason}; FLOOR-ESCAPE: ${action}` };
  }
  return { block: false, reason: null };
}

async function main() {
  try {
    const raw = await readStdin();
    const event = parseEventJson(raw);
    const sess = (event && event.session_id) || 'unknown';
    const r = decide({ toolName: event.tool_name, toolInput: event.tool_input,
      escapeGrants: loadFloorEscapes(sess), escapeConsumed: loadConsumed(sess) });
    if (r.block) {
      logGuardBlock(event, 'М5 Egress-страж', r.reason);
      return exitDecision({ block: true, message: `[mcp-classification] ${r.reason}` });
    }
    return exitDecision({ block: false });
  } catch {
    return exitDecision({ block: true, message: '[mcp-classification] внутренняя ошибка — fail-CLOSE' });
  }
}

const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();
