#!/bin/sh
set -e

hook_dir=$(dirname "$0")
. "$hook_dir/lib.sh"

changed=$(mktemp)
harn_format_files=$(mktemp)
harn_lint_files=$(mktemp)
changed_packages=$(mktemp)
trap 'rm -f "$changed" "$harn_format_files" "$harn_lint_files" "$changed_packages"' EXIT
hook_write_staged_files "$changed"
hook_disable_cargo_incremental_if_release_bump staged

# ── Guard: block accidental commits of throwaway / temporary code ──────────────
# Mark scratch code so it can never be committed by accident:
#   NOCOMMIT   @nocommit   DO NOT COMMIT   (case-insensitive; -/_ also accepted)
# Only newly *added* staged lines are scanned; this hook excludes itself. Bypass
# a false positive with `git commit --no-verify`. The regex literal is split so
# this file's own definition is not itself a matchable marker.
nocommit_re="NO""COMMIT|@no""commit|DO[ _-]NOT[ _-]COMMIT"
staged_nocommit=$(git diff --cached --no-color -U0 --diff-filter=ACM \
  -- . ":(exclude).githooks/pre-commit" \
  | grep -E '^\+[^+]' | grep -inE "$nocommit_re" 2>/dev/null || true)
if [ -n "$staged_nocommit" ]; then
  echo "✖ Commit blocked: staged changes contain a no-commit marker:" >&2
  printf '%s\n' "$staged_nocommit" | sed 's/^/    /' >&2
  echo "  Remove the temporary code, or bypass intentionally: git commit --no-verify" >&2
  exit 1
fi

# ── Guard: verify tagref cross-references resolve (when tagref is installed) ────
# tagref links code via [tag:name] / [ref:name] comments; fails on a dangling
# [ref:...] or duplicate [tag:...]. Respects .gitignore. Skipped (not failed)
# when absent, keeping the hook portable. Install: `cargo install tagref`.
if command -v tagref >/dev/null 2>&1; then
  if ! tagref --path "$(git rev-parse --show-toplevel)" check >/dev/null 2>&1; then
    echo "✖ tagref check failed: a [ref:...] has no matching [tag:...], or a tag is duplicated." >&2
    echo "  Run \`tagref --path . check\` for details, or \`git commit --no-verify\` to bypass." >&2
    exit 1
  fi
fi

# Track whether the python prompt-prose ratchet has already run in this
# hook, so the ratchet and clippy blocks don't pay for it twice when both
# patterns match (the common case for a Rust crate edit that also
# touches scripts/ or Makefile). Whichever block runs first sets the flag.
prompt_prose_done=0

# ---------------------------------------------------------------------------
# Phase 1 — fast, read-only validators (fail-fast).
#
# These are cheap (python / npx, sub-second) and never build Rust or the
# `harn` binary, so a trivial failure (a stray markdown heading, a keyword
# mirror drift) aborts the commit in seconds instead of after the
# multi-minute clippy build that used to run first. None of them mutate the
# tree, so they are safe to run against the originally-staged file set before
# the formatters re-stage anything.
#
# Note on markdown vs. the generated language spec: `docs/src/language-spec.md`
# is `spec/HARN_SPEC.md` behind a transparent HTML-comment banner, and the
# source is markdown-linted here too, so linting the mirror before the
# Phase-2 sync step yields the same result the sync would produce.
# ---------------------------------------------------------------------------

if hook_paths_match "$changed" "$HOOK_RATCHET_PATTERN"; then
  echo "=== Pre-commit: checking CI ratchets ==="
  if [ "$prompt_prose_done" -eq 0 ]; then
    make lint-no-rust-prompt-prose
    prompt_prose_done=1
  fi
  make lint-no-xfail-regression
  echo "    CI ratchets OK."
else
  echo "=== Pre-commit: skipping CI ratchets (no protected paths changed) ==="
fi

# Pure-Python, sub-second, no harn build — cheap enough to check (not just
# at push) so drift in the lexer<->tree-sitter keyword mirror or the
# generated-artifact registry is caught at the moment it's introduced. We
# check rather than auto-regenerate so the hand-curated grouping/comments
# in keywords.js survive; the hint points at `make gen-tree-sitter-keywords`.
if hook_paths_match "$changed" "$HOOK_TREESITTER_PATTERN"; then
  echo "=== Pre-commit: checking tree-sitter keyword mirror ==="
  make -s check-tree-sitter-keywords
  echo "    Tree-sitter keywords OK."
else
  echo "=== Pre-commit: skipping tree-sitter keyword check (no lexer/grammar changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_GENREGISTRY_PATTERN"; then
  echo "=== Pre-commit: checking generated-artifact registry ==="
  make -s check-generated-registry
  echo "    Generated-artifact registry OK."
else
  echo "=== Pre-commit: skipping generated-artifact registry check (no registry/Makefile/workflow/hook changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_MARKDOWN_PATTERN" && command -v npx >/dev/null 2>&1; then
  echo "=== Pre-commit: checking markdown ==="
  make lint-md
  echo "    Markdown OK."
elif hook_paths_match "$changed" "$HOOK_MARKDOWN_PATTERN"; then
  echo "=== Pre-commit: skipping markdown lint (npx not found) ==="
else
  echo "=== Pre-commit: skipping markdown lint (no markdown changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_ACTIONS_PATTERN"; then
  echo "=== Pre-commit: checking GitHub Actions workflows ==="
  make lint-actions
  echo "    GitHub Actions workflows OK."
