#!/usr/bin/env bash
#
# regista pre-push publication guard (WI-019).
#
# Two checks the other hooks cannot make, because both need to know WHERE the
# push is going:
#
#   1. Plumbing (scripts/check_publication_plumbing.py) — the push URL's owner,
#      the author identity on every commit being pushed, and the repository's
#      declared visibility, all read from the tracked publication.toml. This is
#      the accident class the content denylist never covered: right bytes, wrong
#      destination.
#   2. Commit messages in the push range -- the last chance to catch an
#      identifier in a message before it is published. New branches and
#      force-pushes scan every commit not on any ref of the remote; deletions
#      publish nothing. (A whole-range content/identity scan is in review
#      separately.)
#
# git passes "<remote-name> <remote-url>" as argv and the ref updates on stdin as
# "<local-ref> <local-sha> <remote-ref> <remote-sha>".
#
# Activate (once per clone):   scripts/install-git-hooks.sh
#
# No denylist: the gate decides from publication.toml. A repo declared PUBLIC is
# refused -- on a public repo a push publishes, so this hook is the last
# prevention point and CI only detects after the fact. A private-until-review
# repo stays a no-op. The ONLY bypass is the explicit GATE_ALLOW_NO_DENYLIST=1,
# which prints a warning on every use.
#
# HONEST SCOPE: layer 3/4 accident prevention, not a security boundary — a hook
# is bypassable with --no-verify and absent until installed.
set -euo pipefail

remote_name="${1:-origin}"
remote_url="${2:-}"
repo_root="$(git rev-parse --show-toplevel)"

python_bin="$repo_root/.venv/bin/python"
if [ ! -x "$python_bin" ]; then
  python_bin="$(command -v python3 || command -v python || true)"
fi
if [ -z "${python_bin:-}" ] || [ ! -x "$python_bin" ]; then
  echo "regista pre-push: FATAL — no Python interpreter found." >&2
  echo "  Checked: $repo_root/.venv/bin/python, python3, python" >&2
  echo "  Install Python or create a venv (uv venv && uv sync)." >&2
  exit 1
fi

# Resolve the denylist once (same order as pre-commit; see that hook).
#
# The "agent-suite" in the ~/.config path below is the shared config DIRECTORY --
# one canonical denylist for every repo -- not a project name. A templatising
# pass that treated it as one rewrote it to ~/.config/<repo>/, which exists
# nowhere, and every hook then announced itself INACTIVE while looking installed.
if [ -z "${REGISTA_FORBIDDEN_IDENTIFIERS:-}" ]; then
  for candidate in \
    "$repo_root/.identifiers-denylist.local" \
    "${HOME:-}/.config/agent-suite/forbidden-identifiers"; do
    if [ -f "$candidate" ]; then
      REGISTA_FORBIDDEN_IDENTIFIERS="$(cat "$candidate")"
      export REGISTA_FORBIDDEN_IDENTIFIERS
      break
    fi
  done
fi
no_denylist=0
denylist="${REGISTA_FORBIDDEN_IDENTIFIERS:-}"
if [ -z "${denylist//[[:space:]]/}" ]; then
  no_denylist=1
  if [ "${GATE_ALLOW_NO_DENYLIST:-}" = "1" ]; then
    echo "regista pre-push: WARNING: identifier gate BYPASSED (GATE_ALLOW_NO_DENYLIST=1) -- no denylist, the outgoing range was NOT scanned." >&2
  fi
fi

# Decide the unusable-denylist case once, before reading the ref updates: a
# deletion-only push runs no range scan below, and a public repo whose denylist
# is missing OR unusable (e.g. only too-short entries) must still be refused.
# (--message-file on an empty file: the gate resolves the denylist first, so it
# exits 1 on a public declaration without a usable one, 0 otherwise.)
if [ "${GATE_ALLOW_NO_DENYLIST:-}" = "1" ]; then
  # Warn on EVERY use of the opt-out, not only when no denylist text was found:
  # a denylist of unusable entries is no denylist, and the precheck is skipped.
  [ "$no_denylist" -eq 1 ] || echo "regista pre-push: WARNING: GATE_ALLOW_NO_DENYLIST=1 is set -- the identifier gate is BYPASSED for this push if the denylist is unusable." >&2
else
  if ! "$python_bin" "$repo_root/scripts/check_committed_identifiers.py" \
      --message-file /dev/null; then
    echo "regista pre-push: fix: put the denylist in ~/.config/agent-suite/forbidden-identifiers (or export REGISTA_FORBIDDEN_IDENTIFIERS); to bypass once, GATE_ALLOW_NO_DENYLIST=1 (warned)." >&2
    exit 1
  fi
fi

zero_sha="0000000000000000000000000000000000000000"
status=0

# remote_ref is named but unused: git's stdin contract is four fields, and naming
# all four documents the format at the point of use. Silenced explicitly rather
# than dropped, so the contract stays readable.
# shellcheck disable=SC2034
while read -r local_ref local_sha remote_ref remote_sha; do
  [ -z "${local_ref:-}" ] && continue
  # A zero local sha is a branch deletion: nothing is being published.
  [ "$local_sha" = "$zero_sha" ] && continue

  # Three cases, and the third one matters more than it looks. After a history
  # rewrite (git-filter-repo, rebase, amend) the remote's sha is no longer
  # reachable locally, so "$remote_sha..$local_sha" is an INVALID range — the
  # scanners would fail closed and refuse every force-push, permanently. A
  # publication scrub is exactly when a force-push must remain possible, so an
  # unreachable remote sha is treated like a new branch: scan the whole set being
  # published rather than a diff against a commit that no longer exists.
  if [ "$remote_sha" = "$zero_sha" ] \
     || ! git cat-file -e "${remote_sha}^{commit}" 2>/dev/null; then
    # New branch or rewritten history. Build the exclusion range honestly:
    # --remotes=<name> is only valid when <name> is a configured remote (git
    # push <URL> passes the URL as argv[1], which is NOT a remote name and
    # would make --remotes= match nothing). When the name is not a configured
    # remote, conservatively scan all commits reachable from local_sha.
    if git remote get-url "$remote_name" >/dev/null 2>&1; then
      rev_range="$local_sha --not --remotes=$remote_name"
    else
      rev_range="$local_sha"
    fi
  else
    rev_range="$remote_sha..$local_sha"
  fi

  if ! "$python_bin" "$repo_root/scripts/check_publication_plumbing.py" \
      --remote-url "$remote_url" --rev-range "$rev_range" --repo-root "$repo_root"; then
    status=1
  fi

  if [ "$no_denylist" -eq 1 ] && [ "${GATE_ALLOW_NO_DENYLIST:-}" = "1" ]; then
    continue
  fi
  if ! "$python_bin" "$repo_root/scripts/check_committed_identifiers.py" \
      --rev-range "$rev_range"; then
    status=1
    if [ "$no_denylist" -eq 1 ]; then
      echo "regista pre-push: fix: put the denylist in ~/.config/agent-suite/forbidden-identifiers (or export REGISTA_FORBIDDEN_IDENTIFIERS); to bypass once, GATE_ALLOW_NO_DENYLIST=1 (warned)." >&2
    fi
  fi
done

exit "$status"
