#!/usr/bin/env bash
#
# adr-reserve.sh — atomically reserve the next main-sequence ADR number.
#
# WHY THIS EXISTS
#   A contiguous ADR integer needs a single allocator, but git branches have
#   no shared allocator until they merge — which is why two parallel branches
#   that each invent "the next number" collide. This script makes `main` the
#   allocator and an atomic `git push` the lock:
#
#     1. read an append-only ledger (docs/adr/RESERVATIONS.log) + the existing
#        NNNN-*.md ADRs on main; next = highest + 1;
#     2. append "<NNNN> <slug> <date> <title>" to the ledger, commit, PUSH;
#     3. if the push is rejected because main moved (someone reserved first),
#        re-fetch, recompute, and retry.
#
#   A push to a ref is a compare-and-swap, so the remote `main` ref is the
#   mutex and step 3 is the retry-on-contention. The number is therefore
#   stable from the moment this returns — safe to cite in commit messages
#   (which are immutable) and in other ADRs from the very first commit.
#   (Decision recorded in the dev-workflow ADR.)
#
#   It runs from ANY branch / worktree / directory inside the repo and never
#   touches your working tree: the allocation happens in a throwaway worktree
#   on a detached origin/main.
#
# PREREQUISITE
#   You must be allowed to push directly to `main` (the repo owner is
#   whitelisted in the branch-protection rule for exactly this). Everything
#   substantive still goes through a PR; this one append-only ledger line is
#   the sole sanctioned direct push. If protection rejects the push, the
#   script aborts with a clear message rather than spinning.
#
# USAGE
#   scripts/adr-reserve.sh <slug> [title...]
#   scripts/adr-reserve.sh clause-concept-hints "Clause-concept hints"
#
# OUTPUT
#   The reserved zero-padded number on stdout (e.g. 0058); progress on stderr.

set -euo pipefail

log()  { printf '  reserve-adr: %s\n' "$*" >&2; }
die()  { printf 'reserve-adr: ERROR: %s\n' "$*" >&2; exit 1; }

REMOTE="origin"
BRANCH="main"
LEDGER="docs/adr/RESERVATIONS.log"
MAX_ATTEMPTS=20

[ $# -ge 1 ] || die "usage: adr-reserve.sh <slug> [title...]"
slug="$1"; shift
title="${*:-}"

# Slug must match the NNNN-<slug>.md filename convention.
[[ "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] \
  || die "slug must be lowercase-kebab-case (got: '$slug')"

root="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git repo"

log "allocating next ADR number for '$slug' against $REMOTE/$BRANCH"
git -C "$root" fetch --quiet "$REMOTE" "$BRANCH" || die "git fetch $REMOTE $BRANCH failed"

# Throwaway detached worktree on origin/main — isolated from your working tree.
tmp_base="$(mktemp -d)"
wt="$tmp_base/wt"
# shellcheck disable=SC2329  # invoked indirectly via `trap cleanup EXIT`
cleanup() {
  git -C "$root" worktree remove --force "$wt" >/dev/null 2>&1 || true
  rm -rf "$tmp_base"
}
trap cleanup EXIT
git -C "$root" worktree add --quiet --detach "$wt" "$REMOTE/$BRANCH" \
  || die "could not create temp worktree at $wt"

# Highest number across existing top-level ADR files and the ledger, + 1.
compute_next() {
  local max=0 n base
  shopt -s nullglob
  for f in "$wt"/docs/adr/[0-9][0-9][0-9][0-9]-*.md; do
    base="$(basename "$f")"; n=$((10#${base%%-*}))
    (( n > max )) && max=$n
  done
  if [ -f "$wt/$LEDGER" ]; then
    while read -r num _; do
      [[ "$num" =~ ^[0-9]{4}$ ]] || continue
      n=$((10#$num)); (( n > max )) && max=$n
    done < "$wt/$LEDGER"
  fi
  printf '%04d' $(( max + 1 ))
}

# Idempotence: if this slug is already reserved, report it and stop.
if [ -f "$wt/$LEDGER" ]; then
  existing="$(awk -v s="$slug" '$2==s {print $1; exit}' "$wt/$LEDGER" || true)"
  if [ -n "${existing:-}" ]; then
    log "'$slug' is already reserved as ADR $existing — nothing to do"
    echo "$existing"
    exit 0
  fi
fi

attempt=0
while (( attempt < MAX_ATTEMPTS )); do
  attempt=$(( attempt + 1 ))
  git -C "$wt" fetch --quiet "$REMOTE" "$BRANCH"
  git -C "$wt" reset --hard --quiet "$REMOTE/$BRANCH"

  num="$(compute_next)"

  # Dry run: report the number we WOULD reserve and stop (no commit/push).
  # Useful for a preview and for testing the read/allocation path.
  if [ -n "${ADR_RESERVE_DRY_RUN:-}" ]; then
    log "DRY RUN: would reserve ADR $num for '$slug' (no push)"
    echo "$num"
    exit 0
  fi

  mkdir -p "$wt/docs/adr"
  printf '%s  %s  %s  %s\n' "$num" "$slug" "$(date +%F)" "$title" >> "$wt/$LEDGER"
  git -C "$wt" add "$LEDGER"
  git -C "$wt" commit --quiet -m "docs(adr): reserve $num for $slug"

  log "attempt $attempt: claiming ADR $num …"
  if push_out="$(git -C "$wt" push "$REMOTE" "HEAD:$BRANCH" 2>&1)"; then
    log "reserved ADR $num for '$slug' (pushed to $REMOTE/$BRANCH)"
    echo "$num"
    exit 0
  fi

  # Distinguish a lost race (retryable) from a hard rejection (not).
  if grep -qiE 'non-fast-forward|fetch first|tip of your .* is behind|stale info' <<<"$push_out"; then
    log "race lost (main moved) — re-fetching and retrying"
    continue
  fi
  printf '%s\n' "$push_out" >&2
  die "push to $BRANCH was rejected and this is not a race — likely branch \
protection (is the owner whitelisted for direct pushes to $BRANCH?). Aborting."
done

die "gave up after $MAX_ATTEMPTS contended attempts — main is changing very fast?"
