#!/bin/sh
# keyboot-be-rollback — boot-environment rollback via clone (ADR 0007 rung 2).
#
# "Rollback" in the boot-environment world is NOT `zfs rollback` (per-dataset,
# in-place, destroys newer snapshots). The recommended, reversible rung is:
# clone a chosen point-in-time snapshot into a NEW boot environment and boot
# that — the live BE and the whole snapshot timeline stay untouched, so you can
# destroy the clone and you're back. This tool is that rung.
#
# It clones the recursive snapshot SET (the dataset named in the snapshot + every
# child at the same snapshot name — what keyboot-autosnap's `zfs snapshot -r`
# produces) so the rolled-back system is a consistent point-in-time, and sets
# `mountpoint=/` on the clone root (zfs clone doesn't copy the local mountpoint).
#
# Usage:
#   keyboot-be-rollback [opts] <snapshot>
#     <snapshot>      e.g. rpool/ROOT/alpine@2026-06-01-0000Z-DAILY
#     --new-be NAME   clone name under <pool>/ROOT (default <be>-rb-<snaptag>)
#     --set-bootfs    `zpool set bootfs=<clone>` (permanent default; else print)
#     --dry-run       print the zfs commands, do nothing
#
# Rungs 1 (boot read-only) and 3 (rollback-destroy, with the dependent-clone
# refusal guard) live in keyboot's pre-boot menu (stage-8); this tool is the
# from-userland clone rung used by the upgrade/rollback workflows + its test.

set -u
PROG=keyboot-be-rollback
NEW_BE=; SET_BOOTFS=0; DRYRUN=0; SNAP=

die()  { printf '%s: error: %s\n' "$PROG" "$*" >&2; exit 2; }
log()  { printf '%s: %s\n' "$PROG" "$*" >&2; }
run()  { if [ "$DRYRUN" -eq 1 ]; then printf '[dry-run] %s\n' "$*" >&2; return 0; fi; "$@"; }

while [ $# -gt 0 ]; do case "$1" in
    --new-be) NEW_BE="${2:?}"; shift 2 ;;
    --new-be=*) NEW_BE="${1#*=}"; shift ;;
    --set-bootfs) SET_BOOTFS=1; shift ;;
    --dry-run) DRYRUN=1; shift ;;
    -h|--help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    -*) die "unknown option: $1" ;;
    *) [ -z "$SNAP" ] || die "one snapshot only"; SNAP="$1"; shift ;;
esac; done

[ "$(id -u)" = 0 ] || die "must run as root"
command -v zfs >/dev/null 2>&1 || die "zfs not found"
[ -n "$SNAP" ] || die "need a <snapshot> (e.g. rpool/ROOT/alpine@2026-06-01-0000Z-DAILY)"
case "$SNAP" in *@*) : ;; *) die "not a snapshot (missing @): $SNAP" ;; esac
zfs list -H -o name -t snapshot "$SNAP" >/dev/null 2>&1 || die "no such snapshot: $SNAP"

SRC="${SNAP%@*}"                            # rpool/ROOT/alpine
TAG="${SNAP#*@}"                            # the snapshot name
POOL="${SRC%%/*}"
ROOTPREFIX="${SRC%/*}"
BASENAME="${SRC##*/}"
# Re-stamp the STEM (strip a trailing -YYYY-MM-DD-HHMMZ) so the rollback clone is
# <stem>-<UTC>Z (ADR 0012; no stamp stacking, mirrors keyboot_be_stem). Provenance
# (which snapshot it came from) goes in keyboot:* user props below, not the name.
case "$BASENAME" in
    *-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]Z)
        BASENAME="${BASENAME%-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]Z}" ;;
esac
[ -n "$NEW_BE" ] || NEW_BE="${BASENAME}-$(date -u +%Y-%m-%d-%H%MZ)"
case "$NEW_BE" in */*) DST="$NEW_BE" ;; *) DST="${ROOTPREFIX}/${NEW_BE}" ;; esac
[ "$DST" != "$SRC" ] || die "clone name equals the source BE"
zfs list -H -o name "$DST" >/dev/null 2>&1 && die "destination $DST already exists"

log "rolling $SRC back to @$TAG as a new BE: $DST"

# Recursive clone of the snapshot set (skip any child missing this snapshot,
# e.g. a dataset created after the snapshot was taken). Parent-first.
SETS="$(zfs list -H -o name -r "$SRC")" || die "zfs list failed"
cloned=0
for ds in $SETS; do
    suffix="${ds#"$SRC"}"
    if ! zfs list -H -o name -t snapshot "${ds}@${TAG}" >/dev/null 2>&1; then
        log "skip ${ds}: no @${TAG} snapshot"
        continue
    fi
    run zfs clone -o canmount=noauto "${ds}@${TAG}" "${DST}${suffix}" || die "clone ${ds} failed"
    cloned=$((cloned + 1))
done
[ "$cloned" -gt 0 ] || die "nothing cloned (snapshot set empty?)"
# Clone root must be mountpoint=/ (zfs clone doesn't copy the local mountpoint;
# children re-inherit). Without this be-unlock fails "no mountpoint set".
run zfs set mountpoint=/ "$DST" || die "set mountpoint=/ on clone failed"
# Provenance in user props (ADR 0012): how this BE was made + its source snapshot.
run zfs set keyboot:origin=rollback "keyboot:rolled-back-from=$SNAP" "$DST" || true

if [ "$SET_BOOTFS" -eq 1 ]; then
    run zpool set "bootfs=${DST}" "$POOL" && log "set bootfs=${DST} on ${POOL}"
fi
log "DONE. Rollback clone: ${DST}"
log "next: boot it ONCE:  keyboot-install be boot-next ${DST}    (or --set-bootfs already done)"
log "  keep it:           keyboot-install be promote ${POOL} ${DST}"
log "  discard:           zfs destroy -r ${DST}    (live BE + timeline untouched)"
