#!/bin/bash
# Wave-4 bounded maintenance (SPEC-disk-hygiene.md) plus fail-closed worktree GC.
# Prune reproducible caches over budget, skip categories with an active writer,
# never force-wipe caches, never touch volumes. Single-flight lock so the
# disk-check timer and factory-triggered reclaim cannot race.
# Prints one summary line; exits 1 only on category ERROR (skips are clean).
set -u

BUDGET_UV_GIB=${SM_BUDGET_UV_GIB:-20}
BUDGET_PNPM_GIB=${SM_BUDGET_PNPM_GIB:-20}
BUDGET_PODMAN_GIB=${SM_BUDGET_PODMAN_GIB:-40}
# uv cache prune WAITS on a held cache lock instead of refusing; the timeout
# is the backstop that keeps a held lock from hanging the disk-check timer.
CMD_TIMEOUT_S=${SM_MAINTAIN_CMD_TIMEOUT_S:-1500}
LOCK_WAIT_S=${SM_MAINTAIN_LOCK_WAIT_S:-0}

state_dir=${XDG_STATE_HOME:-$HOME/.local/state}/system-monitor
log_path=$state_dir/maintenance.log
lock_path=$state_dir/disk-maintain.lock
mkdir -p "$state_dir"

# Resolve through install symlinks so lib/ is found beside the repo checkout.
src=$(readlink -f "$0" 2>/dev/null || printf '%s\n' "$0")
src_dir=$(CDPATH= cd -- "$(dirname -- "$src")" && pwd)
monitor_root=$(CDPATH= cd -- "$src_dir/../.." && pwd)
gc_py=$monitor_root/lib/worktree_gc.py
tmp_pack_py=$monitor_root/lib/tmp_pack_sweep.py

freed_kib_total=0
cleaned= skipped= errored=

log() { printf '%s  %s\n' "$(date '+%F %T')" "$1" >>"$log_path"; }

size_kib() { local s; s=$(du -sxk "$1" 2>/dev/null | cut -f1); echo "${s:-0}"; }

gib() { echo $(( ${1:-0} / 1024 / 1024 )); }

mib() { echo $(( ${1:-0} / 1024 )); }

acquire_lock() {
    exec 9>"$lock_path"
    if [ "$LOCK_WAIT_S" -gt 0 ] 2>/dev/null; then
        if ! flock -w "$LOCK_WAIT_S" 9; then
            return 1
        fi
    else
        if ! flock -n 9; then
            return 1
        fi
    fi
    return 0
}

# run_category NAME PATH BUDGET_GIB WRITER_PGREP_PATTERN CMD...
# Writer pattern empty = no process check (tool has its own lock).
run_category() {
    local name=$1 path=$2 budget_gib=$3 writer_pattern=$4
    shift 4
    local before_kib after_kib out rc
    before_kib=$(size_kib "$path")
    if [ "$(gib "$before_kib")" -lt "$budget_gib" ]; then
        return 0
    fi
    if [ -n "$writer_pattern" ] && pgrep -f "$writer_pattern" >/dev/null 2>&1; then
        skipped="$skipped $name(writer)"
        log "SKIP $name: writer matches '$writer_pattern' (size $(gib "$before_kib")G > budget ${budget_gib}G)"
        return 0
    fi
    out=$(timeout "$CMD_TIMEOUT_S" "$@" 2>&1)
    rc=$?
    if [ $rc -ne 0 ]; then
        if [ $rc -eq 124 ]; then
            skipped="$skipped $name(timeout)"
            log "SKIP $name: command exceeded ${CMD_TIMEOUT_S}s (likely waiting on a lock)"
        elif printf '%s' "$out" | grep -qi 'lock'; then
            skipped="$skipped $name(lock)"
            log "SKIP $name: lock held: $(printf '%s' "$out" | head -1)"
        else
            errored="$errored $name"
            log "ERROR $name rc=$rc: $(printf '%s' "$out" | head -3 | tr '\n' ' ')"
        fi
        return 0
    fi
    after_kib=$(size_kib "$path")
    local freed_kib=$(( before_kib - after_kib ))
    [ "$freed_kib" -lt 0 ] && freed_kib=0
    freed_kib_total=$(( freed_kib_total + freed_kib ))
    cleaned="$cleaned $name:$(gib "$freed_kib")G"
    log "CLEAN $name: $(gib "$before_kib")G -> $(gib "$after_kib")G"
}

clean_podman() {
    command -v podman >/dev/null 2>&1 || return 0
    local storage=$HOME/.local/share/containers/storage
    [ -d "$storage" ] || return 0
    local before_kib
    before_kib=$(size_kib "$storage")
    if [ "$(gib "$before_kib")" -lt "$BUDGET_PODMAN_GIB" ]; then
        return 0
    fi
    local df_out
    if ! df_out=$(podman system df 2>&1); then
        errored="$errored podman(metadata)"
        log "ERROR podman metadata: $(printf '%s' "$df_out" | tail -1)"
        return 0
    fi
    if [ -n "$(podman ps -q 2>/dev/null)" ]; then
        skipped="$skipped podman(running)"
        log "SKIP podman: containers running"
        return 0
    fi
    timeout "$CMD_TIMEOUT_S" podman container prune -f >>"$log_path" 2>&1 \
        && timeout "$CMD_TIMEOUT_S" podman image prune -f >>"$log_path" 2>&1
    if [ $? -ne 0 ]; then
        errored="$errored podman(prune)"
        log "ERROR podman prune failed"
        return 0
    fi
    local after_kib
    after_kib=$(size_kib "$storage")
    local freed_kib=$(( before_kib - after_kib ))
    [ "$freed_kib" -lt 0 ] && freed_kib=0
    freed_kib_total=$(( freed_kib_total + freed_kib ))
    cleaned="$cleaned podman:$(gib "$freed_kib")G"
    log "CLEAN podman: $(gib "$before_kib")G -> $(gib "$after_kib")G"
}

