#!/usr/bin/env bash
# gc-worktrees.sh  -  sweep worktree residue in the current project repo.
#
# Complements gc-tmp.sh (which only touches /tmp scratch). This handles the
# leftovers a worktree lifecycle can strand inside the repo itself:
#
#   1. stale worktree admin entries   -> git worktree prune
#   2. orphan .worktrees/* dirs       -> dirs no longer registered as
#      worktrees (killed mid-run, manual deletes). Listed in dry-run,
#      removed only with --yes.
#   3. gitlink residue in the index   -> .worktrees/{id} recorded as a
#      "Subproject commit" entry by a blanket `git add -A` before the
#      residue guard existed. --yes runs `git rm --cached` (the commit
#      itself stays yours to make).
#   4. missing residue guard          -> ensures `.worktrees/` is in
#      .git/info/exclude so the gitlink class cannot re-occur.
#
# Registered, healthy worktrees are NEVER touched. Finishing a task removes its
# own worktree in Phase 6 (worktree-finalize, v14.1.0+); killing one is
# /multi-agent:kill. This sweep only reaps orphans neither of those left behind.
#
# SAFE BY DEFAULT: dry-run. Deletes nothing until you pass --yes.
#
# Usage:
#   gc-worktrees.sh                     # dry-run in the current repo
#   gc-worktrees.sh --yes               # apply
#   gc-worktrees.sh --repo <path>       # operate on another repo
#   gc-worktrees.sh --older-than=60     # only orphan dirs idle > 60 min
#
# Exit: 0 on success (including "nothing to do"), 2 on usage error.

set -uo pipefail

REPO="$PWD"
DELETE=0
OLDER_MIN=0

usage() {
  grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
}

while [ $# -gt 0 ]; do
  case "$1" in
    --yes | --force) DELETE=1; shift ;;
    --repo)
      REPO="${2:-}"
      [ -z "$REPO" ] && { echo "gc-worktrees: --repo needs a path" >&2; exit 2; }
      shift 2
      ;;
    --older-than=*)
      OLDER_MIN="${1#*=}"
      if ! printf '%s' "$OLDER_MIN" | grep -qE '^[0-9]+$'; then
        echo "gc-worktrees: --older-than needs a whole number of minutes, got: $OLDER_MIN" >&2
        exit 2
      fi
      shift
      ;;
    -h | --help) usage; exit 0 ;;
    *) echo "gc-worktrees: unknown argument: $1" >&2; exit 2 ;;
  esac
done

if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  echo "gc-worktrees: $REPO is not a git work tree  -  nothing to do"
  exit 0
fi
REPO="$(cd "$REPO" && pwd -P)"
# Anchor on the MAIN worktree (first porcelain entry) so a run started from a
# subdirectory OR from inside a linked worktree still targets
# <main-repo>/.worktrees/ rather than a bogus <subdir>/.worktrees/ (which
# silently found nothing and left residue behind).
MAIN_WT="$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p' | head -1)"
MAIN_WT="$(cd "$MAIN_WT" 2>/dev/null && pwd -P)" || MAIN_WT=""
[ -n "$MAIN_WT" ] && REPO="$MAIN_WT"
WT_ROOT="$REPO/.worktrees"

changed=0

# 1. Stale admin entries (safe on a clean repo, no confirmation needed:
#    prune only drops bookkeeping for dirs that are already gone).
pruned=$(git -C "$REPO" worktree prune -v 2>&1 | grep -c . || true)
[ "$pruned" -gt 0 ] && echo "→ pruned $pruned stale worktree admin entr$( [ "$pruned" -eq 1 ] && echo y || echo ies)"

# 2. Orphan dirs: present under .worktrees/ but not registered as worktrees.
orphans=()
if [ -d "$WT_ROOT" ]; then
  # Resolve every registered worktree path to its physical form so the
  # orphan test below compares like-for-like against `pwd -P`-resolved dirs.
  # A raw (unresolved) registered path with a symlinked component would fail
  # the match and get a HEALTHY worktree misclassified as an orphan and
  # deleted under --yes; resolving both sides removes that risk (this mirrors
  # purge.sh, which resolves both sides).
  registered=""
  while IFS= read -r regpath; do
    [ -n "$regpath" ] || continue
    regphys="$(cd "$regpath" 2>/dev/null && pwd -P)" || regphys=""
    [ -n "$regphys" ] && registered="$registered$regphys
