#!/bin/bash
# restic-backup — daily restic backup driver, meant to be polled every 15 min by cron.
#
#   restic-backup          run the periodic check (prompt + backup if a daily backup is due)
#   restic-backup run      same as above (explicit)
#   restic-backup status   print the dwmblocks status symbol (running -> symbol, idle -> empty line)
#   restic-backup force    skip the "is it due?" check and back up now (still prompts)
#
# dwmblocks integration: add a block whose command is `restic-backup status` on signal
# RTMIN+14, e.g. in ~/src/dwmblocks/config.h:
#     {"restic-backup status", 0, 14},
# The script refreshes that block itself via `pkill -RTMIN+14 dwmblocks`.

set -u

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
REPO="sftp:root@bocken.org:/backups/t14"
HOST="bocken.org"             # backup server, checked for reachability before we prompt
PORT=22                       # sftp/ssh port
INCLUDE_FILE="$HOME/.config/restic/include_files"
EXCLUDE_FILE="$HOME/.config/restic/exclude_files"

SIGNAL=14                     # dwmblocks RTMIN+SIGNAL (unused in current config.h)
SYMBOL=$''              # status symbol shown while a backup is running (nf  database; edit to taste)
LOW_BATTERY=30                # %, below this on battery we postpone and notify

STATE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/restic-backup"
LAST_SUCCESS="$STATE_DIR/last_success"   # holds YYYY-MM-DD of last successful backup
RUNNING_FLAG="$STATE_DIR/running"        # present only while restic is running
LOCK_FILE="$STATE_DIR/lock"              # flock target, serialises cron invocations
SNOOZE_FILE="$STATE_DIR/snooze"          # epoch until which we must not re-prompt
LOG_FILE="$STATE_DIR/backup.log"

mkdir -p "$STATE_DIR"

# Repository password comes from pass(1) at "Misc/restic", supplied to restic
# below via --password-command. (pass -> gpg needs the user's gpg-agent, which
# the graphical-env import / DISPLAY recovery below makes reachable from cron.)
PASS_CMD="pass show Misc/restic"

export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin:$PATH"

# ---------------------------------------------------------------------------
# Status subcommand — used by dwmblocks. Must be cheap and side-effect free.
# ---------------------------------------------------------------------------
if [ "${1:-run}" = "status" ]; then
    if [ -f "$RUNNING_FLAG" ]; then
        printf '%s\n' "$SYMBOL"
    else
        printf '\n'
    fi
    exit 0
fi

# ---------------------------------------------------------------------------
# Everything below runs from cron, which has no graphical/session environment.
# Recover DISPLAY / XAUTHORITY / DBUS so dmenu and notify-send work.
# ---------------------------------------------------------------------------
import_graphical_env() {
    local pid var val
    # Find a process that belongs to our graphical session.
    pid=$(pgrep -u "$(id -u)" -nx dwm 2>/dev/null) \
        || pid=$(pgrep -u "$(id -u)" -nx dwmblocks 2>/dev/null) \
        || pid=$(pgrep -u "$(id -u)" -nx Xorg 2>/dev/null)
    [ -n "$pid" ] && [ -r "/proc/$pid/environ" ] || return 0
    for var in DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS; do
        if [ -z "${!var:-}" ]; then
            val=$(tr '\0' '\n' < "/proc/$pid/environ" | grep "^$var=" | head -n1 | cut -d= -f2-)
            [ -n "$val" ] && export "$var=$val"
        fi
    done
}
import_graphical_env
export DISPLAY="${DISPLAY:-:0}"
export XAUTHORITY="${XAUTHORITY:-$HOME/.Xauthority}"

notify() { notify-send -a "restic backup" "$@"; }

refresh_bar() { pkill "-RTMIN+$SIGNAL" dwmblocks 2>/dev/null; }

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

# ---------------------------------------------------------------------------
# Battery check. Returns 0 = ok to back up, 1 = postpone (low battery on AC off).
# ---------------------------------------------------------------------------
battery_ok() {
    local ac on_ac=0 cap=100 bat
    for ac in /sys/class/power_supply/A{C,DP}*; do
        [ -r "$ac/online" ] && on_ac=$(cat "$ac/online") && break
    done
    [ "$on_ac" = "1" ] && return 0          # plugged in -> always fine

    for bat in /sys/class/power_supply/BAT*; do
        [ -r "$bat/capacity" ] && cap=$(cat "$bat/capacity") && break
    done
    [ "$cap" -ge "$LOW_BATTERY" ] && return 0
    return 1
}

