#!/bin/bash
# Universal RouteRTL Project Pre-Commit Hook
# 
# This hook reads configuration from project.yml via project_manager.py
# and executes the requested checks (linting, yaml check, tests).

# 0. Source project-local hook extensions (if present)
if [ -f "hooks/pre-commit.local" ]; then
    echo "🔌 Sourcing hooks/pre-commit.local..."
    source hooks/pre-commit.local
fi

# ── Markdown-Only Detection ──
# If the commit only touches .md files, skip heavyweight checks (linting,
# regression, simulation) but still run docs-related checks (link
# validation, MkDocs build).
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
NON_MD_FILES=$(echo "$STAGED_FILES" | grep -v '\.md$' || true)
MD_ONLY=false
if [ -z "$NON_MD_FILES" ] && [ -n "$STAGED_FILES" ]; then
    MD_ONLY=true
    echo "📝 Markdown-only commit — skipping HDL/sim checks, running docs checks."
fi

# 1. Detect Python interpreter (use venv if available) — needed early so
#    the SDK lookup can probe site-packages for a pip-installed routertl.
if [ -f ".venv/bin/python3" ]; then
    PYTHON_CMD=".venv/bin/python3"
else
    PYTHON_CMD="python3"
fi

# 2. Locate SDK Tools
#    Post-P2.39: sdk/ is the canonical layout (engine/, infra/, generators/, …).
#    Pre-P2.39:  tools/ held the same content (project-manager/, docs/, …).
#    Check sdk/ first so consumers that updated their submodule get the right
#    paths even if a leftover tools/ directory still exists on disk.  When
#    none of the on-disk layouts match, probe the active Python for a
#    pip-installed routertl SDK (RTL-P2.346: hooks used to silently skip
#    for `pip install routertl` consumers).
if [ -d "vendor/routertl/sdk" ]; then
    SDK_TOOLS="vendor/routertl/sdk"
    PROJECT_MANAGER="$SDK_TOOLS/engine/project_manager.py"
    CHECK_LINKS="$SDK_TOOLS/infra/docs/check_links.py"
elif [ -d "vendor/routertl/tools" ]; then
    # Legacy layout (pre-P2.39 consumers)
    SDK_TOOLS="vendor/routertl/tools"
    PROJECT_MANAGER="$SDK_TOOLS/project-manager/project_manager.py"
    CHECK_LINKS="$SDK_TOOLS/docs/check_links.py"
elif [ -n "$ROUTERTL_ROOT" ] && [ -f "$ROUTERTL_ROOT/sdk/engine/project_manager.py" ]; then   # RTL-P2.994: fall through on a bogus ROUTERTL_ROOT
    SDK_TOOLS="$ROUTERTL_ROOT/sdk"
    PROJECT_MANAGER="$SDK_TOOLS/engine/project_manager.py"
    CHECK_LINKS="$SDK_TOOLS/infra/docs/check_links.py"
elif [ -d "sdk/engine" ]; then
    # Fallback for the SDK repo itself
    SDK_TOOLS="sdk"
    PROJECT_MANAGER="$SDK_TOOLS/engine/project_manager.py"
    CHECK_LINKS="$SDK_TOOLS/infra/docs/check_links.py"
elif [ -d "tools/project-manager" ]; then
    # Legacy fallback for the SDK repo itself (pre-P2.39)
    SDK_TOOLS="tools"
    PROJECT_MANAGER="$SDK_TOOLS/project-manager/project_manager.py"
    CHECK_LINKS="$SDK_TOOLS/docs/check_links.py"
elif PIP_SDK=$($PYTHON_CMD -c 'import os, sdk; print(os.path.dirname(sdk.__file__))' 2>/dev/null) && [ -n "$PIP_SDK" ] && [ -d "$PIP_SDK/engine" ]; then
    # Pip-installed routertl — the SDK lives under site-packages/sdk/.
    SDK_TOOLS="$PIP_SDK"
    PROJECT_MANAGER="$SDK_TOOLS/engine/project_manager.py"
    CHECK_LINKS="$SDK_TOOLS/infra/docs/check_links.py"
    echo "🔍 Using pip-installed RouteRTL SDK: $SDK_TOOLS"