"
  done <<EOF
$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
EOF
  for d in "$WT_ROOT"/*/; do
    [ -d "$d" ] || continue
    [ -L "${d%/}" ] && continue  # never follow a symlinked entry out of the tree
    dir="$(cd "$d" && pwd -P)"
    # Path guard: only ever consider dirs strictly inside <repo>/.worktrees/.
    case "$dir" in "$WT_ROOT"/*) ;; *) continue ;; esac
    if printf '%s\n' "$registered" | grep -qxF "$dir"; then
      continue  # healthy registered worktree - kill/finish territory
    fi
    if [ "$OLDER_MIN" -gt 0 ] && find "$dir" -type f -mmin "-$OLDER_MIN" 2>/dev/null | head -1 | grep -q .; then
      continue  # recently touched - spare it
    fi
    orphans+=("$dir")
  done
fi
for dir in ${orphans+"${orphans[@]}"}; do
  size=$(du -sh "$dir" 2>/dev/null | cut -f1)
  if [ "$DELETE" -eq 1 ]; then
    rm -rf "$dir" && { echo "→ removed orphan worktree dir: ${dir#"$REPO"/} (${size:-?})"; changed=$((changed + 1)); }
  else
    echo "would remove orphan worktree dir: ${dir#"$REPO"/} (${size:-?})"
  fi
done

# 3. Gitlink residue in the index ("Subproject commit" entries under
#    .worktrees/ from a pre-guard `git add -A`).
# Path is the tab-separated second field ("mode sha stage\tpath"); splitting on
# whitespace truncated any path containing a space and the follow-up
# `git rm --ignore-unmatch` exited 0 on the phantom, reporting a false success.
gitlinks=$(git -C "$REPO" ls-files -s -- .worktrees 2>/dev/null | awk -F'\t' '$1 ~ /^160000 / { print $2 }')
if [ -n "$gitlinks" ]; then
  while IFS= read -r path; do
    [ -z "$path" ] && continue
    if [ "$DELETE" -eq 1 ]; then
      git -C "$REPO" rm --cached --ignore-unmatch -q -- "$path" \
        && { echo "→ unstaged gitlink residue: $path (commit the removal yourself)"; changed=$((changed + 1)); }
    else
      echo "would unstage gitlink residue from the index: $path"
    fi
  done <<EOF
$gitlinks
EOF
fi

# 4. Residue guard: keep .worktrees/ out of the index from now on.
ex="$(git -C "$REPO" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/info/exclude"
if [ -n "${ex%/info/exclude}" ] && ! grep -qxF '.worktrees/' "$ex" 2>/dev/null; then
  if [ "$DELETE" -eq 1 ]; then
    mkdir -p "$(dirname "$ex")" \
      && printf '.worktrees/\n' >> "$ex" \
      && { echo "→ added .worktrees/ to .git/info/exclude (residue guard)"; changed=$((changed + 1)); }
  else
    echo "would add .worktrees/ to .git/info/exclude (residue guard)"
  fi
fi

total=$(( ${#orphans[@]} + $(printf '%s' "$gitlinks" | grep -c . || true) ))
if [ "$DELETE" -eq 1 ]; then
  echo "══ gc-worktrees: applied $changed change(s) in $REPO ══"
elif [ "$total" -eq 0 ] && [ "$pruned" -eq 0 ] && { [ -z "${ex%/info/exclude}" ] || grep -qxF '.worktrees/' "$ex" 2>/dev/null; }; then
  echo "gc-worktrees: no worktree residue in $REPO  -  nothing to do"
else
  echo "══ gc-worktrees: dry-run  -  re-run with --yes to apply ══"
fi
exit 0
