#!/usr/bin/env bash
# gc-refs.sh  -  remove offloaded tool payloads left behind in a repo checkout.
#
# `offload-ref.sh` parks build logs, diffs and test output under
# <root>/.multi-agent/refs/<node_id>.md so a phase prompt can carry a pointer
# instead of the whole log. In worktree modes that directory dies with the
# worktree. In the --local modes there is no worktree: the refs land in the
# real checkout and nothing ever removes them. They are gitignored, so they are
# invisible to `git status` and grow without bound.
#
# This sweeps them, with the same safety contract as gc-tmp.sh:
#   - dry-run by default: lists what WOULD be removed and the space it frees,
#     and deletes nothing until --yes
#   - root guard: refuses a root that is not a directory, and refuses $HOME
#     and / outright, so a stray GC_REFS_ROOT cannot sweep a home directory
#   - fresh-payload grace: files touched in the last GC_REFS_GRACE_MIN minutes
#     (default 10) are spared, so a sweep during an in-flight run cannot pull a
#     ref out from under the agent that is about to read it
#
# Only files matching the node-id shape are touched; anything else a user put
# in that directory is left alone.
#
# Usage:
#   gc-refs.sh                     # dry-run over the current repo
#   gc-refs.sh --yes               # actually delete
#   gc-refs.sh --older-than=1440   # only payloads older than a day
#   gc-refs.sh --all               # every repo under GC_REFS_SEARCH (default $HOME)
#
# Env:
#   GC_REFS_ROOT       override the repo root (default: git toplevel, else $PWD)
#   GC_REFS_SEARCH     where --all looks for checkouts (default $HOME)
#   GC_REFS_GRACE_MIN  grace window in minutes (default 10, 0 = off)
#
# Exit: 0 on success or nothing to do, 2 on usage error or a refused root.

set -uo pipefail

GRACE_MIN="${GC_REFS_GRACE_MIN:-10}"
SEARCH_ROOT="${GC_REFS_SEARCH:-$HOME}"
DELETE=0
OLDER_MIN=0
ALL=0

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

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

if ! printf '%s' "$GRACE_MIN" | grep -qE '^[0-9]+$'; then
  echo "gc-refs: GC_REFS_GRACE_MIN needs a whole number of minutes, got: $GRACE_MIN" >&2
  exit 2
fi

# Collect the refs directories to consider.
REFS_DIRS=""
if [ "$ALL" -eq 1 ]; then
  if [ ! -d "$SEARCH_ROOT" ]; then
    echo "gc-refs: no search root at $SEARCH_ROOT  -  nothing to do"
    exit 0
  fi
  # Prune the trees that are large and never hold a pipeline checkout, so a
  # home-wide sweep stays seconds rather than minutes.
  REFS_DIRS=$(find "$SEARCH_ROOT" \
    \( -name node_modules -o -name Pods -o -name .build -o -name DerivedData -o -name .next \) -prune \
    -o -type d -path "*/.multi-agent/refs" -print 2>/dev/null)
else
  ROOT="${GC_REFS_ROOT:-}"
  if [ -z "$ROOT" ]; then
    ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
    [ -n "$ROOT" ] || ROOT="$PWD"
  fi
  # Root guard: never accept a root that resolves to / or $HOME. A sweep is
  # recursive and a mistyped override should not be able to reach a home dir.
  case "$ROOT" in
    / | "$HOME")
      echo "gc-refs: refusing to sweep '$ROOT'  -  point GC_REFS_ROOT at a repo checkout" >&2
      exit 2
      ;;
  esac
  [ -d "$ROOT" ] || {
    echo "gc-refs: no directory at $ROOT  -  nothing to do"
    exit 0
  }
  [ -d "$ROOT/.multi-agent/refs" ] && REFS_DIRS="$ROOT/.multi-agent/refs"
fi

if [ -z "$REFS_DIRS" ]; then
  echo "gc-refs: no offloaded payloads found  -  nothing to do"
  exit 0
fi

# Match the node-id shape offload-ref.sh writes (p<phase>-<slug>-<8 hex>.md).
# Anything else in that directory belongs to someone else.
NODE_GLOB='p*-*-*.md'

MATCHES=""
while IFS= read -r dir; do
  [ -n "$dir" ] || continue
  # `-mmin +0` is not a no-op: it means "older than a minute" and would drop a
  # file written seconds ago. The age filter is applied only when one was asked
  # for; the grace window below is what protects an in-flight payload.
  if [ "$OLDER_MIN" -gt 0 ]; then
    found=$(find "$dir" -maxdepth 1 -type f -name "$NODE_GLOB" -mmin +"$OLDER_MIN" 2>/dev/null)
  else
    found=$(find "$dir" -maxdepth 1 -type f -name "$NODE_GLOB" 2>/dev/null)
  fi
  [ -n "$found" ] || continue
  while IFS= read -r f; do
    [ -n "$f" ] || continue
    # Grace: skip anything touched inside the window.
    if [ "$GRACE_MIN" -gt 0 ] && [ -n "$(find "$f" -mmin -"$GRACE_MIN" 2>/dev/null)" ]; then
      continue
    fi
    MATCHES="$MATCHES$f
"
  done <<EOF
$found
EOF
done <<EOF
$REFS_DIRS
EOF

MATCHES=$(printf '%s' "$MATCHES" | grep -v '^$' || true)
if [ -z "$MATCHES" ]; then
  echo "gc-refs: no offloaded payloads outside the grace window  -  nothing to do"
  exit 0
fi

COUNT=$(printf '%s\n' "$MATCHES" | grep -c .)
BYTES=0
while IFS= read -r f; do
  [ -n "$f" ] || continue
  sz=$(wc -c < "$f" 2>/dev/null | tr -d ' ')
  BYTES=$((BYTES + ${sz:-0}))
done <<EOF
$MATCHES
EOF
KB=$(((BYTES + 1023) / 1024))

if [ "$DELETE" -eq 0 ]; then
  echo "gc-refs: would remove $COUNT offloaded payload(s), freeing ${KB} KB"
  printf '%s\n' "$MATCHES" | sed 's/^/  /'
  echo ""
  echo "Nothing deleted. Re-run with --yes to remove them."
  exit 0
fi

removed=0
while IFS= read -r f; do
  [ -n "$f" ] || continue
  rm -f "$f" && removed=$((removed + 1))
done <<EOF
$MATCHES
EOF

# Drop a refs directory that is now empty; leave one that still holds
# something we did not put there.
while IFS= read -r dir; do
  [ -n "$dir" ] || continue
  rmdir "$dir" 2>/dev/null || true
done <<EOF
$REFS_DIRS
EOF

echo "gc-refs: removed $removed offloaded payload(s), freed ${KB} KB"
exit 0