else
    # No SDK anywhere.  Loudly refuse — silent skip used to mask the
    # fact that hooks.pre_commit.tests wasn't running at all, so users
    # believed commits were gated when they weren't (RTL-P2.346).
    HAS_HOOK_CONFIG=""
    if [ -f "project.yml" ] && grep -qE '^[[:space:]]*(hooks|pre_commit):' project.yml 2>/dev/null; then
        HAS_HOOK_CONFIG="yes"
    fi
    echo "" >&2
    echo "❌ RouteRTL SDK tools not found." >&2
    echo "   Looked in:" >&2
    echo "     • vendor/routertl/sdk/"           >&2
    echo "     • vendor/routertl/tools/ (legacy)" >&2
    echo "     • \$ROUTERTL_ROOT/sdk/"           >&2
    echo "     • sdk/ (in-tree SDK repo)"        >&2
    echo "     • tools/project-manager/ (legacy)" >&2
    echo "     • $PYTHON_CMD site-packages (pip-installed routertl)" >&2
    echo "" >&2
    echo "   Fix by one of:" >&2
    echo "     • pip install routertl (into the active venv)" >&2
    echo "     • export ROUTERTL_ROOT=/path/to/routertl" >&2
    echo "     • add routertl as a git submodule at vendor/routertl" >&2
    echo "" >&2
    if [ -n "$HAS_HOOK_CONFIG" ]; then
        echo "   project.yml declares hooks — aborting commit so configured" >&2
        echo "   tests actually run (set HOOK_SKIP_IF_MISSING=1 to override)." >&2
        if [ "$HOOK_SKIP_IF_MISSING" = "1" ]; then
            echo "   HOOK_SKIP_IF_MISSING=1 — skipping anyway." >&2
            exit 0
        fi
        exit 1
    else
        echo "   project.yml has no hooks configured — skipping." >&2
        exit 0
    fi
fi

if [ ! -f "project.yml" ]; then
    echo "⚠️ project.yml not found. Skipping hook."
    exit 0
fi

# 2.5 Gate execution helpers (RTL-P3.1537) — sourced, shared with the routertl
#     hook (RTL-P2.1101). See sdk/infra/hooks/gate_lib.sh for the full rationale.
_GATE_LIB="$(dirname "${BASH_SOURCE[0]}")/gate_lib.sh"
if [ -f "$_GATE_LIB" ]; then
    # shellcheck source=/dev/null
    source "$_GATE_LIB"
elif [ -n "${SDK_ROOT:-}" ] && [ -f "$SDK_ROOT/sdk/infra/hooks/gate_lib.sh" ]; then
    # shellcheck source=/dev/null
    source "$SDK_ROOT/sdk/infra/hooks/gate_lib.sh"
else
    echo "❌ gate_lib.sh not found next to this hook — refusing to run gates" >&2
    echo "   without could-not-run classification, which would report a crashed" >&2
    echo "   tool as a finding about your code (RTL-P3.1537)." >&2
    exit 1
fi

# 3. Extract Configuration
echo "🔍 Reading hook configuration from project.yml..."

# Inject SDK repo root into PYTHONPATH so project_manager can import
# 'sdk.*' and 'routertl_core.*'.  When SDK_TOOLS is a bare name like
# "sdk" (SDK repo itself), %/* has no effect → fall back to CWD.
SDK_REPO_ROOT="${SDK_TOOLS%/*}"
if [ "$SDK_REPO_ROOT" = "$SDK_TOOLS" ]; then
    SDK_REPO_ROOT="."
fi
export PYTHONPATH="$SDK_REPO_ROOT:$PYTHONPATH"

