#!/bin/sh
# Pre-commit hook for Signal Fish Server
# Runs code formatting, linting, and validation checks before each commit.
#
# To install: ./scripts/enable-hooks.sh
# To bypass (not recommended): git commit --no-verify
#
# Checks performed:
#   1. Code formatting (cargo fmt)
#   2. Clippy lints (cargo clippy)
#   3. Panic-prone patterns
#   4. MSRV consistency (if Cargo.toml or config files modified)
#   5. Workflow AWK scripts (if workflow files modified)
#   6. YAML lint (if workflow files modified)
#   7. AWK file syntax (if .awk files modified)
#   8. Shellcheck for CI scripts (if .github/scripts/ files modified)
#   9. Markdown relative link validation (if docs/ markdown modified)
#  10. Markdown linting (if markdown files modified)
#  11. Link checking (if markdown files modified)
#  12. Markdown code block validation (if markdown files modified)
#  13. British English spellings (warning only)
#  14. Path-filtered workflow registration (if workflow files modified)
#  15. Schedule trigger guards (if workflow files modified)
#  16. Skills index generation freshness (if skills inputs/script/context change)
#  17. Workflow hygiene script checks (if workflow/hygiene script files modified)
#  18. LLM file size limit (if .llm/ files modified — max 300 lines per file)
#  19. Miri compatibility (if Rust source files modified)
#  20. README badge style consistency (if README.md modified)
#  21. Documentation + changelog consistency (version sync, changelog format, protocol drift)

set -e

# Color output helpers
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Disable colors if not a TTY
if [ ! -t 1 ]; then
    RED=''
    GREEN=''
    YELLOW=''
    BLUE=''
    NC=''
fi

TOTAL_CHECKS=21

echo "${BLUE}[pre-commit]${NC} Running pre-commit checks..."
echo ""

CHECKS_PASSED=0
CHECKS_FAILED=0

# Helper to report check results
check_pass() {
    echo "${GREEN}✓ PASS${NC}: $1"
    CHECKS_PASSED=$((CHECKS_PASSED + 1))
}

check_fail() {
    echo "${RED}✗ FAIL${NC}: $1"
    echo "${RED}[pre-commit] ERROR:${NC} $2"
    CHECKS_FAILED=$((CHECKS_FAILED + 1))
}

check_warn() {
    echo "${YELLOW}⚠ WARN${NC}: $1"
    echo "${YELLOW}[pre-commit] WARNING:${NC} $2"
    CHECKS_PASSED=$((CHECKS_PASSED + 1))
}

check_skip() {
    echo "${YELLOW}⊘ SKIP${NC}: $1 ($2)"
}

check_fix() {
    echo "${GREEN}✓ PASS${NC}: $1 (auto-fixed)"
    CHECKS_PASSED=$((CHECKS_PASSED + 1))
}

# Check 1: Code formatting (always run, auto-fixable)
echo "${BLUE}[1/${TOTAL_CHECKS}]${NC} Checking code formatting..."
if cargo fmt --check 2>/dev/null; then
    check_pass "Code formatting"
else
    # Save list of staged .rs files before auto-fixing
    FMT_STAGED_RS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.rs$' || true)
    echo "${YELLOW}  Auto-fixing...${NC} running cargo fmt"
    if cargo fmt 2>/dev/null; then
        # Re-stage only the .rs files that were originally staged
        if [ -n "$FMT_STAGED_RS" ]; then
            printf '%s\n' "$FMT_STAGED_RS" | while IFS= read -r _file; do
                if [ -n "$_file" ]; then git add "$_file"; fi
            done
        fi
        check_fix "Code formatting"
    else
        check_fail "Code formatting" "cargo fmt failed. Run 'cargo fmt' manually to investigate."
        echo ""
    fi
fi

# Check 2: Clippy lints (fast check on staged Rust files only, auto-fixable)
echo "${BLUE}[2/${TOTAL_CHECKS}]${NC} Running clippy on staged files..."
# Get list of staged Rust files
STAGED_RS_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.rs$' || true)

if [ -n "$STAGED_RS_FILES" ]; then
    # Run clippy on workspace (faster than per-file)
    # Use --all-features to catch all issues, but allow a longer timeout
    if CLIPPY_OUT=$(cargo clippy --all-targets --all-features -- -D warnings 2>&1); then
        check_pass "Clippy lints"
    else
        # Save list of staged .rs files before auto-fixing
        CLIPPY_STAGED_RS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.rs$' || true)
        echo "${YELLOW}  Auto-fixing...${NC} running cargo clippy --fix"
        # --allow-dirty: working tree may have unstaged changes
        # --allow-staged: pre-commit hook always has staged changes
        CLIPPY_FIX_OUT=$(cargo clippy --fix --allow-dirty --allow-staged --all-targets --all-features 2>&1) || true
        # Re-stage only the .rs files that were originally staged
        if [ -n "$CLIPPY_STAGED_RS" ]; then
            printf '%s\n' "$CLIPPY_STAGED_RS" | while IFS= read -r _file; do
                if [ -n "$_file" ]; then git add "$_file"; fi
            done
        fi
        # Re-run clippy to verify the fix worked
        if CLIPPY_RECHECK=$(cargo clippy --all-targets --all-features -- -D warnings 2>&1); then
            check_fix "Clippy lints"
        else
            check_fail "Clippy lints" "Fix clippy warnings before committing."
            echo "$CLIPPY_RECHECK"
            echo "${YELLOW}Note:${NC} Some lints have placeholder suggestions and cannot be auto-fixed."
            echo "${YELLOW}Tip:${NC} cargo clippy --fix --allow-dirty --all-targets --all-features"
            echo ""
        fi
    fi
else
    check_skip "Clippy lints" "no Rust files staged"
fi

