#!/usr/bin/env bash
#
# Weekly copy of the Media pool -> the Backup pool, both on this machine.
#
# Purpose: survive losing the Media pool itself (or a bad delete on it). Both pools are
# in this one box, so this is NOT an offsite backup and never will be.
#
#   source  /home/gib/Public                     (Media, 50.9 T raidz)
#   dest    /home/gib/.local/share/Backup/Public (Backup, 14.5 T raidz1)
#
# Runs as ROOT via media-backup.service. Not optional: /home/gib/Public/Nextcloud/Server is
# uid 33 mode 0750 and the borg repo is root:root mode 0600. The historical hand-run rsync
# ran as gib, could not read those, skipped them -- AND EXITED 0. That is how 442 G of
# Nextcloud ended up in the Backup pool while nobody could tell it was the wrong 442 G.
#
# HISTORY IS TAKEN BY THIS SCRIPT, NOT BY A TIMER. zfs-auto-snapshot is disabled on the
# Backup pool (com.sun:auto-snapshot=false) because a timer-driven snapshot can land
# mid-rsync and record a half-written backup as that week's restore point. Here the snapshot
# is taken only AFTER a successful sync, so every snapshot on that pool is, by construction,
# a complete backup.

set -uo pipefail

SOURCE="/home/gib/Public/"
DEST="/home/gib/.local/share/Backup/Public"
POOL="Backup"
BORG_LOCK="/home/gib/Public/Nextcloud/Backups/borg/lock.exclusive"
KEEP_SNAPSHOTS=8            # ~2 months at weekly
MIN_FREE_GB=200             # refuse to start if the pool is already this tight
LOG="/var/log/media-backup.log"
LOCK="/run/media-backup.lock"

log() { printf '%s  %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG"; }
die() { log "FATAL: $*"; exit 1; }

exec 9>"$LOCK" || die "cannot open lock $LOCK"
flock -n 9 || die "another run is already in progress"

EXCLUDES=(
    # --- too big, deliberately not backed up -------------------------------
    # 23.2 T. Larger on its own than the entire Backup pool. gib's explicit decision:
    # if Media is lost, the TV library gets rebuilt.
    "/TV"

    # Torrent scratch. qBittorrent manages it (ratio 2, ~14.7 d inactive, then
    # RemoveWithContent), and anything worth keeping has already been imported.
    "/Downloads"

    # --- already self-versioning, so a second copy buys nothing ------------
    # TimeMachine is its own backup format with its own history.
    "/Mac"

    # 586 G of raw Nextcloud data. Covered by AIO's borg repo below, which is
    # deduplicated, encrypted, and carries 7d/4w/6m of history instead of one stale
    # point-in-time. Verified: the borgbackup container mounts this exact path as
    # /nextcloud_aio_volumes/nextcloud_aio_nextcloud_data and borg walks the whole tree.
    # We keep Nextcloud/Backups (the repo). We do NOT keep Nextcloud/Server.
    "/Nextcloud/Server"

    # --- regenerable ------------------------------------------------------
    "/Server_Backup/Ollama_Models"     # re-pullable model weights
    "/Server_Backup/ComfyUI"           # re-downloadable checkpoints
    "/Server_Backup/frigate"           # camera footage; container is decommissioned
    "/Server_Backup/.Trash-1000"       # desktop trash
)
# Kept on purpose, so nobody "tidies" them later: Server_Backup/Containers (the only copy
# of both machines' ~/Server backups outside the Media pool), immich (irreplaceable photos),
# Games and ROMs (gib: "they can be hard to get").

EXCLUDE_ARGS=()
for e in "${EXCLUDES[@]}"; do EXCLUDE_ARGS+=( "--exclude=$e" ); done

log "=== media-backup start ==="

# --- preflight ------------------------------------------------------------
mountpoint -q /home/gib/Public \
    || die "/home/gib/Public is not a mountpoint -- Media pool not imported, refusing to run"
mountpoint -q /home/gib/.local/share/Backup \
    || die "Backup pool is not mounted -- refusing to write into a plain directory on the NVMe"
mkdir -p "$DEST"

# A borg run holds this lock for 2-4 hours and rewrites index/hints/integrity at the END.
# Copying mid-run pairs new data chunks with a stale index: a repo that looks fine and
# fails at restore. Defer rather than queue -- the next weekly run will catch it.
if [ -e "$BORG_LOCK" ]; then
    log "borg is mid-run (lock held at $BORG_LOCK)"
    log "DEFERRING this week's run rather than copying a torn repo. Next fire: next Saturday."
    log "=== media-backup deferred ==="
    exit 0
fi

avail_gb=$(zfs get -Hp -o value available "$POOL" 2>/dev/null)
avail_gb=$(( ${avail_gb:-0} / 1024 / 1024 / 1024 ))
log "Backup pool free: ${avail_gb} GB"
[ "$avail_gb" -lt "$MIN_FREE_GB" ] \
    && die "only ${avail_gb} GB free (floor ${MIN_FREE_GB} GB) -- refusing to start"

# --- sync -----------------------------------------------------------------
# --numeric-ids is mandatory: `getent passwd 33` returns nothing on this host (uid 33 is
# www-data inside a container), so rsync has no name to map ownership by.
# --delete/--delete-excluded keep the copy exact; anything wrongly removed is still in
# last week's snapshot.
log "Syncing ${SOURCE} -> ${DEST}/ ..."
rsync -a --numeric-ids --delete --delete-excluded --human-readable --stats \
      "${EXCLUDE_ARGS[@]}" "$SOURCE" "$DEST/" 2>&1 | tee -a "$LOG"
status=${PIPESTATUS[0]}

# 24 = "source files vanished during transfer". Normal on a live system.
if [ "$status" -ne 0 ] && [ "$status" -ne 24 ]; then
    die "rsync exit $status -- NOT snapshotting. Last good snapshot is still the newest."
fi
[ "$status" -eq 24 ] && log "NOTE: rsync exit 24 (files vanished mid-run) -- expected"

# --- snapshot, only now that the sync succeeded ---------------------------
SNAP="${POOL}@backup-$(date +%Y-%m-%d)"
if zfs list -t snapshot -H "$SNAP" >/dev/null 2>&1; then
    log "snapshot $SNAP already exists (second run today), skipping"
else
    zfs snapshot "$SNAP" && log "snapshot taken: $SNAP" || log "WARNING: snapshot failed"
fi

# Keep the newest N. These are ours, so nothing else prunes them.
mapfile -t snaps < <(zfs list -t snapshot -H -o name -s creation -d 1 "$POOL" 2>/dev/null | grep "@backup-")
if [ "${#snaps[@]}" -gt "$KEEP_SNAPSHOTS" ]; then
    drop=$(( ${#snaps[@]} - KEEP_SNAPSHOTS ))
    for s in "${snaps[@]:0:$drop}"; do
        zfs destroy "$s" && log "pruned old snapshot: $s"
    done
fi
log "snapshots retained: $(zfs list -t snapshot -H -o name -d 1 "$POOL" 2>/dev/null | grep -c '@backup-')"

# Heartbeat for Uptime Kuma ("Media Backup (server.gib)"), sent only after a completed
# sync and snapshot. A deferred week exits before this, so it does not ping.
if [ -r /home/gib/Server/.media-backup-heartbeat-url ]; then
    url=$(< /home/gib/Server/.media-backup-heartbeat-url)
    curl -fsS -m 20 "$url" >/dev/null 2>&1 \
        && log "Heartbeat sent" || log "NOTE: heartbeat ping failed (backup itself was fine)"
fi

log "=== media-backup complete ==="