echo "🔍 Using Python interpreter: $PYTHON_CMD"
_gate_tmp
CONFIG=$($PYTHON_CMD "$PROJECT_MANAGER" project.yml --gen-hook-config 2>"$GATE_OUTPUT_FILE")
gate_classify "$?"

if [ "$GATE_STATUS" != "ok" ]; then
    if [ "$GATE_STATUS" = "violation" ]; then
        cat "$GATE_OUTPUT_FILE" >&2
    fi
    gate_abort "project.yml hook-config parse ($PROJECT_MANAGER)" \
        "Failed to parse project.yml. Commit aborted."
fi

eval "$CONFIG"

# 3. Check Enabled Status
if [ "$HOOK_ENABLED" != "true" ]; then
    echo "⏩ Hooks disabled in project.yml. Skipping."
    exit 0
fi

# ── 3.2 No-HDL-touched fast path (RTL-P1.119) ──
# MD_ONLY above is far too coarse to be the only skip: it fires only when EVERY
# staged file is .md. A commit touching a .gitignore, a shell script and a doc —
# nothing that can change a line of HDL — still ran the full `rr lint` over the
# whole source tree, spawning a dependency resolver per entity. Measured on
# deepskopion 2026-08-02: over ten minutes for a config-and-docs commit.
#
# routertl's OWN hook has had this since RTL-P3.426; the project hook it ships
# to consumers did not. This is that predicate, made generic — the HDL roots
# come from HOOK_HDL_ROOTS, derived by project_manager from the project's
# declared paths.sources / paths.simulation / paths.constraints, because every
# project puts its HDL somewhere different.
#
# SKIP_HDL_HEAVY is deliberately a SEPARATE switch from MD_ONLY. MD_ONLY gates
# several steps here, including Python linting and the targeted staged-file
# lints (Immediate()-on-ready, VALID-into-reset, AXIS-skid). Reusing it would
# silently disable those on exactly the commits this speeds up — trading
# minutes for a hole in the gate. Only the two genuinely heavy steps (full
# lint, full regression) read this flag.
#
# VERSION SKEW: the fast path is armed only when HOOK_HDL_ROOTS is SET — which
# distinguishes "this project declares no source roots" (set, empty: trust it)
# from "the project_manager that produced this config predates RTL-P1.119"
# (unset). Treating unset as "no HDL anywhere" would make an older SDK paired
# with a newer hook silently stop running the full gate — a hook that quietly
# does less is far worse than one that does too much. Unset ⇒ old ⇒ full gate.
SKIP_HDL_HEAVY=false
if [ "$MD_ONLY" = "false" ] && [ -n "${HOOK_HDL_ROOTS+set}" ]; then
    # HDL extensions always count, wherever they live.
    _HDL_PATTERN='\.(vhd|vhdl|v|sv|svh|vh)$'
    # Declared source/sim/constraint roots for THIS project.
    for _root in $HOOK_HDL_ROOTS; do
        _HDL_PATTERN="${_HDL_PATTERN}|^${_root}/"
    done
    # Declared TCL hooks (block design / ps7 / gen_project) — build inputs with
    # no HDL extension, living outside paths.*. Matched as exact paths.
    for _f in $HOOK_HDL_FILES; do
        _HDL_PATTERN="${_HDL_PATTERN}|^$(printf '%s' "$_f" | sed 's/[.[\*^$]/\\&/g')\$"
    done
    # Build wiring: these can change the COMPILE SET with no .vhd staged, so
    # they must trigger the full lint. Conservative on purpose.
    _HDL_PATTERN="${_HDL_PATTERN}|^project\.yml\$|^targets/|^ip\.yml\$|^ip\.lock\$|^Makefile\$"

    _HDL_STAGED=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "$_HDL_PATTERN" || true)
    if [ -z "$_HDL_STAGED" ]; then
        SKIP_HDL_HEAVY=true
        echo "⚡ No-HDL-touched fast path (RTL-P1.119): skipping full lint + regression."
        echo "   No staged file matches an HDL surface or build-wiring path."
        echo "   Everything else — python lint, staged-file HDL lints, yaml/docs — still runs."
    else
        echo "🔬 HDL surface touched — full lint. Triggering files:"
        echo "$_HDL_STAGED" | sed 's/^/     /'
    fi
