#!/usr/bin/env bash
# Thin dispatcher — the actual PORT-DEBT direct-main leg is
# scripts/port-debt-direct-main.sh (port-debt-scanner-missed-rows, 2026-08-26),
# shared verbatim with .githooks/post-commit: a commit made straight on main
# and a merge made straight on main are the SAME hazard, checked by the SAME
# predicate. See that script's docstring for the full rationale, including
# why BOTH hooks exist (`git merge` never invokes `post-commit` — githooks(5)
# — so a post-commit-only design would miss a hand `git merge` run straight
# on main no matter how its diff was computed).
#
# git passes ONE argument: 1 if this was `git merge --squash`, 0 otherwise. A
# squash merge stages changes but makes NO commit (the user commits
# separately, which then fires post-commit normally) — HEAD has not moved,
# so there is nothing new to scan and running anyway would re-scan whatever
# commit HEAD already sat on, misattributing its debt to "just now".
set -uo pipefail
[ "${1:-0}" = "1" ] && exit 0
TOPLEVEL="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0

# ---------------------------------------------------------------------------
# DOC-INDEX DRIFT — the AUTOMATIC-MERGE half (2026-09-09).
#
# THE "HOOK AUTHORING RULE" NAMED BELOW ("A HOOK IS A GIT SUBPROCESS...") IS
# NOW A STANDING BLOCK IN `.githooks/pre-commit`, right after that hook's
# gitleaks arm — read it first; what follows here is the second worked
# example, independently re-verified rather than assumed to carry over.
#
# WHY HERE. `.githooks/pre-commit`'s DOC-INDEX leg (2026-09-05/06) closes the
# AUTHORING-TIME half of this drift class in full, and its own comment states
# the remainder plainly: "a clean `git merge` never runs pre-commit at all...
# an automatic [merge] gets nothing... a `post-merge` warning would be a
# separate change to a separate hook." This is that change. Escalated by lane
# doc-index-check-not-in-precommit §7.
#
# THE SHAPE IS WARN, NEVER BLOCK — a constraint, not a preference. By the
# time post-merge runs, `git merge` has ALREADY written the merge commit;
# nothing this leg does can un-merge it. So every branch below prints and
# falls through — nothing in this leg calls `exit` with a non-zero status,
# and a defect in it must never stop the port-debt leg that follows.
#
# MERGE_HEAD IS STILL SET HERE FOR A THREE-WAY CLEAN MERGE — MEASURED, NOT
# ASSUMED. `git`'s `finish_automerge()` runs the post-merge hook and only
# THEN clears MERGE_HEAD: verified against a throwaway fixture (a post-merge
# hook printing `git rev-parse -q --verify MERGE_HEAD` reports the parent sha
# on a divergent-but-clean 3-way merge, and reports nothing on a
# fast-forward). `scripts/generate-doc-index.py` REFUSES unconditionally
# whenever MERGE_HEAD is set, by design and with no escape hatch
# (doc-index-midmerge-duplicates), so it cannot be called directly against
# the real checkout on the 3-way path — which is also the shape a
# `git merge --no-ff -m` (a plain merge commit, no separate `git commit`
# step) always produces, the exact invocation the merge-window driver uses.
# Two branches, therefore:
#
#   * MERGE_HEAD unset (fast-forward): the generator runs directly against
#     the real checkout — `--root "$TOPLEVEL"` IS the repo the hook's own
#     environment already describes, so no rebinding is needed.
#   * MERGE_HEAD set: HEAD is already the completed merge commit, so
#     `git archive HEAD` materializes it — a REAL commit, not a staged tree,
#     so `git write-tree` is not needed the way the pre-commit leg needs it —
#     into a throwaway checkout with no MERGE_HEAD of its own.
#
# A HOOK IS A GIT SUBPROCESS, AND ITS ENVIRONMENT IS GIT'S — the pre-commit
# leg's lesson, re-verified here rather than assumed to carry over: MEASURED
# against a throwaway fixture, a post-merge hook fired from a LINKED WORKTREE
# sees `GIT_DIR=<repo>/.git/worktrees/<name>` (absolute), where a plain
# checkout sees none. `-C <dir>` does not override an inherited `GIT_DIR`
# with no work-tree setting — cwd becomes the work tree regardless of `-C`'s
# target — so every call aimed at the MATERIALIZED EXTRACT clears the repo
# binding first, from a list derived from `git rev-parse --local-env-vars`
# (never copied here — a list git extends and this file does not is a silent
# fork). The DIRECT branch above (root = the real repo) deliberately KEEPS
# the binding — it is the correct, already-consistent one there.
#
# rc is NEVER checked with `if ! cmd` — CLAUDE.md's shell-boundary rule: that
# construct records 0 for every failure. Every rc here is taken on its own
# line.
dgi_check() {
  local toplevel="$1" gen="$2"
  local merge_head pathfile paths=() tmp arc envfile unset_vars=() v run outfile out rc
  merge_head="$(git -C "$toplevel" rev-parse -q --verify MERGE_HEAD 2>/dev/null)"
  if [ -z "$merge_head" ]; then
    out="$(python3 "$gen" --check --root "$toplevel" 2>&1)"
    rc=$?
  else
    pathfile="$(mktemp)"
    git -C "$toplevel" ls-files -z -- '*.md' 'scripts/generate-doc-index.py' > "$pathfile" 2>/dev/null
    while IFS= read -r -d '' p; do [ -n "$p" ] && paths+=("$p"); done < "$pathfile"
    rm -f "$pathfile"
    if [ "${#paths[@]}" -lt 2 ]; then
      echo "post-merge: doc-index check — HEAD holds ${#paths[@]} of its input path(s); expected at least 2 (the guide and the generator). Not grading an empty estate." >&2
      return 0
    fi
    tmp="$(mktemp -d)"
    git -C "$toplevel" archive --format=tar HEAD -- "${paths[@]}" 2>/dev/null | tar -x -C "$tmp" 2>/dev/null
    arc=$?
    if [ "$arc" -ne 0 ]; then
      echo "post-merge: doc-index check — could not materialize HEAD for the check (archive/extract rc=$arc); skipping (never blocks a completed merge)." >&2
      rm -rf "$tmp"
      return 0
    fi
    envfile="$(mktemp)"
    git rev-parse --local-env-vars > "$envfile" 2>/dev/null
    while IFS= read -r v; do [ -n "$v" ] && unset_vars+=(-u "$v"); done < "$envfile"
    rm -f "$envfile"
    if [ "${#unset_vars[@]}" -eq 0 ]; then
      echo "post-merge: doc-index check — could not determine the repo-binding env vars to clear; skipping rather than risking the real index." >&2
      rm -rf "$tmp"
      return 0
    fi
    env "${unset_vars[@]}" git -C "$tmp" init -q >/dev/null 2>&1
    env "${unset_vars[@]}" git -C "$tmp" add -A >/dev/null 2>&1
    run="$tmp/scripts/generate-doc-index.py"
    [ -f "$run" ] || run="$gen"
    outfile="$(mktemp)"
    env "${unset_vars[@]}" python3 "$run" --check --root "$tmp" > "$outfile" 2>&1
    rc=$?
    sed -i "s#$tmp/##g" "$outfile"
    out="$(cat "$outfile")"
    rm -f "$outfile"
    rm -rf "$tmp"
  fi
  if [ "$rc" -ne 0 ]; then
    {
      echo
      echo "post-merge: WARNING — docs/document-guide.md is STALE on HEAD ($(git -C "$toplevel" rev-parse --short HEAD 2>/dev/null)) — this merge landed a document-estate change without a regenerated guide. A completed merge is never blocked for it."
      printf '%s\n' "$out"
      echo
      echo "  Regenerate and commit the guide:"
      echo "    python3 scripts/generate-doc-index.py --write"
      echo "    or: make doc-index"
    } >&2
  fi
  return 0
}

