#!/usr/bin/env node
/**
 * Hook resolver for the brain governance observer.
 * Reverse-lookup .claude/settings.json (+ ~/.claude/settings.json):
 * matcher (event:tool) → list of hook-script names.
 *
 * Pure — no exec, no fs side-effects (Security Guidance #40).
 * Caller is responsible for reading the JSON; this module operates on
 * already-parsed settings objects.
 *
 * Per spec: docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md
 */

import { createHash } from 'node:crypto';

const TOOL_SCRIPT_RE = /(?:^|[\s"'/\\])(tools[\/\\][\w-]+\.(?:mjs|py|sh))/;
const NPX_RE = /(?:^|[\s"'])npx\s+(?:-y\s+)?([\w@/.-]+)/;

/**
 * Normalize a command string for stable hashing:
 * - strip surrounding whitespace
 * - collapse internal whitespace runs to single space
 * No lowercase (script names are case-sensitive in Windows-aware contexts).
 */
function normalizeCommand(s) {
  return String(s || '').trim().replace(/\s+/g, ' ');
}

/**
 * Extract a stable, human-readable identifier from a hook command string.
 * Priority: tools/X.{mjs,py,sh} → npx <pkg> → inline:<sha-16>.
 */
export function extractScriptName(command) {
  const cmd = String(command || '');
  const toolMatch = cmd.match(TOOL_SCRIPT_RE);
  if (toolMatch) return toolMatch[1].replace(/\\/g, '/');
  const npxMatch = cmd.match(NPX_RE);
  if (npxMatch) return npxMatch[1];
  const sha = createHash('sha256').update(normalizeCommand(cmd)).digest('hex').slice(0, 16);
  return `inline:${sha}`;
}

/**
 * Build matcher → [scriptName, ...] from one or two settings objects.
 * Matcher key format:
 *   - "<event>:<tool>" when entry has `matcher` (e.g. "PreToolUse:Bash")
 *   - "<event>" when entry has no `matcher` (UserPromptSubmit, SessionStart)
 *
 * Project settings listed before user settings on shared matchers.
 */
export function buildHookMap(projectSettings = {}, userSettings = {}) {
  const map = new Map();
  for (const settings of [projectSettings, userSettings]) {
    const hooks = settings && settings.hooks;
    if (!hooks || typeof hooks !== 'object') continue;
    for (const [event, entries] of Object.entries(hooks)) {
      if (!Array.isArray(entries)) continue;
      for (const entry of entries) {
        if (!entry || typeof entry !== 'object') continue;
        const scripts = Array.isArray(entry.hooks) ? entry.hooks : [];
        const scriptNames = [];
        for (const h of scripts) {
          if (!h || h.type !== 'command') continue;
          scriptNames.push(extractScriptName(h.command));
        }
        if (scriptNames.length === 0) continue;
        const matcherKeys = entry.matcher
          ? String(entry.matcher).split('|').map((t) => `${event}:${t.trim()}`).filter(Boolean)
          : [event];
        for (const matcher of matcherKeys) {
          const existing = map.get(matcher) || [];
          existing.push(...scriptNames);
          map.set(matcher, existing);
        }
      }
    }
  }
  return map;
}

/**
 * Given matcher counts (from parser hook_fired.counts) and a hook map,
 * return per-script counts. Each script's count = sum over matchers that
 * include it of matcherCounts[matcher]. Matchers not in map are skipped
 * silently (their counts remain reflected in the original `counts` field).
 */
export function resolveScriptCounts(matcherCounts, hookMap) {
  const result = {};
  for (const [matcher, count] of Object.entries(matcherCounts || {})) {
    const scripts = hookMap.get(matcher);
    if (!scripts || scripts.length === 0) continue;
    for (const script of scripts) {
      result[script] = (result[script] || 0) + count;
    }
  }
  return result;
}