fi

# 3.5. Frozen Directory Guard
if [ -n "$HOOK_FROZEN_DIRS" ]; then
    STAGED_FILES=$(git diff --cached --name-only)
    for dir in $HOOK_FROZEN_DIRS; do
        VIOLATIONS=$(echo "$STAGED_FILES" | grep "^${dir}/" || true)
        if [ -n "$VIOLATIONS" ]; then
            echo "❌ Frozen directory violation: '$dir' is marked frozen in project.yml"
            echo "   Offending files:"
            echo "$VIOLATIONS" | sed 's/^/     /'
            echo ""
            echo "   Remove these files from staging (git reset HEAD <file>) or update project.yml."
            exit 1
        fi
    done
    echo "✅ Frozen directory check passed."
fi

# 4. Phase 1.5: Markdown Link Validation
if [ "$HOOK_CHECK_LINKS" = "true" ]; then
    echo "🔗 Validating Markdown Links..."
    EXCLUDE_ARGS=""
    for dir in $HOOK_EXCLUDE_DOC_DIRS; do
        EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude $dir"
    done
    # shellcheck disable=SC2086
    if ! run_gate $PYTHON_CMD "$CHECK_LINKS" --dir docs $EXCLUDE_ARGS; then
        gate_abort "Markdown link validation ($CHECK_LINKS)" \
            "Markdown link validation failed. Commit aborted."
    fi
fi

# 4.5 MkDocs Documentation Build (strict mode)
if [ "$HOOK_CHECK_DOCS" = "true" ] && [ -f "mkdocs.yml" ]; then
    echo "📚 Building documentation (mkdocs build --strict)..."
    MKDOCS_TMPDIR="/tmp/mkdocs-precommit-$$"
    if ! run_gate mkdocs build --strict --site-dir "$MKDOCS_TMPDIR"; then
        rm -rf "$MKDOCS_TMPDIR"
        gate_abort "MkDocs strict build (mkdocs build --strict)" \
            "MkDocs build failed. Commit aborted."
    fi
    rm -rf "$MKDOCS_TMPDIR"
    echo "✅ Documentation build passed."
fi

# 4. Check YAML (implicit in the parsing above, but good to be explicit)
if [ "$HOOK_CHECK_YAML" = "true" ]; then
    echo "✅ YAML check passed (implicit)."
fi

# 4.7 Python Linting (ruff)
if [ "$HOOK_LINT_PYTHON" = "true" ] && [ "$MD_ONLY" = "false" ]; then
    echo "🐍 Running Python linting (ruff check)..."
    if ! run_gate ruff check .; then
        gate_abort "Python linting (ruff check)" \
            "Python linting failed. Commit aborted."
    fi
    echo "✅ Python linting passed."
fi

# RTL-P2.597: Heavyweight steps below invoke `rr` directly via the same
# Python interpreter + PYTHONPATH the hook already resolved, instead of
# delegating to `make linting / clean / regression / simulation`.
# The Makefile-driven shape was incompatible with pure-routertl projects
# (no Makefile) — `rr` is the canonical entry per CLAUDE.md, and a
# Makefile shim simply forwarded to the same commands anyway.
RR="$PYTHON_CMD -m sdk.cli.main"
# run_gate pipes tool output through tee; keep Python line-buffered so a long
# gate still streams instead of arriving in 8 KiB blocks.
export PYTHONUNBUFFERED=1