# ---------------------------------------------------------------------------
# Is the backup server reachable? TCP connect to the sftp port, no extra deps.
# ---------------------------------------------------------------------------
host_reachable() {
    timeout 5 bash -c "exec 3<>/dev/tcp/$HOST/$PORT" 2>/dev/null
}

# ---------------------------------------------------------------------------
# Is a daily backup due? Due unless we already succeeded today.
# ---------------------------------------------------------------------------
backup_due() {
    [ -f "$LAST_SUCCESS" ] || return 0
    [ "$(cat "$LAST_SUCCESS")" != "$(date '+%F')" ]
}

# Currently snoozed by an earlier decline? (cleared automatically once it lapses)
snoozed() {
    [ -f "$SNOOZE_FILE" ] || return 1
    [ "$(date '+%s')" -lt "$(cat "$SNOOZE_FILE")" ]
}

# ---------------------------------------------------------------------------
# Run restic. Sets the running flag, repaints the bar, restores on exit.
# ---------------------------------------------------------------------------
do_backup() {
    : > "$RUNNING_FLAG"
    refresh_bar
    # Always clear the flag and repaint, no matter how we exit.
    trap 'rm -f "$RUNNING_FLAG"; refresh_bar' EXIT

    log "backup started"
    # restic command uses paths relative to $HOME in the original invocation.
    cd "$HOME" || exit 1
    if restic backup -r "$REPO" \
            --password-command "$PASS_CMD" \
            --files-from "$INCLUDE_FILE" \
            --exclude-file "$EXCLUDE_FILE" >> "$LOG_FILE" 2>&1; then
        date '+%F' > "$LAST_SUCCESS"
        rm -f "$SNOOZE_FILE"
        log "backup finished OK"
        notify "✅ Backup complete" "restic snapshot stored on bocken.org:/backups/t14"
    else
        log "backup FAILED (see $LOG_FILE)"
        notify -u critical "❌ Backup failed" "restic exited non-zero. See $LOG_FILE"
    fi
}

# ---------------------------------------------------------------------------
# Main: the periodic (cron) entrypoint.
# ---------------------------------------------------------------------------
main() {
    local force="${1:-}"

    # Only ever decide whether a backup is due if none is in progress / pending.
    # flock -n serialises the 15-minute cron invocations: while a prompt is open
    # or restic is running, later invocations just exit instead of stacking.
    exec 9>"$LOCK_FILE"
    flock -n 9 || exit 0

    if [ "$force" != "force" ] && ! backup_due; then
        exit 0
    fi

    # Honour a snooze the user picked after a previous decline ("force" overrides).
    if [ "$force" != "force" ] && snoozed; then
        exit 0
    fi

    if ! battery_ok; then
        notify -u critical "🔌 Backup postponed" \
            "A daily backup is due but the battery is low. Plug in the laptop to start it."
        log "postponed: low battery"
        exit 0
    fi

    # Don't pester the user if the server can't be reached; just retry next tick.
    if ! host_reachable; then
        log "postponed: $HOST:$PORT unreachable"
        exit 0
    fi

    # Ask before doing the (potentially long, bandwidth-heavy) backup.
    # `prompt` (~/.local/bin) runs its 2nd arg only when "Yes" is picked; we give
    # it a no-op so its exit status reports the choice while do_backup stays here
    # in the parent, under the flock we already hold.
    if prompt "Daily restic backup is due — run it now?" true; then
        do_backup
    else
        # Declined: ask how long to wait before prompting again.
        local sel until
        sel=$(printf 'in 15 minutes\nin 1 hour\nin 4 hours\nnext day' \
            | dmenu -i -p "Backup postponed — re-attempt when?")
        case "$sel" in
            "in 15 minutes") until=$(date -d '+15 minutes' '+%s') ;;
            "in 1 hour")     until=$(date -d '+1 hour'     '+%s') ;;
            "in 4 hours")    until=$(date -d '+4 hours'    '+%s') ;;
            "next day")      until=$(date -d 'tomorrow 00:00' '+%s') ;;
            *)               until=0 ;;   # dismissed -> no snooze, retry next tick
        esac
        if [ "$until" -gt 0 ]; then
            printf '%s\n' "$until" > "$SNOOZE_FILE"
            log "user declined; snoozed $sel (until $(date -d "@$until" '+%F %T'))"
        else
            rm -f "$SNOOZE_FILE"
            log "user declined; no snooze chosen, retry next tick"
        fi
    fi
}

main "${1:-run}"
