#!/usr/bin/env bash
#
# allocate-id.sh — atomic, cross-worktree backlog-ID allocator (same machine).
#
# Problem it solves: several PRD/card sessions running in parallel on sibling
# worktrees all branch from the same trunk, so each computes the same
# "max(^id: PREFIX-) + 1" and they collide at rebase/merge time. A plain
# backlog scan cannot see IDs that are still in flight on an unmerged sibling
# worktree branch.
#
# Mechanism: every worktree shares the same main repo root (resolved from
# `git rev-parse --git-common-dir`), and `.worktrees/` lives there and is
# gitignored. We anchor a lock + a per-prefix high-water-mark file there, so a
# reservation is atomic across every worktree on this machine. The high-water
# bumped under the lock is the correctness mechanism; the max() against the real
# backlog makes it self-healing if the counter file is ever lost or bypassed.
#
# Scope: SAME MACHINE only (a shared local file). Cross-machine (separate cloud
# agents) is not covered here — the caller keeps the merge-base scan as a
# best-effort cross-machine guard and surfaces an [ID-RACE-RISK] note.
#
# Usage:
#   allocate-id.sh reserve <PREFIX> <slug>     # prints the reserved integer, zero-padded to 4
#   allocate-id.sh release <worktree-path>     # prunes that worktree's reservations (best-effort)
#
# Exit non-zero on hard failure (not a git repo, lock unobtainable) so the
# caller can fall back to its inline scan. All diagnostics go to stderr; stdout
# carries ONLY the allocated number on success.

set -u

LOCK_STALE_SECONDS=30
LOCK_MAX_TRIES=150   # ~30s at 0.2s/try

err() { printf '%s\n' "$*" >&2; }

# --- Resolve the shared main repo root from any worktree -------------------
resolve_main() {
  local common
  common="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
  case "$common" in
    /*) ;;                          # already absolute
    *)  common="$(pwd)/$common" ;;  # relative (we're in the main repo) → absolutise
  esac
  (cd "$common/.." 2>/dev/null && pwd) || return 1
}

# --- Read a paths.* / git.* scalar from baldart.config.yml -----------------
config_scalar() {
  # $1 = top-level block (paths|git), $2 = key
  local block="$1" key="$2" cfg="$MAIN/baldart.config.yml"
  [ -f "$cfg" ] || return 0
  grep -A60 "^${block}:" "$cfg" 2>/dev/null \
    | grep -m1 "[[:space:]]*${key}:" \
    | sed -E "s/.*${key}:[[:space:]]*\"?([^\"#]*)\"?.*/\1/" \
    | sed -E 's/[[:space:]]+$//'
}

# --- Highest integer for PREFIX across one or more directories -------------
scan_dirs_max() {
  local prefix="$1"; shift
  local max="$1"; shift     # starting floor
  local d line n
  for d in "$@"; do
    [ -d "$d" ] || continue
    while IFS= read -r line; do
      [ -n "$line" ] || continue
      n="$(printf '%s' "$line" | sed -E "s/^id:[[:space:]]*${prefix}-0*([0-9]+).*/\1/")"
      case "$n" in ''|*[!0-9]*) continue ;; esac
      [ "$n" -gt "$max" ] && max="$n"
    done <<EOF