run_worktree_gc() {
    [ -f "$gc_py" ] || {
        errored="$errored worktree-gc(missing)"
        log "ERROR worktree-gc: missing $gc_py"
        return 0
    }
    local out rc reaped salvaged
    out=$(python3 "$gc_py" --apply --state-dir "$state_dir" \
        --proc-root "${SM_WORKTREE_GC_PROC_ROOT:-/proc}" \
        --budget-ms "${SM_WORKTREE_GC_BUDGET_MS:-60000}" \
        --vault-root "${SM_WORKTREE_GC_VAULT_ROOT:-$HOME/.local/state/overdeck/worktree-vault}" 2>&1)
    rc=$?
    if [ $rc -ne 0 ]; then
        errored="$errored worktree-gc"
        log "ERROR worktree-gc rc=$rc: $(printf '%s' "$out" | head -3 | tr '\n' ' ')"
        return 0
    fi
    reaped=$(python3 -c 'import json,sys; print(int(json.load(sys.stdin).get("reaped", 0)))' <<<"$out" 2>/dev/null)
    if [ -z "$reaped" ]; then
        errored="$errored worktree-gc(output)"
        log "ERROR worktree-gc: malformed output"
        return 0
    fi
    salvaged=$(python3 -c 'import json,sys; print(int(json.load(sys.stdin).get("salvaged", 0)))' <<<"$out" 2>/dev/null)
    salvaged="${salvaged:-0}"
    cleaned="$cleaned worktree-gc:$reaped(${salvaged}s)"
    log "CLEAN worktree-gc: $out"
}

run_tmp_pack_sweep() {
    [ -f "$tmp_pack_py" ] || {
        errored="$errored tmp-pack(missing)"
        log "ERROR tmp-pack: missing $tmp_pack_py"
        return 0
    }
    local out rc removed freed_bytes freed_kib
    out=$(python3 "$tmp_pack_py" --apply \
        --min-age-s "${SM_TMP_PACK_MIN_AGE_S:-5400}" 2>&1)
    rc=$?
    if [ $rc -ne 0 ]; then
        errored="$errored tmp-pack"
        log "ERROR tmp-pack rc=$rc: $(printf '%s' "$out" | head -3 | tr '\n' ' ')"
        return 0
    fi
    removed=$(python3 -c 'import json,sys; print(int(json.load(sys.stdin).get("removed", 0)))' <<<"$out" 2>/dev/null)
    if [ -z "$removed" ]; then
        errored="$errored tmp-pack(output)"
        log "ERROR tmp-pack: malformed output"
        return 0
    fi
    freed_bytes=$(python3 -c 'import json,sys; print(int(json.load(sys.stdin).get("freedBytes", 0)))' <<<"$out" 2>/dev/null)
    freed_kib=$(( ${freed_bytes:-0} / 1024 ))
    freed_kib_total=$(( freed_kib_total + freed_kib ))
    cleaned="$cleaned tmp-pack:$removed($(mib "$freed_kib")MB)"
    log "CLEAN tmp-pack: $out"
}

if ! acquire_lock; then
    skipped="$skipped lock"
    log "SKIP maintenance: single-flight lock held"
    printf 'skipped:lock; now %sG free on /\n' "$(gib "$(df -Pk / | awk 'NR == 2 { print $4 }')")"
    exit 0
fi

log "START maintenance"

run_worktree_gc
run_tmp_pack_sweep

run_category uv "$HOME/.cache/uv" "$BUDGET_UV_GIB" 'uv (cache (prune|clean)|pip (install|sync)|sync|add|lock|tool install)' \
    uv cache prune
run_category pnpm "${PNPM_HOME:-$HOME/.local/share/pnpm}" "$BUDGET_PNPM_GIB" \
    '(^|/)pnpm( |$)|runplan|vitest|npm (install|ci)|(vite|astro|next|tsc|tsup|webpack|rollup|esbuild).* build' \
    pnpm store prune
clean_podman

avail_kib=$(df -Pk / | awk 'NR == 2 { print $4 }')
summary="freed $(gib "$freed_kib_total")G"
[ -n "$cleaned" ] && summary="$summary (${cleaned# })"
[ -n "$skipped" ] && summary="$summary; skipped:$skipped"
[ -n "$errored" ] && summary="$summary; ERRORS:$errored"
summary="$summary; now $(gib "$avail_kib")G free on /"

log "END $summary"
printf '%s\n' "$summary"
date +%s > "$state_dir/maintenance.last"

[ -z "$errored" ]
