#!/usr/bin/env bash
# Global pre-commit hook
# Runs checks before allowing a commit
#
# Scans the staged DIFF (added lines only), not whole files, so that
# pre-existing content in modified files does not trigger false positives.

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

echo "Running pre-commit checks..."

# Collect added lines from the staged diff, once, for subsequent content checks.
# -U0: zero context lines (only the +/- lines)
# ^\+       : lines that start with + (added)
# ^\+\+\+   : excluded (these are file headers like "+++ b/foo.txt")
added_lines="$(git diff --cached -U0 | grep -E '^\+' | grep -vE '^\+\+\+' || true)"

# Check for debugging keywords or TODOs in ADDED lines only
if echo "$added_lines" | grep -qE "(console\.log|debugger|binding\.pry|TODO:|FIXME:|XXX:)"; then
    echo -e "${YELLOW}Warning: Found debugging keywords or TODOs in added lines:${NC}"
    echo "$added_lines" | grep -nE "(console\.log|debugger|binding\.pry|TODO:|FIXME:|XXX:)" || true
    echo
    # Probe by OPENING /dev/tty, not testing existence — on macOS /dev/tty always
    # exists ([ -e ] passes) but open() fails with ENXIO when there is no controlling
    # terminal (agent harnesses, CI), which used to kill the read and block the commit.
    if ( : < /dev/tty ) 2>/dev/null; then
        printf "Continue anyway? (y/n) "
        read -n 1 -r REPLY < /dev/tty
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            exit 1
        fi
    else
        echo -e "${YELLOW}(non-interactive; proceeding — rerun in a TTY to enforce)${NC}"
    fi
fi

# Check for large files (>5MB) — operates on file size, not diff content
#
# -z / read -d '': the path list MUST be null-delimited. Unquoted $(git diff
# --cached --name-only) word-splits, so a staged path containing whitespace broke
# into tokens, every token failed the [ -f ] test below, and the file was skipped
# ENTIRELY — a 17 MB "my big file.dat" passed this check without it ever running.
# That is REVIEWED-105's class again (a check that passes because it could not
# run), and it is why the loop is fed by process substitution rather than a pipe:
# a pipe would put the body in a subshell where `exit 1` cannot refuse the commit.
# Measured 2026-08-26 (PENDING-163 AMENDMENT 1). Narrows nothing, widens nothing —
# it makes this check do what it already claimed to do.
while IFS= read -r -d '' file; do
    if [ -f "$file" ]; then
        size=$(wc -c < "$file")
        if [ "$size" -gt 5242880 ]; then
            echo -e "${RED}Error: $file is larger than 5 MiB (5,242,880 bytes)${NC}"
            echo "This ceiling measures the working-tree file. Git LFS does not exempt it,"
            echo "and LFS was tried in this system and retired (chamber-library 0677e8a, 2026-06-05)."
            echo "If this repo legitimately holds large files it needs its own hook:"
            echo "copy .githooks from chamber-library (400c054), add the path exemption there,"
            echo "then: git config --local core.hooksPath .githooks"
            exit 1
        fi
    fi
done < <(git diff --cached --name-only -z)

# Check for secrets in ADDED lines only (basic check)
if echo "$added_lines" | grep -qE "(password|secret|token|api_key)[[:space:]]*=[[:space:]]*[\"'][^\"']+[\"']"; then
    echo -e "${RED}Warning: Possible secrets detected in added lines:${NC}"
    echo "$added_lines" | grep -nE "(password|secret|token|api_key)[[:space:]]*=[[:space:]]*[\"'][^\"']+[\"']" || true
    echo "Please review your changes carefully."
    echo
    # Same open()-probe as above. Note the deliberate asymmetry: secrets BLOCK when
    # non-interactive (fail closed); debug keywords proceed with a warning.
    if ( : < /dev/tty ) 2>/dev/null; then
        printf "Continue anyway? (y/n) "
        read -n 1 -r REPLY < /dev/tty
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            exit 1
        fi
    else
        echo -e "${RED}(non-interactive; blocking — rerun in a TTY to confirm or use --no-verify with explicit justification)${NC}"
        exit 1
    fi
fi

