#!/usr/bin/env bash
# risk-router.sh — deterministic high/low-risk classifier for a wave diff.
#
# The ONE decision that gates opus spend. Kept deterministic so a cheap (sonnet)
# orchestrator cannot mis-route it. CONSERVATIVE BY DESIGN: a false-positive costs
# one extra opus pass (cheap); a false-negative ships a money/auth bug (catastrophic).
# Always err HIGH.
#
# Classifies HIGH when ANY of:
#   1. Added OR REMOVED diff lines match a money/auth/sql-write/crypto/concurrency sink
#      (removed lines matter: deleting a guard carries no sink keyword on the +side).
#   2. A changed file is reverse-imported by a money/auth-named file (blast radius:
#      a benign-looking edit to a shared helper a payment path consumes is HIGH).
# Emits a TRUST_BOUNDARY flag when a HIGH diff also touches an api/route/webhook
# surface → caller adds security-guard alongside opus.
#
# Usage: risk-router.sh <worktree_path> <base_sha>..<head_sha>
# Output (stdout): RISK=HIGH|LOW, then REASON / TRUST_BOUNDARY lines. Exit 0 always
# unless args/range are invalid (exit 2) — an erroring router must NOT read as LOW.

set -euo pipefail

WT="${1:?usage: risk-router.sh <worktree_path> <base..head>}"
RANGE="${2:?usage: risk-router.sh <worktree_path> <base..head>}"

# Accept both regular repos (.git dir) and git worktrees (.git file)
[ -d "$WT/.git" ] || [ -f "$WT/.git" ] || { echo "RISK=ERROR" ; echo "REASON: not a git repo/worktree: $WT" ; exit 2 ; }
git -C "$WT" rev-parse "${RANGE%%..*}" >/dev/null 2>&1 || { echo "RISK=ERROR"; echo "REASON: bad base in range: $RANGE"; exit 2; }
git -C "$WT" rev-parse "${RANGE##*..}" >/dev/null 2>&1 || { echo "RISK=ERROR"; echo "REASON: bad head in range: $RANGE"; exit 2; }

# --- sink lexicon (case-insensitive). Conservative; extend, never trim silently. ---
SINK_MONEY='pay(ment|out|er)?|refund|charge|invoice|ledger|balance|price|amount|stripe|checkout|wallet|credit|debit|billing|subscription|coupon|discount|tax|payout'
SINK_AUTH='auth|session|token|jwt|passwd|password|permission|role|owner|tenant|acl|login|signup|verify|2fa|otp|cookie|bearer|claim|scope|grant|revoke'
SINK_SQLW='insert\s|update\s|delete\s|for update|upsert|\.set\(|truncate|drop\s|alter\s|migration'
SINK_CRYPTO='crypto|hash|hmac|sign|encrypt|decrypt|secret|nonce|salt|cipher|randombytes|pbkdf2|bcrypt|scrypt'
SINK_CONC='lock|mutex|transaction|begin\s|commit\s|atomic|race|semaphore|serializable|advisory_lock'
SINKS="(${SINK_MONEY}|${SINK_AUTH}|${SINK_SQLW}|${SINK_CRYPTO}|${SINK_CONC})"

# --- 1. sink scan over CHANGED lines (+ and -), excluding the diff file headers ---
CHANGED_LINES="$(git -C "$WT" diff "$RANGE" | grep -E '^[+-]' | grep -Ev '^(\+\+\+|---)' || true)"
SINK_HIT="$(printf '%s\n' "$CHANGED_LINES" | grep -iEn "$SINKS" | head -20 || true)"
# money-specific sub-scan: drives the dedicated finance money-logic gate in run-plan (gate.py --detector finance).
# Separate from the HIGH verdict so the caller spends the measured money detector ONLY on money-touching diffs.
MONEY_HIT="$(printf '%s\n' "$CHANGED_LINES" | grep -iEn "$SINK_MONEY" | head -10 || true)"

# --- 2. reverse-dependency blast radius ---
# Changed files → their import basenames. If any money/auth-NAMED file in the repo
# imports a changed file, the change reaches a sensitive path → HIGH.
CHANGED_FILES="$(git -C "$WT" diff --name-only "$RANGE" || true)"
BLAST=""
HIRISK_NAME='(pay|payment|payout|refund|charge|invoice|ledger|billing|wallet|checkout|auth|session|login|permission|role|tenant|owner|webhook)'
while IFS= read -r f; do
  [ -n "$f" ] || continue
  base="$(basename "$f")"; stem="${base%.*}"
  [ -n "$stem" ] || continue
  # files (a) whose path/name is sensitive AND (b) that import this changed stem
  hits="$(git -C "$WT" grep -lEi "import .*['\"/]${stem}(['\"./]|\$)|require\(['\"].*${stem}" -- '*.ts' '*.tsx' '*.js' '*.mjs' '*.astro' 2>/dev/null \
            | grep -Ei "$HIRISK_NAME" | grep -v -F "$f" | head -5 || true)"
  [ -n "$hits" ] && BLAST="${BLAST}\n  ${f} <- imported by: $(printf '%s' "$hits" | tr '\n' ' ')"
done <<< "$CHANGED_FILES"

# --- 3. trust-boundary surface (drives +security-guard, only meaningful when HIGH) ---
TB="$(printf '%s\n' "$CHANGED_FILES" | grep -Ei '(^|/)(api|routes?|pages/api|functions|webhooks?)/|\.(route|handler|endpoint|webhook)\.' | head -10 || true)"

# --- verdict ---
if [ -n "$SINK_HIT" ] || [ -n "$BLAST" ]; then
  echo "RISK=HIGH"
  [ -n "$SINK_HIT" ] && { echo "REASON: sink match in changed lines:"; printf '%s\n' "$SINK_HIT" | sed 's/^/  /'; }
  [ -n "$BLAST" ] && { echo "REASON: reverse-dep blast radius:"; printf '%b\n' "$BLAST"; }
  [ -n "$TB" ] && { echo "TRUST_BOUNDARY: yes — add security-guard:"; printf '%s\n' "$TB" | sed 's/^/  /'; }
  [ -n "$MONEY_HIT" ] && { echo "MONEY: yes — run finance money-logic gate:"; printf '%s\n' "$MONEY_HIT" | sed 's/^/  /'; }
else
  echo "RISK=LOW"
  echo "REASON: no money/auth/sql-write/crypto/concurrency sink in changed lines; no sensitive reverse-dep."
fi
exit 0
