#!/bin/sh
# stacker installer — bootstrap a Linux box with a verified stacker binary
# and the self-update substrate, so ALL future updates ride
# `stacker self-update` (one update engine; this script is install-only).
#
#   curl -fsSL https://boxlet.app/stacker/install.sh | sudo sh
#
# What it does (mirrors the umbrella ansible role's substrate, minus the
# fleet-specific served-TUI service):
#   1. verify:  download the stable manifest, the binary, its .minisig and
#      SHA256SUMS; check sha256 AND the minisign signature INCLUDING the
#      trusted comment (version + filename pin) against the pinned pubkey
#      below. Nothing unverified is ever installed — same posture as
#      stacker's own self-update.
#   2. install: /usr/local/bin/stacker (root-owned, atomic rename).
#   3. substrate: /var/lib/stacker/update staging dir (writable by
#      STACKER_USER, default the sudo-ing user) + the stacker-install
#      path/service units, so `stacker self-update` works from day one.
#
# Env overrides:
#   STACKER_BASE_URL  distribution endpoint (default https://boxlet.app/stacker/download)
#   STACKER_USER      unprivileged user allowed to stage self-updates
#                     (default: $SUDO_USER; root-only staging if unset)
#
# The served TUI (`stacker serve` as stacker.service) is deliberately NOT
# installed here — it needs box-specific choices (Tailscale, authorized
# keys, Docker); the fleet gets it from the ansible role. self-update
# tolerates its absence (restart skipped).
set -eu

# ── pinned trust anchor (rotated 2026-07-22, key ID 847C9C519EA94F18) ──────
# Must match release/minisign.pub in the repo. On rotation: update BOTH in
# the same release.
PUBKEY="RWQYT6meUZx8hBg5jI7DRWPm51gQckO5gktp45KlTzKRihNwHZd/M3eM"

BASE_URL="${STACKER_BASE_URL:-https://boxlet.app/stacker/download}"
BIN_PATH="/usr/local/bin/stacker"
UPDATE_DIR="/var/lib/stacker/update"
STAGE_USER="${STACKER_USER:-${SUDO_USER:-}}"

say()  { printf '%s\n' "$*"; }
fail() { printf 'install.sh: %s\n' "$*" >&2; exit 1; }

[ "$(id -u)" = 0 ] || fail "must run as root (try: curl -fsSL ${BASE_URL%/download}/install.sh | sudo sh)"
[ "$(uname -s)" = Linux ] || fail "Linux only — macOS host support is tracked upstream, not shipped"

case "$(uname -m)" in
	x86_64)  ARCH=amd64 ;;
	aarch64) ARCH=arm64 ;;
	*) fail "unsupported architecture $(uname -m) (amd64/arm64 binaries are published)" ;;
esac

command -v curl >/dev/null 2>&1 || fail "curl is required"
command -v systemctl >/dev/null 2>&1 || fail "systemd is required (the self-update substrate is systemd units)"

# minisign is the verification tool; install it if the box lacks it.
if ! command -v minisign >/dev/null 2>&1; then
	if command -v apt-get >/dev/null 2>&1; then
		say "installing minisign (required to verify the release signature) …"
		apt-get update -q && apt-get install -qy minisign
	else
		fail "minisign is required to verify the download — install it and re-run"
	fi
fi

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
cd "$WORK"

say "fetching ${BASE_URL}/stable/latest.json …"
curl -fsSO "${BASE_URL}/stable/latest.json"
VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' latest.json | head -1)"
case "$VERSION" in
	v[0-9]*) say "latest stable: ${VERSION}" ;;
	*) fail "could not read a version from the manifest" ;;
esac

BIN="stacker-${VERSION}-linux-${ARCH}"
say "downloading ${BIN} …"
curl -fsSO "${BASE_URL}/${VERSION}/${BIN}"
curl -fsSO "${BASE_URL}/${VERSION}/${BIN}.minisig"
curl -fsSO "${BASE_URL}/${VERSION}/SHA256SUMS"

say "verifying sha256 …"
grep " ${BIN}\$" SHA256SUMS | sha256sum -c - >/dev/null || fail "sha256 mismatch"

say "verifying minisign signature + trusted comment …"
VOUT="$(minisign -Vm "$BIN" -P "$PUBKEY")" || fail "signature verification FAILED — refusing to install"
printf '%s\n' "$VOUT" | grep -q "boxlet stacker ${VERSION} ${BIN}" \
	|| fail "trusted comment does not pin ${VERSION} ${BIN} — possible replay; refusing to install"

say "installing ${BIN_PATH} …"
install -m 0755 "$BIN" "${BIN_PATH}.new"
mv -f "${BIN_PATH}.new" "$BIN_PATH"

say "installing the self-update substrate …"
mkdir -p "$UPDATE_DIR"
if [ -n "$STAGE_USER" ] && id "$STAGE_USER" >/dev/null 2>&1; then
	chown "$STAGE_USER" "$UPDATE_DIR"
	say "staging dir ${UPDATE_DIR} writable by ${STAGE_USER}"
else
	say "no unprivileged user resolved — self-update staging will need root"
fi
chmod 0755 "$UPDATE_DIR"

cat > /etc/systemd/system/stacker-install.path <<EOF
[Unit]
# stacker-install.path — the trigger half of stacker's self-update staged
# handoff. Installed by install.sh from boxlet.app (fleet boxes get the
# identical unit from the ansible role).
#
# \`stacker self-update\` (running unprivileged) verifies and stages a new
# binary in ${UPDATE_DIR}, then writes the command file LAST as its commit
# point. This unit watches ONLY that command file — never the directory —
# so temp files and the staged binary itself can't fire the installer early.
Description=Watch for staged stacker self-update commands
Documentation=https://forge.possibility.space/boxlet/stacker

[Path]
PathModified=${UPDATE_DIR}/command
Unit=stacker-install.service

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/stacker-install.service <<EOF
[Unit]
# stacker-install.service — the root half of stacker's self-update staged
# handoff. Triggered by stacker-install.path; never enabled or started
# directly.
#
# Runs the RUNNING (root-installed, trusted) stacker binary's hidden
# \`self-update apply\` verb: consume the command file, independently
# RE-VERIFY the staged binary against the public key embedded in the
# trusted binary (root never trusts the unprivileged stage), keep
# ${BIN_PATH}.prev for rollback, rename(2) the new binary into place,
# restart stacker.service if it exists. Version-guarded and idempotent.
#
# Recovery: if a bad update ever leaves the new stacker broken, rollback
# needs NO working stacker —
#   echo rollback > ${UPDATE_DIR}/command
# over plain SSH restores ${BIN_PATH}.prev.
Description=Verify and install a staged stacker self-update
Documentation=https://forge.possibility.space/boxlet/stacker

[Service]
Type=oneshot
ExecStart=${BIN_PATH} self-update apply
EOF

systemctl daemon-reload
systemctl enable --now stacker-install.path

say ""
say "installed: $("$BIN_PATH" version)"
say ""
say "next steps:"
say "  stacker self-update -check    # update visibility (any user)"
say "  stacker self-update           # verified update + root swap, forever"
say "  stacker new <name>            # needs Docker + Tailscale on this box"
say "note: the served TUI (stacker.service) is not installed by this script."