# Check 3: Panic-prone patterns (fast pattern scan only)
echo "${BLUE}[3/${TOTAL_CHECKS}]${NC} Checking for panic-prone patterns..."
if [ -x scripts/check-no-panics.sh ]; then
    if scripts/check-no-panics.sh patterns 2>&1 | grep -q "ERROR"; then
        check_fail "Panic patterns" "Remove .unwrap(), panic!(), expect() from production code."
        echo ""
    else
        check_pass "Panic patterns"
    fi
else
    check_skip "Panic patterns" "check-no-panics.sh not found or not executable"
fi

# Check 4: MSRV consistency (only if relevant files changed)
echo "${BLUE}[4/${TOTAL_CHECKS}]${NC} Checking MSRV consistency..."
MSRV_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '(Cargo.toml|rust-toolchain.toml|clippy.toml|Dockerfile)$' || true)

if [ -n "$MSRV_FILES" ]; then
    if [ -x scripts/check-msrv-consistency.sh ]; then
        if scripts/check-msrv-consistency.sh 2>&1 | grep -q "FAILED"; then
            check_fail "MSRV consistency" "MSRV mismatch across configuration files."
            echo "${YELLOW}Tip:${NC} ./scripts/check-msrv-consistency.sh for details"
            echo ""
        else
            check_pass "MSRV consistency"
        fi
    else
        check_skip "MSRV consistency" "check-msrv-consistency.sh not found"
    fi
else
    check_skip "MSRV consistency" "no MSRV-related files changed"
fi

# Check 5: Workflow AWK validation (only if workflow files changed)
echo "${BLUE}[5/${TOTAL_CHECKS}]${NC} Validating AWK scripts in workflows..."
WORKFLOW_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.github/workflows/.*\.ya?ml$' || true)

if [ -n "$WORKFLOW_FILES" ]; then
    if [ -x scripts/validate-workflow-awk.sh ]; then
        # Pass workflow files as NUL-delimited values to preserve spaces in paths.
        if git diff --cached --name-only -z --diff-filter=ACM -- \
            ':(glob).github/workflows/*.yml' \
            ':(glob).github/workflows/*.yaml' \
            | xargs -0 scripts/validate-workflow-awk.sh 2>&1 | grep -q "FAILED"; then
            check_fail "AWK validation" "AWK script syntax errors in workflow files."
            echo "${YELLOW}Tip:${NC} ./scripts/validate-workflow-awk.sh"
            echo ""
        else
            check_pass "AWK validation"
        fi
    else
        check_skip "AWK validation" "validate-workflow-awk.sh not found"
    fi
else
    check_skip "AWK validation" "no workflow files changed"
fi

# Check 6: YAML lint (only if workflow files changed)
echo "${BLUE}[6/${TOTAL_CHECKS}]${NC} Running yamllint on workflow files..."

if [ -n "$WORKFLOW_FILES" ]; then
    if command -v yamllint >/dev/null 2>&1; then
        YAMLLINT_ERRORS=0
        for wf_file in $WORKFLOW_FILES; do
            if [ -f "$wf_file" ]; then
                YAMLLINT_OUT=$(yamllint -c .yamllint.yml "$wf_file" 2>&1) || true
                if echo "$YAMLLINT_OUT" | grep -q "\[error\]"; then
                    echo "${RED}  YAML lint errors:${NC} $wf_file"
                    echo "$YAMLLINT_OUT" | grep "\[error\]" || true
                    YAMLLINT_ERRORS=$((YAMLLINT_ERRORS + 1))
                fi
            fi
        done
        if [ "$YAMLLINT_ERRORS" -gt 0 ]; then
            check_fail "YAML lint" "Fix YAML lint errors in workflow files."
            echo "${YELLOW}Tip:${NC} yamllint -c .yamllint.yml .github/workflows/"
            echo ""
        else
            check_pass "YAML lint"
        fi
    else
        check_skip "YAML lint" "yamllint not installed (pip install yamllint)"
    fi
else
    check_skip "YAML lint" "no workflow files changed"
fi

# Check 7: AWK file syntax (only if .awk files are staged)
echo "${BLUE}[7/${TOTAL_CHECKS}]${NC} Validating AWK file syntax..."
STAGED_AWK_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.awk$' || true)

if [ -n "$STAGED_AWK_FILES" ]; then
    AWK_ERRORS=0
    for awk_file in $STAGED_AWK_FILES; do
        if [ -f "$awk_file" ]; then
            if ! awk -f "$awk_file" < /dev/null > /dev/null 2>&1; then
                echo "${RED}  AWK syntax error:${NC} $awk_file"
                awk -f "$awk_file" < /dev/null 2>&1 || true
                AWK_ERRORS=$((AWK_ERRORS + 1))
            fi
        fi
    done
    if [ "$AWK_ERRORS" -gt 0 ]; then
        check_fail "AWK file syntax" "Fix AWK syntax errors in staged .awk files."
        echo ""
    else
        check_pass "AWK file syntax"
    fi
else
    check_skip "AWK file syntax" "no .awk files staged"
fi

# Check 8: Shellcheck for CI scripts (only if .github/scripts/ files are staged)
echo "${BLUE}[8/${TOTAL_CHECKS}]${NC} Running shellcheck on CI scripts..."
STAGED_CI_SCRIPTS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.github/scripts/.*\.sh$' || true)

if [ -n "$STAGED_CI_SCRIPTS" ]; then
    if command -v shellcheck >/dev/null 2>&1; then
        SC_ERRORS=0
        for script in $STAGED_CI_SCRIPTS; do
            if [ -f "$script" ]; then
                if ! shellcheck -s bash "$script" > /dev/null 2>&1; then
                    echo "${RED}  shellcheck errors:${NC} $script"
                    shellcheck -s bash "$script" 2>&1 | head -10
                    SC_ERRORS=$((SC_ERRORS + 1))
                fi
            fi
        done
        if [ "$SC_ERRORS" -gt 0 ]; then
            check_fail "Shellcheck CI scripts" "Fix shellcheck errors in .github/scripts/."
            echo ""
        else
            check_pass "Shellcheck CI scripts"
        fi
    else
        check_skip "Shellcheck CI scripts" "shellcheck not installed"
    fi
