#!/usr/bin/env bash
#
# Weekly image refresh for the compose stack in /home/connor.
#
# Why this exists: every service in compose.yml is pinned to `:latest` (bar the
# two Immich images, which carry digests), podman-auto-update.timer is disabled
# and nothing else pulled. The tag made the stack look current while the
# running images were up to twelve months old -- jellyseerr, the personal-site
# pair and searxng's valkey had not moved since 2025. `:latest` without a
# puller is not a rolling tag, it is a snapshot with a misleading name.
#
# Recreation is deliberate rather than pull-only. Stale images are the larger
# standing risk, `restart: unless-stopped` plus the nightly restic snapshot
# make a bad pull recoverable, and podman keeps the previous image so a
# rollback is `podman tag` away. Set APPLY=0 below to downgrade this to
# pull-and-notify if that trade ever stops being worth it.
set -euo pipefail
umask 077

APPLY=1
PROJECT_DIR=/home/connor
NTFY_URL=https://ntfy.rcjohnstone.com/infra
NTFY_ENV=/etc/ntfy/publish.env

notify() {  # notify <priority> <tags> <title> <body>
    local u p
    [ -r "$NTFY_ENV" ] || return 0
    # PARSED, not sourced: the bot password contains ` and &, so sourcing it
    # dies with a syntax error. Same reason the restic scripts use sed.
    u=$(sed -n 's/^NTFY_USER=//p' "$NTFY_ENV" | head -1)
    p=$(sed -n 's/^NTFY_PASS=//p' "$NTFY_ENV" | head -1)
    [ -n "$u" ] && [ -n "$p" ] || return 0
    curl -fsS --max-time 20 -u "$u:$p" \
         -H "Title: $3" -H "Priority: $1" -H "Tags: $2" \
         -d "$4" "$NTFY_URL" >/dev/null || true
}

# Trap installed before anything can fail, so a crash still reports. The
# restic script learned this the hard way: its failure-log code sat above the
# first thing that could die, and the one failure it was written to capture
# happened before it was reached.
LOG=$(mktemp /tmp/podman-stack-update.XXXXXX)
FAILLOG=/var/log/podman-stack-update.failed.log
cleanup() {
    local rc=$?
    if [ "$rc" -ne 0 ]; then
        cp -f "$LOG" "$FAILLOG" 2>/dev/null || true
        notify urgent rotating_light "Stack update FAILED on $(uname -n)" \
            "exit $rc"$'\n\n'"$(tail -n 25 "$LOG")"
    fi
    rm -f "$LOG"
}
trap cleanup EXIT

cd "$PROJECT_DIR"

# Image IDs before, so the report names what actually moved rather than
# everything that was pulled.
declare -A before
while read -r ref id; do before["$ref"]=$id; done < <(
    podman images --format '{{.Repository}}:{{.Tag}} {{.ID}}' 2>/dev/null)

echo "=== pull ===" >>"$LOG"
podman-compose pull >>"$LOG" 2>&1

changed=()
while read -r ref id; do
    [ "$ref" = "<none>:<none>" ] && continue
    if [ "${before[$ref]:-none}" != "$id" ]; then changed+=("$ref"); fi
done < <(podman images --format '{{.Repository}}:{{.Tag}} {{.ID}}' 2>/dev/null)

if [ "${#changed[@]}" -eq 0 ]; then
    echo "no image changes" >>"$LOG"
    notify low white_check_mark "Stack update: no changes" "All images already current."
    exit 0
fi

if [ "$APPLY" -ne 1 ]; then
    notify default package "Stack update: ${#changed[@]} image(s) available" \
        "$(printf '%s\n' "${changed[@]}")"$'\n\n'"APPLY=0, not recreated."
    exit 0
fi

echo "=== up -d ===" >>"$LOG"
podman-compose up -d >>"$LOG" 2>&1

# A container that is not running 30s after recreation is the failure mode
# worth shouting about -- a bad image starts, crashes, and without this the
# run still looks like a success.
sleep 30
notrunning=$(podman ps -a --filter 'label=io.podman.compose.project=connor' \
             --format '{{.Names}} {{.Status}}' 2>/dev/null | grep -v '^\S* Up' || true)

if [ -n "$notrunning" ]; then
    notify urgent rotating_light "Stack update: containers DOWN on $(uname -n)" \
        "Updated:"$'\n'"$(printf '%s\n' "${changed[@]}")"$'\n\n'"Not running:"$'\n'"$notrunning"
    exit 1
fi

notify default package "Stack update OK on $(uname -n)" \
    "${#changed[@]} image(s) updated:"$'\n'"$(printf '%s\n' "${changed[@]}")"