# 4.9 Immediate()-on-ready sim lint (RTL-P3.855) — the Haz.73 footgun gate.
#     Cross-repo enforcement of the lint routertl self-guards via
#     test_routertl_sim_tree_is_clean: a consumer `*_ready`/`*_tready` driven by
#     cocotb `Immediate()` lands in the same timestep as a post-edge monitor
#     sample and manufactures a bogus `*_DATA_HOLD` violation (the false
#     DDR-T2.9). Scans ONLY the staged .py files (cheap AST check, per-line
#     `# noqa: immediate-ready` escape) so unrelated commits pay nothing.
if [ "$MD_ONLY" = "false" ]; then
    STAGED_PY=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.py$' || true)
    if [ -n "$STAGED_PY" ]; then
        echo "🚦 Linting staged tests for Immediate()-on-ready (rr sim lint-immediate)..."
        # shellcheck disable=SC2086
        if ! run_gate $RR sim lint-immediate $STAGED_PY; then
            gate_abort "Immediate()-on-ready lint (rr sim lint-immediate)" \
                "Immediate()-on-ready lint failed (RTL-P3.853/Haz.73). Commit aborted."
        fi
    fi
fi

# 4.10 VALID-into-reset static lint (RTL-P3.858) — the AXIS_RST footgun gate.
#      A combinational VALID strobe (tvalid/OUT_VALID/BVALID) driven off an FSM
#      state with NO reset term stays HIGH for the first reset cycle (AMBA
#      §2.7.1). Hit 5x (RTL-T2.26, DS-T2.47, VHD-T2.9/T2.10). Scans ONLY staged
#      .vhd/.vhdl (cheap line/regex check, per-line `-- noqa: valid-into-reset`).
if [ "$MD_ONLY" = "false" ]; then
    STAGED_VHD=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.vhdl?$' || true)
    if [ -n "$STAGED_VHD" ]; then
        echo "🚦 Linting staged VHDL for VALID-into-reset (rr sim lint-valid-into-reset)..."
        # shellcheck disable=SC2086
        if ! run_gate $RR sim lint-valid-into-reset $STAGED_VHD; then
            gate_abort "VALID-into-reset lint (rr sim lint-valid-into-reset)" \
                "VALID-into-reset lint failed (RTL-P3.858/AMBA §2.7.1). Commit aborted."
        fi
    fi
fi

# 4.11 RR-AXIS-001 skid-buffer static lint (RTL-P3.1073). AXI ready
#      backpressure across a registered boundary needs a two-entry skid buffer:
#      register forward valid/data and backward ready together. Scans only
#      staged HDL (cheap regex check, per-statement `lint-ok: axis-skid`).
if [ "$MD_ONLY" = "false" ]; then
    STAGED_HDL=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(vhd|vhdl|v|sv|vh|svh)$' || true)
    if [ -n "$STAGED_HDL" ]; then
        echo "🚦 Linting staged HDL for AXI skid-buffer anti-patterns (rr sim lint-axis-skid)..."
        # shellcheck disable=SC2086
        if ! run_gate $RR sim lint-axis-skid $STAGED_HDL; then
            gate_abort "AXI skid-buffer lint (rr sim lint-axis-skid)" \
                "AXI skid-buffer lint failed (RR-AXIS-001/RTL-P3.1073). Commit aborted."
        fi
    fi
fi

# 4.12 FDD documentation gate (RTL-P2.956) — staged-aware, FAST depth only.
#      Fires ONLY when a staged file is an eligible target's canonical FDD input
#      (requirements*.yml / ip.yml / project_regbank.yml / FDD.md / decisions.yml)
#      or a project.yml policy change. Advisory by default (findings printed,
#      commit proceeds); strict (fdd.policy: strict) refuses the commit. The
#      bash-side pattern gate keeps an unrelated commit free (no `rr` startup).
#      Runs for MD-only commits too (an FDD.md edit is markdown).
STAGED_FDD=$(git diff --cached --name-only --diff-filter=ACMR \
    | grep -E '(^|/)(requirements[^/]*\.yml|ip\.yml|project_regbank\.yml|FDD\.md|decisions\.yml|project\.yml)$' || true)
