#!/bin/bash
# pre-push: block push if lint fails.
set -uo pipefail

if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then
  exit 0
fi

# Inner (self-wrapped) push: lint already ran in the outer invocation and the
# real push is happening now — let it through before doing any work again.
# The marker is an env var scoped to this process subtree, so it cannot leak
# into a later unrelated `git push` (no stale-flag risk).
if [ -n "${GIT_POSTPUSH:-}" ]; then
  exit 0
fi

cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)"

if [ -f ".venv/bin/activate" ]; then
  source .venv/bin/activate
fi

errors=""

# Python lint — apple
if [ -d "apple" ] && command -v ruff &>/dev/null; then
  py_out=$(ruff check apple/ 2>&1)
  if [ $? -ne 0 ]; then
    errors+="=== ruff check apple/ ===\n${py_out}\n\n"
  fi
fi

# Python lint — pine-python
if [ -d "pine-python" ] && command -v ruff &>/dev/null; then
  pyp_out=$(ruff check pine-python/ 2>&1)
  if [ $? -ne 0 ]; then
    errors+="=== ruff check pine-python/ ===\n${pyp_out}\n\n"
  fi
fi

# Go lint
if [ -f "pine-go/go.mod" ] && command -v golangci-lint &>/dev/null; then
  go_out=$(cd pine-go && golangci-lint run ./... 2>&1)
  if [ $? -ne 0 ]; then
    errors+="=== golangci-lint run ./... ===\n${go_out}\n\n"
  fi
fi

# Java lint
if [ -f "pine-java/pom.xml" ] && command -v mvn &>/dev/null; then
  java_out=$(cd pine-java && mvn checkstyle:check -B -q 2>&1)
  if [ $? -ne 0 ]; then
    errors+="=== mvn checkstyle:check ===\n${java_out}\n\n"
  fi
fi

# C++ format
if [ -d "pine-cpp" ] && command -v clang-format &>/dev/null; then
  cpp_out=$(find pine-cpp -type d \( -name 'build' -o -name 'build-*' \) -prune \
    -o -type f \( -name '*.cpp' -o -name '*.hpp' \) -print0 \
    | xargs -0 clang-format --dry-run --Werror 2>&1)
  if [ $? -ne 0 ]; then
    errors+="=== clang-format ===\n${cpp_out}\n\n"
  fi
fi

if [ -n "$errors" ]; then
  echo -e "$errors" >&2
  exit 1
fi

echo "pre-push: all lint checks passed."

# ── post-push CI watch (self-wrapping) ──────────────────────────────────────
# git has no native post-push hook. We emulate one: on the OUTER push the
# GIT_POSTPUSH marker is absent, so we perform the real push ourselves (INNER),
# which re-enters this hook WITH the marker and is let through immediately at
# the top. Control then returns here and we block on the remote CI.
#
# Read the refspecs git handed us on stdin (one per line:
#   <local ref> <local sha> <remote ref> <remote sha>)
# and mirror them EXACTLY on the inner push, instead of assuming a single
# current-branch push. This keeps force-pushes, tag pushes, ref deletions
# (`git push origin :foo`) and multi-ref pushes (`git push --all`) working.
zero="0000000000000000000000000000000000000000"
refspecs=()
have_update=0          # at least one non-delete ref → CI watch is meaningful
force_lease_args=()    # one --force-with-lease=ref:sha per non-fast-forward ref

while read -r local_ref local_sha remote_ref remote_sha; do
  [ -z "${remote_ref:-}" ] && continue
  if [ "$local_sha" = "$zero" ]; then
    # Deletion: the refspec is ":<remote_ref>" (empty source).
    refspecs+=(":${remote_ref}")
  else
    refspecs+=("${local_ref}:${remote_ref}")
    have_update=1
    # Non-fast-forward (remote has commits not contained in local) ⇒ a plain
    # push would be rejected, so the user must have used --force. Mirror that
    # with --force-with-lease so the inner push isn't rejected either.
    #
    # Bare `--force-with-lease` computes the lease from refs/remotes/origin/*
    # (the local tracking ref), which can be stale if the developer hasn't
    # fetched recently and silently mismatch the real remote. The remote_sha
    # we got on stdin is git's *current* view of the actual remote sha (freshly
    # negotiated as part of this push), so pin the lease to it explicitly per
    # refspec — exact, no fetch-or-die requirement.
    if [ "$remote_sha" != "$zero" ] \
       && ! git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
      force_lease_args+=("--force-with-lease=${remote_ref}:${remote_sha}")
    fi
  fi
done

# Nothing on stdin → let the outer push proceed normally; nothing to wrap.
if [ "${#refspecs[@]}" -eq 0 ]; then
  exit 0
fi

# Deletions only → no CI to watch. Let the OUTER push perform the delete
# normally (no self-wrap needed) and return.
if [ "$have_update" -eq 0 ]; then
  exit 0
fi

remote_name="${1:-origin}"

echo "post-push: performing real push to '${remote_name}' (${refspecs[*]})..." >&2
# `${arr[@]+"${arr[@]}"}` is the parameter-expansion guard form: empty under
# `set -u` on bash 3.2 (the default /bin/bash on macOS), equivalent to
# `"${arr[@]}"` on bash >=4.4. A bare expansion of an empty array trips
# "unbound variable" on bash 3.2 and aborts the hook on every non-force push.
GIT_POSTPUSH=1 git push ${force_lease_args[@]+"${force_lease_args[@]}"} "${remote_name}" "${refspecs[@]}"
push_rc=$?
if [ "$push_rc" -ne 0 ]; then
  echo "post-push: inner push failed (rc=${push_rc}) — not watching CI." >&2
  exit "$push_rc"
fi

# The inner push already updated the remote. The OUTER push that invoked this
# hook is now guaranteed to fail — either git's atomic ref protection rejects it
# ("remote rejected / cannot lock ref"), or the connection drops during the long
# CI watch (SIGPIPE / "connection closed"). Both are EXPECTED and HARMLESS: the
# push already succeeded via the inner push. Consequently the OUTER push's exit
# code does NOT reflect CI status — read the ✓/✗ report below for the verdict.
echo "post-push: ✔ push succeeded (remote updated by inner push)." >&2
echo "post-push:   any outer 'remote rejected' or 'connection closed' line below is expected — ignore it." >&2
echo "post-push:   the real verdict is the ✓/✗ report once CI finishes." >&2

# The inner push has already updated the remote. Block on CI, then report.
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
if [ -x "${repo_root}/scripts/check-pr-ci.sh" ]; then
  "${repo_root}/scripts/check-pr-ci.sh"
  ci_rc=$?
  # rc 2  = no PR / gh missing → non-fatal, fall through.
  # rc 1  = CI failed          → surface as failure.
  # rc 75 = CI passed but new review activity / unresolved threads remain →
  #         surface as failure too. Returning 0 here would let the outer push
  #         exit 0 and any tooling that only inspects exit status (agent
  #         loops, CI driver scripts) would silently miss the
  #         "address review feedback" stderr block.
  if [ "$ci_rc" -eq 1 ] || [ "$ci_rc" -eq 75 ]; then
    exit 1
  fi
  # rc 0 path: check-pr-ci.sh has already printed either the "nothing more to
  # do" terminator OR a Claude-targeted instruction block describing how to
  # fetch review comments and loop until terminal. Nothing to add here.
fi

# Reaching here means the real push already succeeded (inner). Returning 0 makes
# the OUTER push a no-op ("Everything up-to-date") — it does NOT push again.
exit 0