else
  echo "=== Pre-commit: skipping GitHub Actions lint (no workflow/hook changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_PORTAL_PATTERN" && command -v npm >/dev/null 2>&1; then
  echo "=== Pre-commit: linting portal frontend ==="
  ./scripts/ensure_portal_deps.sh
  npm run portal:lint
  echo "    Portal lint OK."
elif hook_paths_match "$changed" "$HOOK_PORTAL_PATTERN"; then
  echo "=== Pre-commit: skipping portal lint (npm not found) ==="
else
  echo "=== Pre-commit: skipping portal lint (no portal changes) ==="
fi

# ---------------------------------------------------------------------------
# Phase 2 — formatters, clippy, and generated-file sync (the expensive,
# tree-mutating steps). Reached only once the fast validators above pass.
# ---------------------------------------------------------------------------

if hook_paths_match "$changed" "$HOOK_RUST_PATTERN"; then
  echo "=== Pre-commit: formatting Rust ==="
  cargo fmt --all
  if ! git diff --quiet; then
    git diff --name-only -z | xargs -0 git add
  fi
  echo "    Rust formatting OK."
else
  echo "=== Pre-commit: skipping Rust formatting (no Rust/Cargo changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_HARN_PATTERN"; then
  hook_write_harn_format_files "$changed" "$harn_format_files"
  if [ -s "$harn_format_files" ]; then
    echo "=== Pre-commit: formatting staged Harn files ==="
    # Build + re-codesign once before xargs so the freshly-relinked
    # binary keeps its ad-hoc signature; otherwise Gatekeeper shows a
    # multi-second "Verifying 'harn'..." popup on every invocation.
    harn_bin=$(hook_ensure_harn)
    xargs -0 "$harn_bin" fmt < "$harn_format_files"
    xargs -0 git add -- < "$harn_format_files"
    echo "    Harn formatting OK."
  else
    echo "=== Pre-commit: skipping Harn formatting (no format-supported Harn files) ==="
  fi

  hook_write_staged_files "$changed"
  hook_write_harn_lint_files "$changed" "$harn_lint_files"
  if [ -s "$harn_lint_files" ]; then
    echo "=== Pre-commit: linting staged Harn files ==="
    harn_bin=$(hook_ensure_harn)
    xargs -0 "$harn_bin" lint --fix < "$harn_lint_files"
    xargs -0 "$harn_bin" fmt < "$harn_lint_files"
    xargs -0 git add -- < "$harn_lint_files"
    echo "    Harn lint OK."
  else
    echo "=== Pre-commit: skipping Harn lint (no lint-supported Harn files) ==="
  fi
else
  echo "=== Pre-commit: skipping Harn formatting/lint (no Harn changes) ==="
fi

hook_write_staged_files "$changed"

if hook_paths_match "$changed" "$HOOK_RUST_PATTERN"; then
  # Match the intent of 63b3ebdc: pre-commit lints the *library* surface
  # of changed packages only — fast feedback for the dev loop. Workspace
  # `--all-targets` clippy is still enforced by the `Rust lint` CI job
  # and by `make lint` for ad-hoc local runs; pre-push catches
  # test-target compile errors via `cargo check --tests`.
  if [ "$prompt_prose_done" -eq 0 ]; then
    echo "=== Pre-commit: lint-no-rust-prompt-prose ==="
    make lint-no-rust-prompt-prose
    prompt_prose_done=1
  fi

  hook_write_changed_cargo_packages "$changed" "$changed_packages"
  if hook_paths_match "$changed" '(^Cargo\.toml$|^Cargo\.lock$|^Makefile$)'; then
    echo "=== Pre-commit: running clippy on full workspace (workspace-scope change) ==="
    cargo clippy --workspace -- -D warnings
  elif [ -s "$changed_packages" ]; then
    package_flags=""
    while IFS= read -r package; do
      package_flags="$package_flags -p $package"
    done < "$changed_packages"
    # shellcheck disable=SC2086  # intentional word splitting on -p flags
    echo "=== Pre-commit: running clippy on changed packages ($(tr '\n' ' ' < "$changed_packages")) ==="
    cargo clippy $package_flags -- -D warnings
  else
    # `.rs` outside any `crates/` package (eg. top-level build script) —
    # fall back to a workspace check rather than skip silently.
    echo "=== Pre-commit: running clippy on full workspace (no crate package matched) ==="
    cargo clippy --workspace -- -D warnings
  fi
  echo "    Rust lint OK."
else
  echo "=== Pre-commit: skipping clippy (no Rust/Cargo changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_HIGHLIGHT_PATTERN"; then
  echo "=== Pre-commit: syncing generated highlight keywords ==="
  make gen-highlight
  git add docs/theme/harn-keywords.js
  echo "    Highlight keywords OK."
else
  echo "=== Pre-commit: skipping highlight sync (no lexer/stdlib changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_LANGSPEC_PATTERN"; then
  # spec/HARN_SPEC.md is the authoring source; docs/src/language-spec.md
  # is a `cat`-with-header mirror. Regen + restage is sub-second, so do
  # the consistency fixup inline rather than failing the commit and
  # leaving the human to run the script. Mirrors the highlight stanza.
  echo "=== Pre-commit: syncing generated language spec ==="
  "$(hook_ensure_harn)" run scripts/sync_language_spec.harn
  git add docs/src/language-spec.md
  echo "    Language spec OK."
else
  echo "=== Pre-commit: skipping language spec sync (no spec changes) ==="
fi

echo "=== All pre-commit checks passed. ==="
