#!/usr/bin/env bash
# Bound the build mirror cache without guessing about live or recent work.
# Only direct children of ~/builds are eligible, and only after they have aged past
# CACHE_MIN_AGE_DAYS. Once over CACHE_MAX_BYTES, reclaim oldest eligible entries
# until CACHE_TARGET_BYTES rather than thrashing at the edge.
set -euo pipefail

ROOT="${BUILDBOX_BUILDS_ROOT:-$HOME/builds}"
MAX_BYTES="${BUILDBOX_CACHE_MAX_BYTES:-$((100 * 1024 * 1024 * 1024))}"
TARGET_BYTES="${BUILDBOX_CACHE_TARGET_BYTES:-$((80 * 1024 * 1024 * 1024))}"
MIN_AGE_DAYS="${BUILDBOX_CACHE_MIN_AGE_DAYS:-7}"

die() { printf 'buildbox-build-cache-prune: %s\n' "$*" >&2; exit 2; }
is_uint() { [[ "$1" =~ ^[0-9]+$ ]]; }

is_uint "$MAX_BYTES" || die "BUILDBOX_CACHE_MAX_BYTES must be an integer"
is_uint "$TARGET_BYTES" || die "BUILDBOX_CACHE_TARGET_BYTES must be an integer"
is_uint "$MIN_AGE_DAYS" || die "BUILDBOX_CACHE_MIN_AGE_DAYS must be an integer"
[ "$TARGET_BYTES" -lt "$MAX_BYTES" ] || die "target must be lower than max"
[ -d "$ROOT" ] || die "cache root is not a directory: $ROOT"

ROOT=$(readlink -f -- "$ROOT")
[ "$ROOT" != / ] || die "refusing filesystem root"
[ "$ROOT" != "$HOME" ] || die "refusing home directory"

cache_bytes() { du -sB1 --one-file-system -- "$ROOT" | awk '{print $1}'; }
used=$(cache_bytes) || die "cannot measure cache root: $ROOT"
if [ "$used" -le "$MAX_BYTES" ]; then
  printf 'build-cache no-op used=%s max=%s root=%s\n' "$used" "$MAX_BYTES" "$ROOT"
  exit 0
fi

cutoff=$(( $(date +%s) - MIN_AGE_DAYS * 86400 ))
candidates=$(mktemp)
trap 'rm -f "$candidates"' EXIT
# The NUL-delimited sort preserves all legal path bytes. The tab is parsed only as
# the separator following the numeric mtime, so a tab in a filename stays in the path.
find -P "$ROOT" -xdev -mindepth 1 -maxdepth 1 -printf '%T@\t%p\0' \
  | sort -z -n >"$candidates"

while IFS= read -r -d '' record; do
  mtime=${record%%$'\t'*}
  path=${record#*$'\t'}
  mtime=${mtime%%.*}
  [ "$mtime" -le "$cutoff" ] || continue
  # find -P plus this direct-child containment check prevents a malformed input from
  # turning this into a recursive cleanup outside the cache root.
  case "$path" in "$ROOT"/*) ;; *) die "candidate escaped cache root";; esac
  rm -rf --one-file-system -- "$path"
  used=$(cache_bytes) || die "cannot remeasure cache root: $ROOT"
  printf 'build-cache pruned=%s used=%s target=%s\n' "$path" "$used" "$TARGET_BYTES"
  [ "$used" -le "$TARGET_BYTES" ] && exit 0
done <"$candidates"

printf 'build-cache cap-unmet used=%s max=%s target=%s (no aged entries remain)\n' \
  "$used" "$MAX_BYTES" "$TARGET_BYTES" >&2
exit 1
