#!/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/sql-write/crypto/concurrency
# sink (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 ; }
# Require explicit base..head notation — reject bare SHAs
[[ "$RANGE" == *..* ]] || { echo "RISK=ERROR"; echo "REASON: range must be base..head format, got: $RANGE"; exit 2; }
[[ "$RANGE" == *...* ]] && { echo "RISK=ERROR"; echo "REASON: range must use '..' not '...' (three-dot changes diff semantics), got: $RANGE"; exit 2; }
stripped="${RANGE/../}"
[[ "$stripped" == *..* ]] && { echo "RISK=ERROR"; echo "REASON: range must contain exactly one '..' separator, got: $RANGE"; 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[[:space:]]|update[[:space:]]|delete[[:space:]]|for update|upsert|\.set\(|truncate|drop[[:space:]]|alter[[:space:]]|migration|\.insert\(|\.create\(|\.createMany\(|\.createOne\(|\.insertMany\(|\.insertOne\(|\.bulkInsert\(|\.bulkCreate\(|\.save\(|\.destroy\(|\.deleteOne\(|\.deleteMany\(|\.remove\(|\.findOneAndDelete\(|\.findOneAndUpdate\(|\.replaceOne\(|\.updateOne\(|\.updateMany\(|\.update\(|\.delete\('
SINK_CRYPTO='crypto|hash|hmac|sign|encrypt|decrypt|secret|nonce|salt|cipher|randombytes|pbkdf2|bcrypt|scrypt'
SINK_CONC='lock|mutex|transaction|begin[[:space:]]|commit[[:space:]]|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 ---
DIFF_OUT=""
DIFF_RC=0
DIFF_OUT="$(git -C "$WT" diff "$RANGE" 2>&1)" || DIFF_RC=$?
if [[ $DIFF_RC -ne 0 ]]; then
  echo "RISK=HIGH"
  echo "REASON: git diff failed (rc=$DIFF_RC) -- failing high per router doctrine: $DIFF_OUT"
  exit 0
fi
CHANGED_LINES="$(printf '%s\n' "$DIFF_OUT" | grep -E '^[+-]' | grep -Ev '^(\+\+\+|---)' || true)"
SINK_HIT=""
SINK_RC=0
SINK_HIT="$(printf '%s\n' "$CHANGED_LINES" | grep -iEn "$SINKS" | head -20)" || SINK_RC=$?
if [[ $SINK_RC -eq 2 ]]; then
  echo "RISK=HIGH"
  echo "REASON: sink scan error (grep rc=$SINK_RC) -- failing high per router doctrine"
  exit 0
fi
# 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=""
MONEY_RC=0
MONEY_HIT="$(printf '%s\n' "$CHANGED_LINES" | grep -iEn "$SINK_MONEY" | head -10)" || MONEY_RC=$?
if [[ $MONEY_RC -eq 2 ]]; then
  echo "RISK=HIGH"
  echo "REASON: money sink scan error (grep rc=$MONEY_RC) -- failing high per router doctrine"
  exit 0
fi

# --- 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=""
CF_RC=0
CHANGED_FILES="$(git -C "$WT" diff --name-only "$RANGE" 2>&1)" || CF_RC=$?
if [[ $CF_RC -ne 0 ]]; then
  echo "RISK=HIGH"
  echo "REASON: git diff --name-only failed (rc=$CF_RC) -- failing high per router doctrine: $CHANGED_FILES"
  exit 0
fi
BINARY_FILES=""
BIN_RC=0
BINARY_FILES="$(git -C "$WT" diff --numstat "$RANGE" 2>/dev/null | awk -F'\t' '$1=="-" && $2=="-" {print $3}')" || BIN_RC=$?
if [[ $BIN_RC -ne 0 ]]; then
  echo "RISK=HIGH"
  echo "REASON: binary file numstat scan error (rc=$BIN_RC) -- failing high per router doctrine"
  exit 0
fi
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
  stem_esc="$(printf '%s' "$stem" | sed 's/[.+*?^${}()|[\]\\]/\\&/g')"
  # files (a) whose path/name is sensitive AND (b) that import this changed stem
  git_grep_out=""
  git_grep_rc=0
  git_grep_out="$(git -C "$WT" grep -lEi "import .*['\"/]${stem_esc}(['\"./]|\$)|require\(['\"].*${stem_esc}" -- '*.ts' '*.tsx' '*.js' '*.mjs' '*.astro' 2>/dev/null)" || git_grep_rc=$?
  if [[ $git_grep_rc -ge 2 ]]; then
    echo "RISK=HIGH"
    echo "REASON: blast-radius scan error (git grep rc=$git_grep_rc on stem '$stem') -- failing high per router doctrine"
    exit 0
  fi
  hits=""
  HITS_RC=0
  hits="$(printf '%s\n' "$git_grep_out" | grep -Ei "$HIRISK_NAME" | grep -v -F "$f" | head -5)" || HITS_RC=$?
  if [[ $HITS_RC -eq 2 ]]; then
    echo "RISK=HIGH"
    echo "REASON: blast-radius name filter error (grep rc=$HITS_RC on stem '$stem') -- failing high per router doctrine"
    exit 0
  fi
  [ -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" ] || [ -n "$TB" ] || [ -n "$BINARY_FILES" ]; then
  echo "RISK=HIGH"
  [ -n "$SINK_HIT" ] && { echo "REASON: sink match in changed lines:"; printf '%s\n' "$SINK_HIT" | sed 's/^/  /'; }
  [ -n "$BINARY_FILES" ] && { echo "REASON: binary file(s) detected -- uninspectable, routing HIGH:"; printf '%s\n' "$BINARY_FILES" | sed 's/^/  /'; }
  [ -n "$BLAST" ] && { echo "REASON: reverse-dep blast radius:"; printf '%b\n' "$BLAST"; }
  [ -n "$TB" ] && {
    [ -z "$SINK_HIT" ] && [ -z "$BLAST" ] && echo "REASON: trust-boundary surface changed (api/route/webhook) — conservative HIGH"
    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