$(grep -rhoE "^id:[[:space:]]*${prefix}-[0-9]+" "$d"/*.yml 2>/dev/null)
EOF
  done
  printf '%s' "$max"
}

# --- Lock helpers (mkdir is atomic on POSIX; portable, no flock) -----------
# Staleness uses the lock directory's own mtime (set atomically by mkdir, and
# refreshed when the holder writes pid) — NOT a ts file that may not be written
# yet, which would race a just-created lock into an instant false steal. The
# critical section is local-FS only (the slow `git fetch` runs before the lock),
# so the stale threshold is never tripped by a legitimate holder.
file_mtime() { stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0; }
acquire_lock() {
  local tries=0 now m age
  mkdir -p "$WT_DIR" 2>/dev/null || true
  while ! mkdir "$LOCKDIR" 2>/dev/null; do
    if [ -d "$LOCKDIR" ]; then
      now="$(date +%s)"; m="$(file_mtime "$LOCKDIR")"
      case "$m" in ''|*[!0-9]*) m=0 ;; esac
      age=$(( now - m ))
      if [ "$m" -gt 0 ] && [ "$age" -gt "$LOCK_STALE_SECONDS" ]; then
        err "WARN: stealing stale id-alloc lock (age ${age}s)"; rm -rf "$LOCKDIR" 2>/dev/null; continue
      fi
    fi
    tries=$(( tries + 1 ))
    [ "$tries" -gt "$LOCK_MAX_TRIES" ] && { err "ERROR: could not acquire id-alloc lock"; return 1; }
    sleep 0.2
  done
  printf '%s\n' "$$" > "$LOCKDIR/pid" 2>/dev/null || true
}
release_lock() { rm -rf "$LOCKDIR" 2>/dev/null || true; }

# ===========================================================================
CMD="${1:-}"
MAIN="$(resolve_main)" || { err "ERROR: not inside a git repository"; exit 1; }
WT_DIR="$MAIN/.worktrees"
LOCKDIR="$WT_DIR/.id-alloc.lock"
RESV="$WT_DIR/id-reservations.jsonl"
REG="$WT_DIR/registry.json"

case "$CMD" in
  reserve)
    PREFIX="${2:-}"; SLUG="${3:-}"
    [ -n "$PREFIX" ] || { err "usage: allocate-id.sh reserve <PREFIX> <slug>"; exit 2; }
    case "$PREFIX" in *[!A-Z]*|'') err "ERROR: PREFIX must be uppercase letters (e.g. FEAT, BUG)"; exit 2 ;; esac

    BACKLOG_DIR="$(config_scalar paths backlog_dir)"; [ -n "$BACKLOG_DIR" ] || BACKLOG_DIR="backlog"
    TRUNK="$(config_scalar git trunk_branch)"
    WT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$MAIN")"
    HWM="$WT_DIR/.id-hwm-$PREFIX"

    # Refresh the remote trunk ref BEFORE taking the lock — it's the only slow
    # (network) step; keeping it out of the critical section means a holder can
    # never be stale-stolen mid-allocation.
    [ -n "$TRUNK" ] && git fetch origin "$TRUNK" --quiet 2>/dev/null || true

    acquire_lock || exit 1
    trap release_lock EXIT

    # 1) Floor = stored high-water mark (correctness anchor).
    max=0
    if [ -f "$HWM" ]; then
      max="$(tr -cd '0-9' < "$HWM" 2>/dev/null)"; case "$max" in ''|*[!0-9]*) max=0 ;; esac
    fi

    # 2) Local worktree backlog + every sibling worktree's backlog (in flight).
    dirs="$MAIN/$BACKLOG_DIR"
    [ "$WT" != "$MAIN" ] && dirs="$dirs $WT/$BACKLOG_DIR"
    if [ -f "$REG" ]; then
      while IFS= read -r p; do
        [ -n "$p" ] && dirs="$dirs $p/$BACKLOG_DIR"
      done <<EOF
$(grep -oE '"path"[[:space:]]*:[[:space:]]*"[^"]*"' "$REG" 2>/dev/null | sed -E 's/.*"([^"]*)"$/\1/')
EOF
    fi
    # shellcheck disable=SC2086
    max="$(scan_dirs_max "$PREFIX" "$max" $dirs)"

    # 3) Reservations log (covers a wiped HWM with reservations still in flight).
    if [ -f "$RESV" ]; then
      while IFS= read -r n; do
        n="$(printf '%s' "$n" | sed -E "s/.*\"${PREFIX}-0*([0-9]+).*/\1/")"
        case "$n" in ''|*[!0-9]*) continue ;; esac
        [ "$n" -gt "$max" ] && max="$n"
      done <<EOF
$(grep -oE "\"id\":\"${PREFIX}-[0-9]+" "$RESV" 2>/dev/null)
EOF
    fi

    # 4) Best-effort cross-machine guard: merge-base of trunk (already-merged IDs).
    #    The fetch already ran before the lock; this is a local object-store read.
    if [ -n "$TRUNK" ]; then
      MB="$(git merge-base HEAD "origin/$TRUNK" 2>/dev/null || git merge-base HEAD "$TRUNK" 2>/dev/null || true)"
      if [ -n "$MB" ]; then
        while IFS= read -r n; do
          n="$(printf '%s' "$n" | sed -E "s/^id:[[:space:]]*${PREFIX}-0*([0-9]+).*/\1/")"
          case "$n" in ''|*[!0-9]*) continue ;; esac
          [ "$n" -gt "$max" ] && max="$n"
        done <<EOF
$(git grep -hoE "^id:[[:space:]]*${PREFIX}-[0-9]+" "$MB" -- "$BACKLOG_DIR/*.yml" 2>/dev/null)
EOF
      fi
    fi

    NEXT=$(( max + 1 ))
    printf '%s\n' "$NEXT" > "$HWM" 2>/dev/null || true
    printf '{"prefix":"%s","id":"%s-%04d","worktree":"%s","slug":"%s","ts":"%s"}\n' \
      "$PREFIX" "$PREFIX" "$NEXT" "$WT" "$SLUG" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$RESV" 2>/dev/null || true

    release_lock; trap - EXIT
    printf '%04d\n' "$NEXT"
    ;;

  release)
    WT="${2:-}"
    [ -n "$WT" ] || { err "usage: allocate-id.sh release <worktree-path>"; exit 2; }
    [ -f "$RESV" ] || exit 0
    acquire_lock || exit 0   # best-effort prune; never block cleanup
    trap release_lock EXIT
    # grep -v exit code: 0 = some lines kept, 1 = none kept (all pruned → empty
    # file is the correct result), >=2 = real error (keep the original).
    grep -vF "\"worktree\":\"$WT\"" "$RESV" > "$RESV.tmp" 2>/dev/null
    if [ "$?" -le 1 ]; then
      mv "$RESV.tmp" "$RESV" 2>/dev/null || rm -f "$RESV.tmp" 2>/dev/null
    else
      rm -f "$RESV.tmp" 2>/dev/null
    fi
    release_lock; trap - EXIT
    ;;

  *)
    err "usage: allocate-id.sh {reserve <PREFIX> <slug> | release <worktree-path>}"
    exit 2
    ;;
esac
