#!/usr/bin/env node
/**
 * PreToolUse hook — fact-check absolutes guard.
 *
 * Scans Edit/Write tool inputs on markdown/documentation paths for
 * absolute claims (`always`, `never`, `obviously`, `faster`, `better`,
 * `toujours`, `jamais`, `forcement`) without an accompanying source
 * reference.
 *
 * When an unsourced absolute is detected on a doc file, the hook exits
 * with decision=block and a clear reason, forcing the author to cite a
 * source (matching `_byan/knowledge/sources.md`, `RFC`, `CVE-`, a URL,
 * or a `[CLAIM L<n>]` prefix) before writing.
 *
 * Non-blocking outside of Edit/Write tools or when the target is code
 * (not documentation).
 */

const fs = require('fs');
const path = require('path');

const ABSOLUTES = [
  /\btoujours\b/i,
  /\bjamais\b/i,
  /\bforc[eé]ment\b/i,
  /\bobviously\b/i,
  /\balways\b/i,
  /\bnever\b/i,
  /\bclearly\b/i,
  /\bundoubtedly\b/i,
  /\bfaster than\b/i,
  /\bbetter than\b/i,
  /\bplus rapide que\b/i,
  /\bmeilleur que\b/i,
];

const SOURCE_MARKERS = [
  /\bRFC\s*\d+/i,
  /\bCVE-\d{4}-\d+/i,
  /https?:\/\//,
  /\[CLAIM\s+L[1-5]\]/i,
  /\[FACT\s+USER-VERIFIED/i,
  /\bsource\s*:/i,
  /_byan\/knowledge\/sources\.md/,
];

const DOC_EXTS = ['.md', '.mdx', '.rst', '.txt'];

// Paths exempted from scanning — these files DESCRIBE the rule or meta-docs
// where absolutes appear as examples, not as claims.
const EXEMPT_PATH_PATTERNS = [
  /\.claude\/hooks\//,
  /\.claude\/agents\/bmad-compliance\.md$/,
  /_byan\/mcp\/byan-mcp-server\/lib\/(peer-review|fd-state)\.js$/,
  /_byan\/mcp\/byan-mcp-server\/test\//,
  /_byan\/knowledge\/(fact-check|mantras)/i,
  /\/fact-check-absolutes\.js$/,
  /\.claude\/skills\/byan-fact-check\//,
  /install\/__tests__\/.*fact-check/i,
  /__tests__\/.*fact-check/i,
  /\.claude\/skills\/byan-taste(?:-[\w-]+)?\//,
  /\.claude\/skills\/byan-brandkit\//,
  /\.claude\/skills\/impeccable\//,
];

function isExemptPath(filePath) {
  if (!filePath) return false;
  return EXEMPT_PATH_PATTERNS.some((re) => re.test(filePath));
}

// Strip content that cannot be a claim :
//   - fenced code blocks ``` ... ```
//   - inline backticks `...`
//   - block quotes (lines starting with >)
//   - regex / array syntax that contains the word as a token
function stripNonClaimZones(text) {
  if (!text) return '';
  return text
    // Fenced code blocks
    .replace(/```[\s\S]*?```/g, '')
    // Inline code
    .replace(/`[^`\n]+`/g, '')
    // Markdown block quotes
    .replace(/^> .*$/gm, '')
    // Lines that look like list of patterns (e.g. "- toujours")
    .replace(/^[\s-]*['"]?\b(toujours|jamais|forc[eé]ment|obviously|always|never|clearly|undoubtedly)\b['"]?/gim, '');
}

function readStdin() {
  return new Promise((resolve) => {
    if (process.stdin.isTTY) return resolve('');
    let data = '';
    process.stdin.on('data', (c) => (data += c));
    process.stdin.on('end', () => resolve(data));
    process.stdin.on('error', () => resolve(data));
  });
}

function isDoc(filePath) {
  if (!filePath) return false;
  return DOC_EXTS.some((ext) => filePath.toLowerCase().endsWith(ext));
}

function extractText(toolName, input) {
  if (!input) return '';
  if (toolName === 'Write') return String(input.content || '');
  if (toolName === 'Edit') {
    return [input.new_string, input.old_string].filter(Boolean).join('\n');
  }
  return '';
}

function findUnsourced(text) {
  if (!text) return null;
  for (const re of ABSOLUTES) {
    const match = text.match(re);
    if (!match) continue;
    const idx = match.index || 0;
    const windowStart = Math.max(0, idx - 240);
    const windowEnd = Math.min(text.length, idx + match[0].length + 240);
    const ctx = text.slice(windowStart, windowEnd);
    const hasSource = SOURCE_MARKERS.some((sm) => sm.test(ctx));
    if (!hasSource) {
      return { absolute: match[0], context: text.slice(Math.max(0, idx - 80), idx + 80) };
    }
  }
  return null;
}

(async () => {
  const raw = await readStdin();
  let payload = {};
  try {
    payload = raw ? JSON.parse(raw) : {};
  } catch {
    payload = {};
  }

  const toolName = payload.tool_name || payload.toolName || '';
  const input = payload.tool_input || payload.toolInput || {};
  const target = input.file_path || '';

  if (!['Edit', 'Write'].includes(toolName) || !isDoc(target) || isExemptPath(target)) {
    process.stdout.write(
      JSON.stringify({
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          permissionDecision: 'allow',
        },
      })
    );
    process.exit(0);
  }

  const rawText = extractText(toolName, input);
  const text = stripNonClaimZones(rawText);
  const hit = findUnsourced(text);

  if (!hit) {
    process.stdout.write(
      JSON.stringify({
        hookSpecificOutput: {
          hookEventName: 'PreToolUse',
          permissionDecision: 'allow',
        },
      })
    );
    process.exit(0);
  }

  const reason = [
    `BYAN fact-check guard : unsourced absolute "${hit.absolute}" detected in ${path.basename(target)}.`,
    `Context : ...${hit.context}...`,
    `Add a source (RFC, CVE, URL, [CLAIM L<n>], or entry in _byan/knowledge/sources.md) before writing this. `,
    `Alternative : reformulate with hedging ("often", "in my tests", "tends to") to drop the absolute claim.`,
  ].join('\n');

  process.stdout.write(
    JSON.stringify({
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'deny',
        permissionDecisionReason: reason,
      },
    })
  );
  process.exit(0);
})();