else
    check_skip "Shellcheck CI scripts" "no .github/scripts/*.sh files staged"
fi

# Check 9: Markdown relative link validation (only if docs/ markdown files are staged)
echo "${BLUE}[9/${TOTAL_CHECKS}]${NC} Validating markdown relative links in docs/..."
STAGED_DOCS_MD=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^docs/.*\.md$' || true)

if [ -n "$STAGED_DOCS_MD" ]; then
    LINK_ERRORS=0
    for md_file in $STAGED_DOCS_MD; do
        if [ -f "$md_file" ]; then
            base_dir=$(dirname "$md_file")
            # Extract relative links with fence-aware AWK (skips links inside code blocks
            # and inline code spans to avoid false positives on example links).
            for url in $(awk '
              /^```/ {
                if (!fence_width) {
                  n=0;s=$0;while(substr(s,1,1)=="`"){n++;s=substr(s,2)};fence_width=n
                } else {
                  n=0;s=$0;while(substr(s,1,1)=="`"){n++;s=substr(s,2)}
                  if(n>=fence_width&&s~/^[ \t]*$/)fence_width=0
                }
                next
              }
              !fence_width { line=$0; gsub(/`[^`]+`/,"",line)
                while(match(line,/\[([^]]+)\]\(([^)]+)\)/)){
                  u=substr(line,RSTART,RLENGTH);sub(/.*\(/,"",u);sub(/\).*/,"",u);print u
                  line=substr(line,RSTART+RLENGTH)
                }
              }
            ' "$md_file" | grep -vE '^(https?://|mailto:|#)' || true); do
                # Strip anchor for file check
                file_part="${url%%#*}"
                [ -z "$file_part" ] && continue

                resolved="$base_dir/$file_part"
                if [ ! -f "$resolved" ] && [ ! -d "$resolved" ]; then
                    echo "${RED}  Broken link:${NC} $md_file -> $url"
                    LINK_ERRORS=$((LINK_ERRORS + 1))
                fi

                # Check for .llm/ links missing ../ prefix
                case "$url" in
                    .llm/*)
                        echo "${RED}  Invalid prefix:${NC} $md_file -> $url (should be ../$url)"
                        LINK_ERRORS=$((LINK_ERRORS + 1))
                        ;;
                esac
            done
        fi
    done
    if [ "$LINK_ERRORS" -gt 0 ]; then
        check_fail "Docs link validation" "Fix broken or incorrect relative links in docs/."
        echo "${YELLOW}Tip:${NC} ./scripts/validate-ci.sh --links"
        echo ""
    else
        check_pass "Docs link validation"
    fi
else
    check_skip "Docs link validation" "no docs/*.md files staged"
fi

# Check 10: Markdown linting (only if markdown files changed, auto-fixable)
echo "${BLUE}[10/${TOTAL_CHECKS}]${NC} Checking markdown files..."
STAGED_MD_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)

if [ -n "$STAGED_MD_FILES" ]; then
    if [ -x scripts/check-markdown.sh ]; then
        if MARKDOWN_OUTPUT=$(scripts/check-markdown.sh 2>&1); then
            check_pass "Markdown linting"
        else
            MARKDOWN_STATUS=$?
            if [ "$MARKDOWN_STATUS" -eq 2 ]; then
                check_fail "Markdown linting" "markdownlint-cli2 unavailable or wrong pinned version."
                echo "$MARKDOWN_OUTPUT"
                echo "${YELLOW}Tip:${NC} npm install --save-dev --save-exact markdownlint-cli2@\$(cat .markdownlint-version)"
                echo ""
            else
                # Exit code 1 means lint errors — attempt auto-fix
                # Save list of staged .md files before auto-fixing
                MD_STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.md$' || true)
                echo "${YELLOW}  Auto-fixing...${NC} running scripts/check-markdown.sh fix"
                scripts/check-markdown.sh fix 2>/dev/null || true
                # Re-stage only the .md files that were originally staged
                if [ -n "$MD_STAGED_FILES" ]; then
                    printf '%s\n' "$MD_STAGED_FILES" | while IFS= read -r _file; do
                        if [ -n "$_file" ]; then git add "$_file"; fi
                    done
                fi
                # Re-run to verify the fix worked (capture fresh output)
                if MARKDOWN_RECHECK=$(scripts/check-markdown.sh 2>&1); then
                    check_fix "Markdown linting"
                else
                    RECHECK_STATUS=$?
                    if [ "$RECHECK_STATUS" -eq 2 ]; then
                        check_fail "Markdown linting" "markdownlint-cli2 unavailable or wrong pinned version."
                        echo "$MARKDOWN_RECHECK"
                        echo "${YELLOW}Tip:${NC} npm install --save-dev --save-exact markdownlint-cli2@\$(cat .markdownlint-version)"
                        echo ""
                    else
                        check_fail "Markdown linting" "Markdown files have formatting issues."
                        echo "$MARKDOWN_RECHECK"
                        echo "${YELLOW}Tip:${NC} ./scripts/check-markdown.sh fix"
                        echo ""
                    fi
                fi
            fi
        fi
    else
        check_fail "Markdown linting" "check-markdown.sh not found"
        echo "${YELLOW}Tip:${NC} Restore scripts/check-markdown.sh"
        echo ""
    fi
else
    check_skip "Markdown linting" "no markdown files changed"
fi

# Check 11: Link checking (only if markdown files changed)
echo "${BLUE}[11/${TOTAL_CHECKS}]${NC} Checking links in markdown files..."

if [ -n "$STAGED_MD_FILES" ]; then
    if command -v lychee >/dev/null 2>&1; then
        # Run lychee with fast settings (offline mode for pre-commit speed)
        # Use NUL-delimited file lists so paths with spaces are handled safely.
        if ! git diff --cached --name-only -z --diff-filter=ACM -- '*.md' \
            | xargs -0 lychee --offline --quiet --config .lychee.toml 2>/dev/null; then
            check_warn "Link checking (offline)" "Link issues found (offline mode). Run 'lychee --config .lychee.toml <file>' for full check."
        else
            check_pass "Link checking (offline)"
        fi
    else
        check_skip "Link checking" "lychee not installed"
    fi
else
    check_skip "Link checking" "no markdown files changed"
fi

# Check 12: Markdown code block validation (only if markdown files changed)
# Validates JSON (jq), YAML (yq), TOML (taplo), and Bash (bash -n) code blocks
# extracted from staged markdown files, matching the CI doc-validation.yml workflow.
echo "${BLUE}[12/${TOTAL_CHECKS}]${NC} Validating code blocks in markdown files..."

if [ -n "$STAGED_MD_FILES" ]; then
    # Filter out test fixture directories from staged markdown files
    CB_MD_FILES=$(echo "$STAGED_MD_FILES" | grep -vE '^(\.github/test-fixtures/|test-fixtures/)' || true)

    if [ -n "$CB_MD_FILES" ]; then
        CB_TEMP_DIR=$(mktemp -d)
        # Use file-based counters to avoid subshell scope issues
        CB_COUNTER_FILE="$CB_TEMP_DIR/counters"
        # Counter file format: 2 space-separated integers (total failed)
        echo "0 0" > "$CB_COUNTER_FILE"

        # AWK program that extracts code blocks and writes each to a file.
        # This matches the fence tracking pattern used in doc-validation.yml:
        #   - Outer fences (4+ backticks) skip nested 3-backtick examples
        #   - Closing fences allow trailing spaces/tabs per CommonMark spec
        #
        # AWK writes each block to OUTDIR/block_NNN.EXT and emits
        # "line_number<TAB>filename" to stdout for the shell to process.
        # This avoids NUL-delimited reads (read -d is a bashism, not POSIX sh).
        #
        # Parameters (via -v):
        #   LANG_PATTERN  - regex to match opening fence tags
        #   OUTDIR        - directory to write extracted block files
        #   EXT           - file extension for extracted blocks
        EXTRACT_BLOCKS_AWK='
          # Skip content inside outer fences (4+ backticks) which may contain
          # nested 3-backtick examples (e.g., documentation about code blocks).
          /^````/ {
            if (!outer_fence) {
              n = 0; s = $0; while (substr(s,1,1)=="`") { n++; s = substr(s,2) }
              outer_fence = n
            } else {
              n = 0; s = $0; while (substr(s,1,1)=="`") { n++; s = substr(s,2) }
              if (n >= outer_fence && s ~ /^[ \t]*$/) outer_fence = 0
            }
            next
          }
          outer_fence { next }
          # Match opening fence for target language code blocks
          $0 ~ LANG_PATTERN {
            in_block = 1
            block_start = NR
            block_num++
            content = ""
            next
          }
          # Match closing fence only if in a block
          # CommonMark: closing fences may have trailing spaces/tabs
          /^```[ \t]*$/ && in_block {
            if (content != "") {
              outfile = OUTDIR "/block_" block_num "." EXT
              printf "%s", content > outfile
              close(outfile)
              print block_start "\t" outfile
            }
            in_block = 0
            next
          }
          # Accumulate content while in block
          in_block {
            content = content $0 "\n"
          }
          # Handle unclosed blocks at end of file
          END {
            if (in_block && content != "") {
              outfile = OUTDIR "/block_" block_num "." EXT
              printf "%s", content > outfile
              close(outfile)
              print block_start "\t" outfile
            }
          }
        '

        # Check which validation tools are available
        HAS_JQ=0
        HAS_YQ=0
        HAS_TAPLO=0

        if command -v jq >/dev/null 2>&1; then HAS_JQ=1; fi
        if command -v yq >/dev/null 2>&1; then HAS_YQ=1; fi
        if command -v taplo >/dev/null 2>&1; then HAS_TAPLO=1; fi

        # --- Validate JSON code blocks ---
        if [ "$HAS_JQ" -eq 1 ]; then
            for md_file in $CB_MD_FILES; do
                [ -f "$md_file" ] || continue
                awk -v LANG_PATTERN='^```json$' \
                    -v OUTDIR="$CB_TEMP_DIR" \
                    -v EXT="json" \
                    "$EXTRACT_BLOCKS_AWK" "$md_file" | while IFS='	' read -r line_num outfile; do
                    [ -z "$outfile" ] && continue
                    read -r cb_total cb_failed < "$CB_COUNTER_FILE"
                    cb_total=$((cb_total + 1))

                    if ! jq empty "$outfile" 2>/dev/null; then
                        echo "${RED}  Invalid JSON in $md_file at line $line_num${NC}"
                        cb_failed=$((cb_failed + 1))
                    fi

                    echo "$cb_total $cb_failed" > "$CB_COUNTER_FILE"
                done
            done
        fi

        # --- Validate YAML code blocks ---
        if [ "$HAS_YQ" -eq 1 ]; then
            for md_file in $CB_MD_FILES; do
                [ -f "$md_file" ] || continue
                awk -v LANG_PATTERN='^```(yaml|yml)$' \
                    -v OUTDIR="$CB_TEMP_DIR" \
                    -v EXT="yaml" \
                    "$EXTRACT_BLOCKS_AWK" "$md_file" | while IFS='	' read -r line_num outfile; do
                    [ -z "$outfile" ] && continue
                    read -r cb_total cb_failed < "$CB_COUNTER_FILE"
                    cb_total=$((cb_total + 1))

                    if ! yq eval . "$outfile" >/dev/null 2>&1; then
                        echo "${RED}  Invalid YAML in $md_file at line $line_num${NC}"
                        cb_failed=$((cb_failed + 1))
                    fi

                    echo "$cb_total $cb_failed" > "$CB_COUNTER_FILE"
                done
            done
        fi

        # --- Validate TOML code blocks (skip if taplo not available) ---
        if [ "$HAS_TAPLO" -eq 1 ]; then
            for md_file in $CB_MD_FILES; do
                [ -f "$md_file" ] || continue
                awk -v LANG_PATTERN='^```toml$' \
                    -v OUTDIR="$CB_TEMP_DIR" \
                    -v EXT="toml" \
                    "$EXTRACT_BLOCKS_AWK" "$md_file" | while IFS='	' read -r line_num outfile; do
                    [ -z "$outfile" ] && continue
                    read -r cb_total cb_failed < "$CB_COUNTER_FILE"
                    cb_total=$((cb_total + 1))

                    if ! taplo check "$outfile" 2>/dev/null; then
                        echo "${RED}  Invalid TOML in $md_file at line $line_num${NC}"
                        cb_failed=$((cb_failed + 1))
                    fi

                    echo "$cb_total $cb_failed" > "$CB_COUNTER_FILE"
                done
            done
        fi

        # --- Validate Bash code blocks (bash -n syntax check, always available) ---
        for md_file in $CB_MD_FILES; do
            [ -f "$md_file" ] || continue
            awk -v LANG_PATTERN='^```(bash|sh|shell)$' \
                -v OUTDIR="$CB_TEMP_DIR" \
                -v EXT="sh" \
                "$EXTRACT_BLOCKS_AWK" "$md_file" | while IFS='	' read -r line_num outfile; do
                [ -z "$outfile" ] && continue
                # Skip placeholder/example-only blocks
                if grep -qE '^#.*example|^# Note:|^\.\.\.$' "$outfile"; then
                    continue
                fi
                read -r cb_total cb_failed < "$CB_COUNTER_FILE"
                cb_total=$((cb_total + 1))

                if ! bash -n "$outfile" 2>/dev/null; then
                    echo "${RED}  Invalid Bash syntax in $md_file at line $line_num${NC}"
                    cb_failed=$((cb_failed + 1))
                fi

                echo "$cb_total $cb_failed" > "$CB_COUNTER_FILE"
            done
        done

        # Read final counters
        read -r cb_total cb_failed < "$CB_COUNTER_FILE"
        rm -rf "$CB_TEMP_DIR"

        # Build a summary of which validators ran
        CB_TOOLS_USED=""
        if [ "$HAS_JQ" -eq 1 ]; then CB_TOOLS_USED="${CB_TOOLS_USED}JSON "; fi
        if [ "$HAS_YQ" -eq 1 ]; then CB_TOOLS_USED="${CB_TOOLS_USED}YAML "; fi
        if [ "$HAS_TAPLO" -eq 1 ]; then CB_TOOLS_USED="${CB_TOOLS_USED}TOML "; fi
        CB_TOOLS_USED="${CB_TOOLS_USED}Bash"

        if [ "$cb_failed" -gt 0 ]; then
            check_fail "Code block validation" "$cb_failed/$cb_total blocks invalid (checked: $CB_TOOLS_USED)"
            echo ""
        else
            if [ "$cb_total" -gt 0 ]; then
                check_pass "Code block validation ($cb_total blocks: $CB_TOOLS_USED)"
            else
                check_pass "Code block validation (no blocks found)"
            fi
        fi
    else
        check_skip "Code block validation" "only test fixture markdown files staged"
    fi
