#!/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_push_files "$changed"
hook_disable_cargo_incremental_if_release_bump "push $(hook_push_base)"

# Merge-queue guard. GitHub does not reject pushes to a PR that's already
# in the merge queue — it just keeps using the snapshot it took when the
# PR was enqueued. Any commit pushed after that point is silently dropped
# from the merged state. Bypass with `--no-verify` if you mean it. Kept
# first because it's a cheap correctness gate: there's no point running
# any local check if the push itself would be silently dropped.
if command -v gh >/dev/null 2>&1; then
  branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
  if [ -n "$branch" ] && [ "$branch" != "HEAD" ] && [ "$branch" != "main" ]; then
    queued_branches=$(gh api graphql -f query='
      query($owner: String!, $name: String!) {
        repository(owner: $owner, name: $name) {
          mergeQueue(branch: "main") {
            entries(first: 100) {
              nodes { pullRequest { headRefName } }
            }
          }
        }
      }' -f owner=burin-labs -f name=harn \
      --jq '.data.repository.mergeQueue.entries.nodes[].pullRequest.headRefName' \
      2>/dev/null || true)
    if [ -n "$queued_branches" ] && echo "$queued_branches" | grep -Fxq "$branch"; then
      echo ""
      echo "error: branch '$branch' is currently in the merge queue."
      echo ""
      echo "  GitHub snapshotted your PR when it entered the queue. Pushing"
      echo "  more commits now silently drops them from the merged state."
      echo ""
      echo "  Fix:"
      echo "    1. gh pr merge $branch --disable-auto      # dequeue"
      echo "       (if --disable-auto says 'already queued',"
      echo "        use the GraphQL dequeuePullRequest mutation)"
      echo "    2. git push                                  # push again"
      echo "    3. gh pr merge $branch --auto                # re-enqueue"
      echo ""
      echo "  Or bypass: git push --no-verify"
      exit 1
    fi
  fi
fi

# ---------------------------------------------------------------------------
# Phase 1 — fast, read-only validators (fail-fast).
#
# Cheap (python / npx / actionlint, sub-second) and no Rust or `harn` build,
# so a trivial drift fails the push in seconds instead of after the
# `cargo check --tests` / generated-mirror rebuilds below.
# ---------------------------------------------------------------------------

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

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

if hook_paths_match "$changed" "$HOOK_TREESITTER_PATTERN"; then
  echo "=== Pre-push: checking tree-sitter keyword mirror ==="
  make -s check-tree-sitter-keywords
  echo "    Tree-sitter keywords OK."
else
  echo "=== Pre-push: skipping tree-sitter keyword check (no lexer/grammar changes) ==="
fi

# Meta-guard: keep scripts/generated_artifacts.toml in agreement with the
# Makefile `all:` recipe and the CI workflows, so a new gen/check pair
# can't silently skip CI. Pure-Python; no harn build.
if hook_paths_match "$changed" "$HOOK_GENREGISTRY_PATTERN"; then
  echo "=== Pre-push: checking generated-artifact registry ==="
  make -s check-generated-registry
  echo "    Generated-artifact registry OK."
else
  echo "=== Pre-push: skipping generated-artifact registry check (no registry/Makefile/workflow/hook changes) ==="
fi

# Block retroactive edits to released CHANGELOG sections. New entries
# belong under `## Unreleased` (or the in-progress next-version heading
# in a release PR), not under a section that already shipped under a tag.
if hook_paths_match "$changed" '^CHANGELOG\.md$'; then
  echo "=== Pre-push: checking CHANGELOG for retroactive edits ==="
  if ! cargo run --quiet --bin harn -- run scripts/check_changelog_no_retroactive_edits.harn; then
    echo "" >&2
    echo "  Bypass for genuine fix-ups (typos, broken links):" >&2
    echo "    ALLOW_CHANGELOG_RETROACTIVE_EDIT=1 git push" >&2
    exit 1
  fi
else
  echo "=== Pre-push: skipping CHANGELOG retroactive-edit check (no CHANGELOG.md changes) ==="
fi

# ---------------------------------------------------------------------------
# Phase 2 — Rust/Harn compilation and generated-mirror rebuilds (the
# expensive steps). Reached only once the fast validators above pass.
# ---------------------------------------------------------------------------

echo "=== Pre-push: running targeted local checks ==="
if [ "${HARN_PREPUSH_FULL_TESTS:-0}" = "1" ] && hook_paths_match "$changed" "$HOOK_TEST_PATTERN"; then
  make test
elif hook_paths_match "$changed" '(^Cargo\.toml$|^Cargo\.lock$|^Makefile$|^\.config/nextest\.toml$)'; then
  cargo check --workspace --tests
elif hook_paths_match "$changed" "$HOOK_TEST_PATTERN"; then
  hook_write_changed_cargo_packages "$changed" "$changed_packages"
  if [ -s "$changed_packages" ]; then
    # Collect all changed packages into a single `cargo check` so cargo
    # only resolves the dep graph and re-evaluates fingerprints once,
    # instead of paying that overhead N times in a shell loop.
    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
    cargo check $package_flags --tests
  else
    echo "=== Pre-push: skipping Cargo check (no changed crate packages) ==="
  fi
else
  echo "=== Pre-push: skipping Cargo check (no Rust/Harn code changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_HARN_PATTERN"; then
  hook_write_harn_format_files "$changed" "$harn_format_files"
  hook_write_harn_lint_files "$changed" "$harn_lint_files"
  if [ -s "$harn_format_files" ] || [ -s "$harn_lint_files" ]; then
    # Build + re-codesign once so the freshly-relinked binary keeps its
    # ad-hoc signature across both xargs batches; without this, every
    # cargo-run invocation would strip the signature and trip
    # Gatekeeper's "Verifying 'harn'..." popup.
    harn_bin=$(hook_ensure_harn)
  fi
  if [ -s "$harn_format_files" ]; then
    echo "=== Pre-push: checking affected Harn formatting ==="
    xargs -0 "$harn_bin" fmt --check < "$harn_format_files"
  else
    echo "=== Pre-push: skipping Harn format check (no format-supported Harn files) ==="
  fi

  if [ -s "$harn_lint_files" ]; then
    echo "=== Pre-push: linting affected Harn files ==="
    xargs -0 "$harn_bin" lint < "$harn_lint_files"
  else
    echo "=== Pre-push: skipping Harn lint (no lint-supported Harn files) ==="
  fi
else
  echo "=== Pre-push: skipping Harn format/lint (no Harn changes) ==="
fi

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

# Generated-mirror drift guard. The `harn-generated` merge driver
# silently keeps the current side during merge/rebase (see
# scripts/configure_merge_drivers.sh) and the post-rewrite hook
# auto-folds the regen into single-commit rebases, but neither covers
# every case (multi-commit rebases, hooks disabled, etc.). Run the
# same check CI runs so any drift fails the push instead of the PR.
if hook_paths_match "$changed" "$HOOK_LANGSPEC_PATTERN"; then
  echo "=== Pre-push: checking docs/src/language-spec.md mirror ==="
  if ! make -s check-language-spec >/tmp/harn-prepush-langspec.log 2>&1; then
    cat /tmp/harn-prepush-langspec.log >&2
    rm -f /tmp/harn-prepush-langspec.log
    echo "" >&2
    echo "  This can happen after rebasing onto a branch that also touched" >&2
    echo "  the spec — the merge driver intentionally keeps the current" >&2
    echo "  mirror without regenerating from the merged source." >&2
    echo "" >&2
    echo "  Fix:" >&2
    echo "    make sync-language-spec" >&2
    echo "    git add docs/src/language-spec.md" >&2
    echo "    git commit --amend --no-edit       # fold into the last commit" >&2
    echo "    git push --force-with-lease         # if you've already pushed" >&2
    echo "" >&2
    exit 1
  fi
  rm -f /tmp/harn-prepush-langspec.log
  echo "    Language spec mirror OK."
else
  echo "=== Pre-push: skipping language spec check (no spec changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_HIGHLIGHT_PATTERN"; then
  echo "=== Pre-push: checking generated highlight keywords ==="
  make -s check-highlight
  echo "    Highlight keywords OK."
else
  echo "=== Pre-push: skipping highlight check (no lexer/stdlib changes) ==="
fi

if hook_paths_match "$changed" "$HOOK_DIAGCATALOG_PATTERN"; then
  echo "=== Pre-push: checking diagnostic-code catalog ==="
  if ! make -s check-diagnostics-catalog >/tmp/harn-prepush-diagcatalog.log 2>&1; then
    cat /tmp/harn-prepush-diagcatalog.log >&2
    rm -f /tmp/harn-prepush-diagcatalog.log
    echo "" >&2
    echo "  Fix:" >&2
    echo "    make sync-diagnostics-catalog" >&2
    echo "    git add docs/src/diagnostics.md docs/diagnostics-catalog.json" >&2
    echo "    git commit" >&2
    echo "" >&2
    exit 1
  fi
  rm -f /tmp/harn-prepush-diagcatalog.log
  echo "    Diagnostic-code catalog OK."
else
  echo "=== Pre-push: skipping diagnostic catalog check (no registry changes) ==="
fi

echo "=== Pre-push checks passed. ==="
