#!/usr/bin/env node
/**
 * guard-block-log — журнал блоков обороны М1–М6 для доски «кто на посту» (D-3).
 * Логгер пишут машинные хуки при РЕШЁННОМ блоке (best-effort, fail-quiet, Node fs —
 * как logVerdictLine/logViolation). Читатели сканируют все session-файлы runtime для
 * глобальной доски (board-генератор не имеет одного session_id). Достоверность журнала —
 * при зарегистрированном поле-страже runtime (Фаза 8); до флипа данных нет (0/0).
 */
import fsDefault from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { canonicalAction } from './escape-grant.mjs';

function defaultRuntimeDir() { return join(homedir(), '.claude', 'runtime'); }

/** Чистая запись блока. */
export function buildGuardBlockEntry({ machine, action, reason, now }) {
  return { ts: now, machine: String(machine ?? ''), action: String(action ?? ''), reason: String(reason ?? '') };
}

/** Best-effort: записать блок машины. action — из canonicalAction(event); sess — из event.session_id.
 *  НИКОГДА не бросает (вызывается в block-ветке хука; сбой логгирования не влияет на блок). */
export function logGuardBlock(event, machine, reason, { fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now() } = {}) {
  try {
    const action = canonicalAction(event && event.tool_name, (event && event.tool_input) || {});
    const sess = (event && event.session_id) || 'unknown';
    const entry = buildGuardBlockEntry({ machine, action, reason, now });
    fsImpl.mkdirSync(runtimeDir, { recursive: true });
    fsImpl.appendFileSync(join(runtimeDir, `guard-blocks-${sess}.jsonl`), JSON.stringify(entry) + '\n');
  } catch { /* fail-quiet */ }
}

function scanSessionFiles(fsImpl, runtimeDir, prefix) {
  let names = [];
  try { names = fsImpl.readdirSync(runtimeDir).filter((f) => f.startsWith(prefix) && f.endsWith('.jsonl')); }
  catch { return []; }
  const out = [];
  for (const name of names) {
    let raw; try { raw = fsImpl.readFileSync(join(runtimeDir, name), 'utf8'); } catch { continue; }
    for (const line of String(raw).split(/\r?\n/)) {
      const t = line.trim(); if (!t) continue;
      let r; try { r = JSON.parse(t); } catch { continue; }
      out.push(r);
    }
  }
  return out;
}

function windowSortCap(recs, { now, windowMs, limit }) {
  return recs
    .filter((r) => r && typeof r.ts === 'number' && now - r.ts >= 0 && now - r.ts <= windowMs)
    .sort((a, b) => b.ts - a.ts)
    .slice(0, limit)
    .map((r) => ({ ...r, ts: new Date(r.ts).toISOString() }));
}

/** Недавние блоки машин для доски. */
export function loadRecentBlocks({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
  const recs = scanSessionFiles(fsImpl, runtimeDir, 'guard-blocks-')
    .map((r) => ({ ts: r.ts, machine: r.machine, action: r.action, reason: r.reason }));
  return windowSortCap(recs, { now, windowMs, limit });
}

/** Недавние escape владельца (floor_escape) для доски. */
export function loadRecentEscapes({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
  const recs = scanSessionFiles(fsImpl, runtimeDir, 'askuser-decisions-')
    .filter((r) => r && r.type === 'floor_escape' && typeof r.action === 'string')
    .map((r) => ({ ts: typeof r.ts === 'number' ? r.ts : 0, machine: 'escape', action: r.action, reason: 'escape владельца' }));
  return windowSortCap(recs, { now, windowMs, limit });
}