else
    check_skip "Code block validation" "no markdown files changed"
fi

# Check 13: British English spellings (warning only)
# Catches common British spellings that should be American English.
# This is a WARNING (non-blocking) since there may be legitimate uses
# (e.g., external references, proper nouns, quotes).
echo "${BLUE}[13/${TOTAL_CHECKS}]${NC} Checking for British English spellings..."
STAGED_TEXT_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(rs|yml|yaml|md)$' || true)

if [ -n "$STAGED_TEXT_FILES" ]; then
    # Each entry is "british_pattern:american_equivalent"
    BRITISH_WORDS="uninitialised:uninitialized
initialised:initialized
initialise:initialize
initialising:initializing
behaviour:behavior
colour:color
colours:colors
favour:favor
favoured:favored
favouring:favoring
honour:honor
honoured:honored
honouring:honoring
serialise:serialize
serialised:serialized
serialising:serializing
organise:organize
organised:organized
organising:organizing
recognise:recognize
recognised:recognized
recognising:recognizing"

    # Fast path: scan only added staged lines, not full files.
    BRIT_OUTPUT=$(git diff --cached --unified=0 --no-color --diff-filter=ACM -- '*.rs' '*.yml' '*.yaml' '*.md' | awk -v pairs="$BRITISH_WORDS" '
        BEGIN {
            n = split(pairs, entries, "\n")
            word_count = 0
            for (i = 1; i <= n; i++) {
                if (entries[i] == "") {
                    continue
                }
                split(entries[i], kv, ":")
                if (kv[1] == "" || kv[2] == "") {
                    continue
                }
                word_count++
                british_words[word_count] = kv[1]
                american[kv[1]] = kv[2]
            }
            warning_count = 0
            file = ""
            new_line = 0
        }

        /^\+\+\+ b\// {
            file = substr($0, 7)
            next
        }

        /^@@ / {
            hunk = $0
            sub(/^@@ -[0-9]+(,[0-9]+)? \+/, "", hunk)
            sub(/[^0-9].*$/, "", hunk)
            if (hunk ~ /^[0-9]+$/) {
                new_line = hunk - 1
            }
            next
        }

        /^ / {
            new_line++
            next
        }

        /^\+/ && $0 !~ /^\+\+\+/ {
            new_line++
            line = substr($0, 2)

            # Skip URLs to avoid false positives on external references.
            if (line ~ /http:\/\/|https:\/\//) {
                next
            }

            line_lower = tolower(line)
            for (i = 1; i <= word_count; i++) {
                word = british_words[i]
                pattern = "(^|[^[:alnum:]_])" word "([^[:alnum:]_]|$)"
                if (line_lower ~ pattern) {
                    printf "  British spelling: %s:%d: found '\''%s'\'', prefer '\''%s'\''\n", file, new_line, word, american[word]
                    warning_count++
                }
            }
            next
        }

        END {
            print "__BRIT_COUNT__=" warning_count
        }
    ')

    BRIT_WARNINGS=$(printf "%s\n" "$BRIT_OUTPUT" | awk -F= '/^__BRIT_COUNT__=/{print $2}')
    BRIT_WARNINGS=${BRIT_WARNINGS:-0}
    BRIT_MESSAGES=$(printf "%s\n" "$BRIT_OUTPUT" | sed '/^__BRIT_COUNT__=/d' | sed '/^[[:space:]]*$/d')

    if [ -n "$BRIT_MESSAGES" ]; then
        echo "$BRIT_MESSAGES"
    fi

    if [ "$BRIT_WARNINGS" -gt 0 ]; then
        check_warn "British English spellings" "$BRIT_WARNINGS British spelling(s) found. Consider using American English."
    else
        check_pass "British English spellings"
    fi