# ── Repo-declared checks ──────────────────────────────────────── (REVIEWED-100)
# This hook is global to every repo (core.hooksPath), so it must hold no repo
# knowledge. A repo opts in by declaring `.precommit-triggers` at its root:
#
#     <git pathspec> [more pathspecs] | <command>
#
# If the staged diff touches a declared pathspec, the command runs and a non-zero
# exit refuses the commit. Path matching is delegated to git's own pathspec
# engine rather than reimplemented here.
#
# Deliberately dependency-free — no yq, no python. A global convention that needs
# a toolchain silently fails to travel to the next machine or repo, and a check
# that silently does not run is worse than no check, because its absence reads as
# a pass. That is why this departs from the YAML used by data python tools read.
# A DISARMED HOOK MUST NOT LOOK LIKE AN ARMED ONE (REVIEWED-105 / PENDING-123).
# Measured 2026-08-08: five distinct disarming faults — pathspec typo, missing '|',
# empty command, comments-only file, empty file — every one silent at exit 0, and
# indistinguishable both from each other and from the legitimate "docs-only commit"
# case. A malformed declaration therefore REFUSES (never skips), and when a triggers
# file exists but nothing matched, that fact is PRINTED rather than left to silence.
refuse_declaration() {   # $1 line number, $2 fault, $3 the offending line
    echo -e "${RED}Malformed .precommit-triggers — commit refused.${NC}"
    echo "  file:  ${triggers_file}"
    echo "  line:  $1"
    echo "  fault: $2"
    echo "  text:  $3"
    echo "  expected:  <git pathspec> [more pathspecs] | <command>"
    echo "  --no-verify bypasses this; it is a tripwire, not a boundary."
    exit 1
}

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
triggers_file="${repo_root:-.}/.precommit-triggers"
if [ -n "$repo_root" ] && [ -f "$triggers_file" ]; then
    declared=0; matched=0; lineno=0; declared_paths=""; rule_detail=""
    # fd 3, so a check that reads stdin cannot swallow the rest of this file
    while IFS= read -r rawline <&3 || [ -n "${rawline:-}" ]; do
        lineno=$((lineno + 1))
        trimmed="$(printf '%s' "$rawline" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
        case "$trimmed" in ''|\#*) continue ;; esac
        declared=$((declared + 1))

        # ── (b) validate the declaration ────────────────────────────────────────
        case "$rawline" in
            *"|"*) ;;
            *) refuse_declaration "$lineno" "no '|' separator between pathspec and command" "$trimmed" ;;
        esac
        # split exactly as `IFS='|' read paths cmd` did, so matched-rule output stays
        # byte-identical to what REVIEWED-100's acceptance test proved
        paths="${rawline%%|*}"
        cmd="$(printf '%s' "${rawline#*|}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
        paths_trimmed="$(printf '%s' "$paths" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
        [ -n "$paths_trimmed" ] || refuse_declaration "$lineno" "pathspec is empty" "$trimmed"
        [ -n "$cmd" ] || refuse_declaration "$lineno" "command is empty" "$trimmed"
        declared_paths="${declared_paths}${declared_paths:+, }${paths_trimmed}"

        # shellcheck disable=SC2086 — word splitting is intended: multiple pathspecs
        if ! staged="$(git diff --cached --name-only -- $paths 2>/dev/null)"; then
            refuse_declaration "$lineno" "git cannot resolve this pathspec" "$trimmed"
        fi
        if [ -z "$staged" ]; then
            rule_detail="${rule_detail}    line ${lineno}: [${paths_trimmed}] — declared, no staged match
"
            continue
        fi
        matched=$((matched + 1))

        echo -e "${YELLOW}Staged change touches [${paths# }] — running declared check:${NC} $cmd"
        if ! ( cd "$repo_root" && eval "$cmd" ) < /dev/null; then
            echo -e "${RED}Declared check FAILED — commit refused.${NC}"
            echo "  declared in: .precommit-triggers"
            echo "  command:     $cmd"
            echo "  --no-verify bypasses this; it is a tripwire, not a boundary."
            exit 1
        fi
        echo -e "${GREEN}Declared check passed.${NC}"
    done 3< "$triggers_file"

    # ── (e) speak in exactly the ambiguous case ─────────────────────────────────
    # A rule ran: the lines above already said so — add nothing. No triggers file:
    # this block never runs, so no other repo gains noise. Rules declared and none
    # matched is the ONLY case a reader cannot otherwise distinguish from a broken
    # hook, so it is the only case that gets a line.
    if [ "$declared" -eq 0 ]; then
        # A triggers file that declares nothing is a DISARMED state wearing an armed
        # face: the file is present, so the repo looks opted-in, and every commit
        # sails through. Refusing would block a legitimately-emptied file, so this
        # reports rather than refuses — but it must not be silent.
        echo -e "${YELLOW}.precommit-triggers exists but declares no rules — this repo is opted in and unguarded.${NC}"
    elif [ "$matched" -eq 0 ]; then
        echo -e "${YELLOW}${declared} rule(s) declared in .precommit-triggers, none matched staged paths (${declared_paths}).${NC}"
        # A rule that has NEVER matched is honestly unknown, not passing and not
        # failing — the hook holds no history and must not imply one. PRECOMMIT_VERBOSE
        # prints the per-rule detail for the reader who wants to check a suspect pathspec.
        if [ -n "${PRECOMMIT_VERBOSE:-}" ]; then
            printf '%s' "$rule_detail"
        fi
    fi
fi

echo -e "${GREEN}Pre-commit checks passed!${NC}"
