#!/usr/bin/env bash
#
# ONE-TIME seed of the NAS restic repository.
#
# Why this exists rather than just running restic-backup: a single `restic
# backup` over 520G takes ~12h on this uplink and commits nothing until it
# finishes. Two attempts were already lost whole -- one to an edit of the
# running script, one to an unplanned reboot -- because an interrupted run
# leaves only unreferenced packs that the next run cannot reuse.
#
# So: back up in chunks, each of which commits its own snapshot. An
# interruption now costs one chunk, not the run. Re-running picks up where it
# left off via the state file, which survives reboots.
#
# The chunk list does NOT have to be exhaustive. Once it completes, the normal
# restic-backup.service run snapshots the full path set and dedupes against
# everything seeded here, so any file not covered by a chunk is picked up then
# -- in minutes rather than hours. Afterwards:
#
#     restic forget --tag seed --group-by '' && restic prune
#
# drops the seed snapshots; their data stays, referenced by the real snapshot.

set -euo pipefail
umask 077

export RESTIC_CACHE_DIR="$HOME/.cache/restic"
set -a; . "$HOME/.config/restic/hetzner.env"; set +a

EXCLUDES="$HOME/.config/restic/excludes.txt"
STATE="$HOME/.local/state/restic-seed.done"
BATCH_BYTES=$(( 15 * 1024 * 1024 * 1024 ))   # ~15G per chunk => ~20 min each

mkdir -p "$(dirname "$STATE")"; touch "$STATE"

log() { printf '%s restic-seed: %s\n' "$(date -Is)" "$*"; }

# Backed up whole -- each is small enough to be one chunk.
WHOLE=(
    /etc
    /home/connor
    /nas/books
    /nas/docs
    /nas/photo/takeout
    /nas/photo/immich/backups
    /nas/audio/podcast
)

# Too big for one chunk; split into batches of their immediate children.
# admin/ is immich's only library user and holds 185G across 22 year folders;
# music/ and shanty/ are ~190 artist directories each.
SPLIT=(
    /nas/photo/immich/library/admin
    /nas/audio/music
    /nas/audio/shanty
)

# run_chunk <path>...
# Keyed by the md5 of its path list so the state file survives re-ordering.
run_chunk() {
    local key; key=$(printf '%s\n' "$@" | md5sum | cut -c1-12)
    if grep -qx "$key" "$STATE"; then
        log "skip  [$key] $1 ${2:+(+$(($#-1)) more)}"
        return 0
    fi
    log "chunk [$key] $1 ${2:+(+$(($#-1)) more)}"
    # Exit 3 means some files were unreadable but the snapshot was written --
    # still progress, still worth recording.
    local rc=0
    nice -n 10 ionice -c2 -n7 restic backup \
        --one-file-system \
        --exclude-file="$EXCLUDES" \
        --exclude-caches \
        --tag nas --tag seed \
        "$@" || rc=$?
    if [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; then
        printf '%s\n' "$key" >> "$STATE"
        # NOT `[ ... ] && log`: under `set -e` a false test makes that the
        # function's exit status and kills the whole seed on the first
        # SUCCESSFUL chunk.
        if [ "$rc" -eq 3 ]; then log "  (exit 3: some files unreadable)"; fi
    else
        log "  FAILED rc=$rc -- leaving unrecorded so a re-run retries it"
        return "$rc"
    fi
}

for p in "${WHOLE[@]}"; do
    [ -e "$p" ] || { log "missing, skipping: $p"; continue; }
    run_chunk "$p"
done

for root in "${SPLIT[@]}"; do
    [ -d "$root" ] || { log "missing, skipping: $root"; continue; }
    batch=(); size=0
    while IFS= read -r -d '' child; do
        csize=$(du -sb --apparent-size "$child" 2>/dev/null | cut -f1) || csize=0
        # Flush first if adding this child would overflow, so a single
        # oversized child still gets a chunk of its own rather than being
        # merged into a huge one.
        if [ "${#batch[@]}" -gt 0 ] && [ $(( size + csize )) -gt "$BATCH_BYTES" ]; then
            run_chunk "${batch[@]}"
            batch=(); size=0
        fi
        batch+=("$child"); size=$(( size + csize ))
    done < <(find "$root" -mindepth 1 -maxdepth 1 -print0 | sort -z)
    if [ "${#batch[@]}" -gt 0 ]; then run_chunk "${batch[@]}"; fi
done

log "seed complete -- $(wc -l < "$STATE") chunks recorded"
log "next: systemctl start restic-backup.service (full run, dedupes against these)"