else
    check_skip "British English spellings" "no .rs/.yml/.yaml/.md files staged"
fi

# Check 14: Path-filtered workflow registration in release preflight
# Ensures any workflow in REQUIRED_WORKFLOWS that uses path filters is also
# registered in PATH_FILTERED_WORKFLOWS in release.yml, so the release
# preflight correctly handles skipped workflow runs.
echo "${BLUE}[14/${TOTAL_CHECKS}]${NC} Checking path-filtered workflow registration..."

RELEASE_FILE=".github/workflows/release.yml"
if [ -n "$WORKFLOW_FILES" ]; then
    if [ -f "$RELEASE_FILE" ]; then
        PF_ERRORS=0

        # For each staged workflow file, check if it has path filters AND
        # its name appears in REQUIRED_WORKFLOWS. If so, it must also
        # appear in PATH_FILTERED_WORKFLOWS.
        for wf_file in $WORKFLOW_FILES; do
            [ -f "$wf_file" ] || continue
            # Skip release.yml itself
            [ "$wf_file" = "$RELEASE_FILE" ] && continue

            # Extract the workflow name
            WF_NAME=$(grep -E '^name:' "$wf_file" | head -1 | sed 's/^name:[[:space:]]*//' | sed 's/^["'"'"']//' | sed 's/["'"'"']$//' || true)
            [ -z "$WF_NAME" ] && continue

            # Check if this workflow uses path filters
            HAS_PATHS=$(grep -c '^[[:space:]]*paths:' "$wf_file" 2>/dev/null) || HAS_PATHS=0

            if [ "$HAS_PATHS" -gt 0 ]; then
                # Check if it's in REQUIRED_WORKFLOWS
                if grep -q "\"$WF_NAME\"" "$RELEASE_FILE" 2>/dev/null; then
                    # Check if it's registered in PATH_FILTERED_WORKFLOWS
                    if ! grep -q "PATH_FILTERED_WORKFLOWS\[\"$WF_NAME\"\]" "$RELEASE_FILE" 2>/dev/null; then
                        echo "${RED}  Missing registration:${NC} Workflow '${WF_NAME}' (${wf_file})"
                        echo "${RED}    Has path filters and is in REQUIRED_WORKFLOWS but not in PATH_FILTERED_WORKFLOWS${NC}"
                        echo "${RED}    Add: PATH_FILTERED_WORKFLOWS[\"${WF_NAME}\"]=\"<paths>\" to release.yml${NC}"
                        PF_ERRORS=$((PF_ERRORS + 1))
                    fi
                fi
            fi
        done

        if [ "$PF_ERRORS" -gt 0 ]; then
            check_fail "Path-filtered workflow registration" "Workflows with path filters in REQUIRED_WORKFLOWS must be registered in PATH_FILTERED_WORKFLOWS in release.yml."
            echo ""
        else
            check_pass "Path-filtered workflow registration"
        fi
    else
        check_skip "Path-filtered workflow registration" "release.yml not found"
    fi