if [ -n "$STAGED_FDD" ]; then
    echo "📄 FDD documentation gate (rr fdd precommit) — staged canonical inputs..."
    # shellcheck disable=SC2086
    if ! run_gate $RR fdd precommit $STAGED_FDD; then
        gate_abort "FDD documentation gate (rr fdd precommit)" \
            "FDD documentation gate failed (RTL-P2.956, fdd.policy: strict). Commit aborted."
    fi
fi

# 5. Run Linting
if [ "$HOOK_LINT" = "true" ] && [ "$MD_ONLY" = "false" ] && [ "$SKIP_HDL_HEAVY" = "false" ]; then
    echo "🧹 Running HDL Linting (rr lint)..."
    LINT_EXCLUDE_ARGS=""
    for pat in $HOOK_EXCLUDE_LINT; do
        LINT_EXCLUDE_ARGS="$LINT_EXCLUDE_ARGS --exclude $pat"
    done
    # shellcheck disable=SC2086
    if ! run_gate $RR lint $LINT_EXCLUDE_ARGS; then
        gate_abort "HDL linting (rr lint)" \
            "Linting failed. Commit aborted."
    fi
fi

# 5.5 Run Regression (if enabled)
if [ "$HOOK_RUN_REGRESSION" = "true" ] && [ "$MD_ONLY" = "false" ] && [ "$SKIP_HDL_HEAVY" = "false" ]; then
# RTL-T1.25: every `rr sim run` below passes --wait, and that flag is
# LOAD-BEARING. Without it, an environment with RR_QUEUE_SIMS=1 /
# queue_sims: true (which agent benches are told to export, TIC-P3.24) turns
# the run into a queue SUBMIT that exits 0 — so this gate reported PASS for
# tests that had not run, or had failed. Measured: an `assert False` cocotb
# test exited 0 with the knob on, 1 with it off. --wait blocks on the job and
# adopts its exit code; with the knob off it is a no-op, because the submit
# path is never taken.
    echo "🧪 Running full regression suite (rr sim run --all --wait)..."
    # shellcheck disable=SC2086
    if ! run_gate $RR sim run --all --wait; then
        gate_abort "full regression (rr sim run --all)" \
            "Regression failed. Commit aborted."
    fi
fi

# 6. Run Selected Tests
if [ -n "$HOOK_TESTS" ] && [ "$MD_ONLY" = "false" ]; then
    echo "🧪 Running configured tests: $HOOK_TESTS"

    # Clean simulation workspace to avoid false positives from stale artifacts
    echo "🧹 Cleaning simulation workspace (rr workspace clean --sim)..."
    $RR workspace clean --sim || true

    for test in $HOOK_TESTS; do
        # Check if test is in the exclusion list
        SKIP_TEST=false
        for excluded in $HOOK_EXCLUDE_TESTS; do
            if [ "$test" = "$excluded" ]; then
                echo "   ⏭️  Skipping (excluded): $test"
                SKIP_TEST=true
                break
            fi
        done

        if [ "$SKIP_TEST" = "true" ]; then
            continue
        fi

        # HOOK_TESTS may carry dotted module names from `tests: auto`
        # discovery (e.g. tests.units.test_edge). `rr sim run` accepts the
        # bare testbench name, so strip everything up to the last dot.
        TB="${test##*.}"
        echo "   ▶ rr sim run $TB --wait"
        # shellcheck disable=SC2086
        if ! run_gate $RR sim run "$TB" --wait; then
            gate_abort "testbench '$TB' (rr sim run $TB)" \
                "Test '$TB' failed. Commit aborted."
        fi
    done
fi

# ── Re-stage files modified by this hook ──
# rr workspace clean wipes simulation artifacts, then `rr sim run` writes
# a fresh regression_summary.txt. If the file is tracked, git would show
# a staged vs working-tree mismatch (`MM`) that silently prevents commit.
# Fix: re-add any tracked files the hook itself touched.
RESTAGE_FILES="hw/sim/logs/regression_summary.txt"
for f in $RESTAGE_FILES; do
    if [ -f "$f" ] && git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
        git add "$f"
    fi
done

echo "✅ All checks passed."
exit 0