DGI_GEN="$TOPLEVEL/scripts/generate-doc-index.py"
if [ ! -f "$DGI_GEN" ]; then
  echo "post-merge: SKIP — doc-index check (scripts/generate-doc-index.py not found in $TOPLEVEL; not this {{PROJECT_SLUG}} checkout)" >&2
elif ! command -v python3 >/dev/null 2>&1; then
  echo "post-merge: doc-index check — python3 NOT FOUND, skipping (this leg never blocks a completed merge)" >&2
else
  dgi_check "$TOPLEVEL" "$DGI_GEN"
fi

# REPO-COUPLING GUARD (precommit-hook-not-cwd-portable, 2026-08-26): see
# .githooks/post-commit's identical guard for the full rationale — this
# scan must fire ONLY in {{PROJECT_SLUG}}, and TOPLEVEL (the committing repo) is not
# {{PROJECT_SLUG}} for a fixture repo or install-hooks.sh's own self-test.
[ -f "$TOPLEVEL/scripts/port-debt-direct-main.sh" ] || {
  echo "post-merge: SKIP — port-debt scan (scripts/port-debt-direct-main.sh not found in $TOPLEVEL; not this {{PROJECT_SLUG}} checkout)" >&2
  exit 0
}
exec "$TOPLEVEL/scripts/port-debt-direct-main.sh" post-merge