else
    check_skip "Path-filtered workflow registration" "no workflow files changed"
fi

# Check 15: Schedule trigger guards
# Ensures CI jobs have `if:` guards to exclude schedule events when the
# workflow has a schedule trigger. Without these guards, scheduled runs
# (e.g., daily security audits) execute all jobs unnecessarily.
echo "${BLUE}[15/${TOTAL_CHECKS}]${NC} Checking schedule trigger guards..."

if [ -n "$WORKFLOW_FILES" ]; then
    SCHED_ERRORS=0

    for wf_file in $WORKFLOW_FILES; do
        [ -f "$wf_file" ] || continue

        # Check if this workflow has a schedule trigger
        HAS_SCHEDULE=$(grep -c '^[[:space:]]*schedule:' "$wf_file" 2>/dev/null) || HAS_SCHEDULE=0
        [ "$HAS_SCHEDULE" -eq 0 ] && continue

        # Check for workflow-level allowlist: if the workflow has a top-level
        # comment "# all-jobs-run-on-schedule" in the first 30 lines, all jobs
        # are intended to run on schedule, so skip the per-job check entirely.
        if head -30 "$wf_file" | grep -q '# all-jobs-run-on-schedule'; then
            continue
        fi

        # Extract job names and check for schedule guards
        # A job that should run on schedule won't have the guard (e.g., security audit).
        # Jobs that should NOT run on schedule need: if: github.event_name != 'schedule'
        # We look for jobs that have neither a schedule-aware `if:` guard
        # nor an explicit comment indicating they should run on schedule.
        #
        # Use AWK to find job blocks and check for if: guards
        JOBS_WITHOUT_GUARD=$(awk '
            /^jobs:/ { in_jobs = 1; next }
            !in_jobs { next }
            # Match top-level job key (2-space indent under jobs:)
            /^  [a-zA-Z_][a-zA-Z0-9_-]*:/ {
                if (job_name != "" && !has_guard && !is_schedule_job) {
                    print job_name
                }
                job_name = $1
                sub(/:$/, "", job_name)
                has_guard = 0
                is_schedule_job = 0
                next
            }
            # Check for schedule exclusion guard in job-level if:
            /^    if:.*schedule/ { has_guard = 1 }
            # Check for explicit per-job annotation or keyword heuristic
            /^    #.*runs-on-schedule/ { is_schedule_job = 1 }
            /^    #.*schedule|^    #.*security|^    #.*audit|^    #.*daily/ { is_schedule_job = 1 }
            END {
                if (job_name != "" && !has_guard && !is_schedule_job) {
                    print job_name
                }
            }
        ' "$wf_file")

        if [ -n "$JOBS_WITHOUT_GUARD" ]; then
            for job_name in $JOBS_WITHOUT_GUARD; do
                echo "${RED}  Missing guard:${NC} ${wf_file}: job '${job_name}' has no schedule exclusion guard"
                echo "${RED}    Add: if: github.event_name != 'schedule'${NC}"
                SCHED_ERRORS=$((SCHED_ERRORS + 1))
            done
        fi
    done

    if [ "$SCHED_ERRORS" -gt 0 ]; then
        check_fail "Schedule trigger guards" "Jobs in schedule-triggered workflows need 'if: github.event_name != '\"'\"'schedule'\"'\"'' guards."
        echo ""
    else
        check_pass "Schedule trigger guards"
    fi
else
    check_skip "Schedule trigger guards" "no workflow files changed"
fi

# Check 16: Skills index generation freshness
# Ensures .llm/skills/index.md is generated and up-to-date when staged inputs
# change (added/modified/deleted/renamed): skill files, generator script,
# or .llm/context.md.
echo "${BLUE}[16/${TOTAL_CHECKS}]${NC} Checking generated skills index freshness..."
STAGED_SKILLS_INDEX_BYTES=$(git diff --cached --name-only -z --diff-filter=ACDMR -- \
    .llm/context.md \
    scripts/generate-skills-index.sh \
    ':(glob).llm/skills/*.md' \
    | wc -c | tr -d '[:space:]')

if [ "$STAGED_SKILLS_INDEX_BYTES" -gt 0 ]; then
    if [ -x scripts/generate-skills-index.sh ]; then
        if scripts/generate-skills-index.sh --check > /dev/null 2>&1; then
            check_pass "Skills index freshness"
        else
            # Index is stale — attempt auto-fix by regenerating
            echo "${YELLOW}  Auto-fixing...${NC} running scripts/generate-skills-index.sh"
            if scripts/generate-skills-index.sh > /dev/null 2>&1; then
                # Always re-stage index.md — it's a generated file that must
                # be committed alongside its inputs.
                git add .llm/skills/index.md
                # Re-run check to verify the fix worked
                if scripts/generate-skills-index.sh --check > /dev/null 2>&1; then
                    check_fix "Skills index freshness"
                else
                    check_fail "Skills index freshness" "Run './scripts/generate-skills-index.sh' and stage .llm/skills/index.md."
                    echo ""
                fi
            else
                check_fail "Skills index freshness" "scripts/generate-skills-index.sh failed. Run it manually to investigate."
                echo ""
            fi
        fi
    else
        check_fail "Skills index freshness" "scripts/generate-skills-index.sh not found or not executable."
        echo ""
    fi
else
    check_skip "Skills index freshness" "no .llm skills/context/index-related files staged"
fi

# Check 17: Workflow hygiene script checks
# Runs the comprehensive workflow-hygiene validator when workflow files or
# automation scripts/hooks that can execute third-party tooling are staged.
echo "${BLUE}[17/${TOTAL_CHECKS}]${NC} Running workflow hygiene script checks..."
STAGED_WORKFLOW_HYGIENE_BYTES=$(git diff --cached --name-only -z --diff-filter=ACDMR -- \
    ':(glob).github/workflows/*.yml' \
    ':(glob).github/workflows/*.yaml' \
    ':(glob)scripts/*.sh' \
    ':(glob).githooks/*' \
    scripts/check-workflow-hygiene.sh \
    .github/workflows/workflow-hygiene.yml \
    | wc -c | tr -d '[:space:]')

if [ "$STAGED_WORKFLOW_HYGIENE_BYTES" -gt 0 ]; then
    if [ -x scripts/check-workflow-hygiene.sh ]; then
        if scripts/check-workflow-hygiene.sh > /dev/null 2>&1; then
            check_pass "Workflow hygiene checks"
        else
            check_fail "Workflow hygiene checks" "Run './scripts/check-workflow-hygiene.sh' and fix reported errors."
            echo ""
        fi
    else
        check_fail "Workflow hygiene checks" "scripts/check-workflow-hygiene.sh not found or not executable."
        echo ""
    fi
else
    check_skip "Workflow hygiene checks" "no workflow or automation hygiene files staged"
fi

# Check 18: LLM file size limit (only if .llm/ files are staged)
# Ensures all files in .llm/ stay within the 300-line limit so they remain
# focused, scannable, and fit within LLM context windows.
echo "${BLUE}[18/${TOTAL_CHECKS}]${NC} Checking LLM file sizes..."
STAGED_LLM_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^\.llm/.*\.md$' || true)

if [ -n "$STAGED_LLM_FILES" ]; then
    if [ -x scripts/check-llm-file-sizes.sh ]; then
        if ! LLM_SIZE_OUTPUT=$(
            # Preserve spaces in staged file paths by splitting only on newlines.
            IFS='
'
            set -f
            scripts/check-llm-file-sizes.sh --files $STAGED_LLM_FILES 2>&1
        ); then
            check_fail "LLM file sizes" "One or more .llm/ files exceed 300 lines. Run './scripts/check-llm-file-sizes.sh' for details."
            echo "$LLM_SIZE_OUTPUT"
            echo "${YELLOW}Tip:${NC} Split oversized files into focused sub-files of ≤300 lines."
            echo ""
        else
            check_pass "LLM file sizes"
        fi
    else
        check_skip "LLM file sizes" "check-llm-file-sizes.sh not found or not executable"
    fi
else
    check_skip "LLM file sizes" "no .llm/*.md files staged"
fi

# Check 19: Miri compatibility (only if Rust source files staged)
# Ensures lib tests calling Miri-incompatible APIs have #[cfg_attr(miri, ignore)].
echo "${BLUE}[19/${TOTAL_CHECKS}]${NC} Checking Miri compatibility..."
STAGED_SRC_RS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^src/.*\.rs$' || true)

if [ -n "$STAGED_SRC_RS" ]; then
    if [ -x scripts/check-miri-compat.sh ]; then
        MIRI_OUTPUT=$(scripts/check-miri-compat.sh 2>&1) || true
        if echo "$MIRI_OUTPUT" | grep -q 'WARNING:'; then
            check_warn "Miri compatibility" "Some lib tests may need #[cfg_attr(miri, ignore)]. Run './scripts/check-miri-compat.sh' for details."
        else
            check_pass "Miri compatibility"
        fi
    else
        check_skip "Miri compatibility" "check-miri-compat.sh not found or not executable"
    fi
else
    check_skip "Miri compatibility" "no src/*.rs files staged"
fi

# Check 20: README badge style consistency (if README.md or checker script is staged)
# Ensures all Shields.io badges in README.md use style=for-the-badge.
echo "${BLUE}[20/${TOTAL_CHECKS}]${NC} Checking README badge style consistency..."
STAGED_README_BADGE_INPUTS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(README\.md|scripts/check-readme-badges\.sh)$' || true)

if [ -n "$STAGED_README_BADGE_INPUTS" ]; then
    if [ -x scripts/check-readme-badges.sh ]; then
        if README_BADGE_OUTPUT=$(scripts/check-readme-badges.sh README.md 2>&1); then
            check_pass "README badge styles"
        else
            check_fail "README badge styles" "Run './scripts/check-readme-badges.sh README.md' and ensure all Shields badges use style=for-the-badge."
            echo "$README_BADGE_OUTPUT"
            echo ""
        fi
    else
        check_fail "README badge styles" "scripts/check-readme-badges.sh not found or not executable."
        echo ""
    fi
else
    check_skip "README badge styles" "README.md and scripts/check-readme-badges.sh not staged"
fi

# Check 21: Documentation + changelog consistency
# Enforces version sync, keep-a-changelog structure, and protocol quick-reference drift checks.
echo "${BLUE}[21/${TOTAL_CHECKS}]${NC} Checking documentation + changelog consistency..."

if [ -x scripts/check-doc-consistency.sh ]; then
    if scripts/check-doc-consistency.sh --staged > /dev/null 2>&1; then
        check_pass "Doc + changelog consistency"
    else
        check_fail "Doc + changelog consistency" "Run './scripts/check-doc-consistency.sh --staged' and fix reported issues."
        echo ""
    fi
else
    check_fail "Doc + changelog consistency" "scripts/check-doc-consistency.sh not found or not executable."
    echo ""
fi

# Summary
echo ""
echo "=========================================="
if [ "$CHECKS_FAILED" -gt 0 ]; then
    echo "${RED}[pre-commit] ${CHECKS_FAILED} check(s) FAILED, ${CHECKS_PASSED} passed${NC}"
    echo ""
    echo "${YELLOW}Fix the errors above before committing.${NC}"
    echo ""
    echo "${YELLOW}Quick fixes:${NC}"
    echo "  cargo fmt                                    # Fix formatting"
    echo "  cargo clippy --fix --allow-dirty            # Fix clippy warnings"
    echo "  ./scripts/check-markdown.sh fix             # Fix markdown"
    echo "  ./scripts/validate-ci.sh                    # Validate CI config"
    echo "  ./scripts/generate-skills-index.sh          # Refresh .llm/skills/index.md"
    echo "  ./scripts/check-llm-file-sizes.sh           # Check .llm/ file sizes"
    echo "  ./scripts/check-miri-compat.sh              # Check Miri annotations"
    echo "  ./scripts/check-readme-badges.sh README.md  # Check README badge styles"
    echo "  ./scripts/check-doc-consistency.sh --staged # Check version/changelog/doc consistency"
    echo "  ./scripts/run-local-ci.sh --fix             # Fix all issues"
    echo ""
    echo "${YELLOW}To bypass (NOT recommended):${NC}"
    echo "  git commit --no-verify"
    echo ""
    exit 1
else
    echo "${GREEN}[pre-commit] All checks passed! (${CHECKS_PASSED} passed)${NC}"
    echo ""
    exit 0
fi
