#!/bin/bash

# omarchy:summary=Factory-reset this machine back to its freshly-installed state
# omarchy:group=setup
# omarchy:requires-sudo=true
# omarchy:examples=sudo omarchy-system-factory-reset

# Returns the machine to provisioning state — fully installed, no user, first-boot
# setup pending — the state you want before selling or handing the machine on.
#
# On machines installed with an ISO that created the @factory snapshot, the
# running root (@) is swapped for a fresh writable clone of @factory, so
# user-installed packages and /etc drift are gone too. Machines without
# @factory get a degraded reset: the current system is kept and only user
# accounts, home data, and machine identity are wiped.
#
# The heavy lifting happens on the next boot via omarchy-system-factory-reset-finish, staged
# here. On encrypted installs the LUKS volume is re-keyed to a throwaway
# passphrase with an auto-unlock keyfile for the provisioning window; first-boot setup
# re-keys it to the new owner's password and removes the keyfile.
#
# Honest limitations:
#  - On unencrypted disks this is deletion, not forensic erasure. fstrim helps
#    on SSDs; use blkdiscard + a reinstall if you need certainty.
#  - LUKS re-keying changes passphrases, not the volume key. Freed extents
#    remain readable to someone with raw-device access and a valid passphrase.
#    Use cryptsetup reencrypt or a reinstall if you need certainty.

set -euo pipefail

PROVISIONING_DIR=/var/lib/omarchy/provisioning
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
TOP_MNT=/run/omarchy-system-factory-reset/top
NEXT_NAME=@omarchy-reset-next
LOG_FILE=/var/log/omarchy-system-factory-reset.log

# Self-elevate so the menu entry (and a bare shell invocation) need no sudo
# prefix. The caller's gum theme vars are forwarded as `env` arguments rather
# than via `sudo -E`, so styling survives even under an env_reset sudoers.
if (( EUID != 0 )); then
  mapfile -t gum_env < <(env | grep '^GUM_' || true)
  exec sudo env "${gum_env[@]}" "$0" "$@"
fi

export PATH="$OMARCHY_PATH/bin:$PATH"

log() {
  echo "$1" | tee -a "$LOG_FILE" >/dev/null
  gum style --foreground 8 "  $1"
}

fail() {
  gum style --foreground 1 "Error: $1"
  exit 1
}

root_device() {
  findmnt -no SOURCE / | sed 's/\[.*\]//'
}

root_is_btrfs_at() {
  [[ $(findmnt -no FSTYPE /) == btrfs ]] && findmnt -no OPTIONS / | grep -q "subvol=/@\(,\|$\)"
}

# Resolve the crypto_LUKS partition backing the root, or return non-zero if
# the root is not on LUKS. Prefers the cmdline cryptdevice= spec (archinstall
# writes PARTUUID=, the pre-mounted path UUID=), and falls back to walking the
# device tree for roots reached via rd.luks/crypttab with a plain /dev/mapper
# root and no cryptdevice=.
luks_device() {
  local spec
  spec=$(grep -o 'cryptdevice=[^ :]*' /proc/cmdline | head -1 | cut -d= -f2-)
  case $spec in
    UUID=*) echo "/dev/disk/by-uuid/${spec#UUID=}"; return 0 ;;
    PARTUUID=*) echo "/dev/disk/by-partuuid/${spec#PARTUUID=}"; return 0 ;;
    LABEL=*) echo "/dev/disk/by-label/${spec#LABEL=}"; return 0 ;;
    PARTLABEL=*) echo "/dev/disk/by-partlabel/${spec#PARTLABEL=}"; return 0 ;;
    /dev/*) echo "$spec"; return 0 ;;
  esac

  local src part
  src=$(findmnt -no SOURCE / | sed 's/\[.*//')
  [[ -n $src ]] || return 1
  part=$(lsblk -nspo NAME,FSTYPE "$src" 2>/dev/null | awk '$2=="crypto_LUKS"{print $1; exit}')
  [[ -n $part ]] && { echo "$part"; return 0; }
  return 1
}

encrypted_install() {
  luks_device >/dev/null 2>&1
}

# No trailing `| head` stage: under pipefail an infinite tr killed by
# SIGPIPE fails the substitution and errexit aborts the whole reset.
generate_passphrase() {
  local raw
  raw=$(head -c 4096 /dev/urandom | tr -dc 'a-zA-Z0-9')
  printf '%s' "${raw:0:48}"
}

cleanup() {
  if (( ${swap_done:-0} == 0 )); then
    rollback_degraded_rekey
  fi
  if mountpoint -q "$TOP_MNT" 2>/dev/null; then
    if [[ -d $TOP_MNT/$NEXT_NAME && ${swap_done:-0} == 0 ]]; then
      btrfs subvolume delete --recursive "$TOP_MNT/$NEXT_NAME" >/dev/null 2>&1 || true
    fi
    umount -R "$TOP_MNT" 2>/dev/null || true
  fi
  rmdir "$TOP_MNT" 2>/dev/null || true
}
trap cleanup EXIT

confirm_reset() {
  local degraded="$1"

  echo
  gum style --bold --foreground 1 "Reset this computer to factory state?"
  echo
  gum style "This will permanently erase:"
  gum style "  • All user accounts and everything in /home"
  if [[ $degraded == true ]]; then
    gum style --foreground 3 "  • Machine identity (network connections, host keys, machine-id)"
    echo
    gum style --foreground 3 "This machine was installed before factory snapshots existed, so the"
    gum style --foreground 3 "current system (installed packages, /etc changes) is kept — only user"
    gum style --foreground 3 "accounts and machine state are wiped."
  else
    gum style "  • All packages and system changes made since installation"
    gum style "  • Machine identity (network connections, host keys, machine-id)"
  fi
  echo
  gum style "The next boot asks for a new user, exactly like a fresh install."
  gum style --foreground 8 "Note: on disks without encryption this is deletion, not secure erasure."
  echo

  local typed
  typed=$(gum input --placeholder "Type 'reset' to continue" --prompt "> ") || exit 1
  [[ $typed == "reset" ]] || fail "Reset not confirmed."
}

# Re-key to a throwaway passphrase whose keyfile auto-unlocks boot during the
# provisioning window. $1 is the root of the system the staging applies to ("" for the
# running root in degraded mode).
stage_luks_rekey() {
  local next="$1" device passphrase

  device=$(luks_device) || fail "encrypted install, but no usable cryptdevice= in /proc/cmdline"
  [[ -e $device ]] || fail "LUKS device $device not found"

  echo
  gum style "Confirm your disk encryption passphrase to authorize the re-key."
  local current
  while true; do
    current=$(gum input --password --placeholder "Current disk encryption passphrase" --prompt "Passphrase> ") || exit 1
    if cryptsetup open --test-passphrase --key-file <(printf '%s' "$current") "$device" 2>/dev/null; then
      break
    fi
    gum style --foreground 1 "That passphrase does not unlock $device. Try again."
  done

  passphrase=$(generate_passphrase)
  cryptsetup luksAddKey --key-file <(printf '%s' "$current") "$device" <(printf '%s' "$passphrase")

  install -d -m 755 "$next$PROVISIONING_DIR"
  printf '%s' "$passphrase" >"$next$PROVISIONING_DIR/luks-key"
  chmod 600 "$next$PROVISIONING_DIR/luks-key"

  install -d -m 755 "$next/etc/omarchy"
  printf '%s' "$passphrase" >"$next/etc/omarchy/provisioning.key"
  chmod 600 "$next/etc/omarchy/provisioning.key"

  install -d "$next/etc/limine-entry-tool.d" "$next/etc/mkinitcpio.conf.d"
  echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
    >"$next/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf"
  echo 'FILES+=(/etc/omarchy/provisioning.key)' \
    >"$next/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf"
}

install_provisioning_units() {
  local next="$1" unit_src="$2"

  install -Dm644 "$unit_src/omarchy-provision-owner.service" \
    "$next/etc/systemd/system/omarchy-provision-owner.service"
  install -Dm644 "$unit_src/omarchy-system-factory-reset-finish.service" \
    "$next/etc/systemd/system/omarchy-system-factory-reset-finish.service"

  install -d "$next/etc/systemd/system/multi-user.target.wants" \
    "$next/etc/systemd/system/sysinit.target.wants"
  ln -sf /etc/systemd/system/omarchy-provision-owner.service \
    "$next/etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service"
  ln -sf /etc/systemd/system/omarchy-system-factory-reset-finish.service \
    "$next/etc/systemd/system/sysinit.target.wants/omarchy-system-factory-reset-finish.service"
}

# Start the ESP's limine.conf over from the shipped template and drop the
# previous Omarchy identity's state. limine-entry-tool keys OS entries by
# machine-id, so after a reset gives the machine a fresh identity, the
# previous system's entry (with the now-stale UKI hash) would survive, sort
# first, and make Limine refuse the rebuilt UKI with a hash-mismatch warning.
# Only machine-ids the old limine.conf referenced are removed — a shared ESP
# may hold other installations' machine-id directories.
reset_limine_config() {
  local root="$1" esp="$2" template found=""

  local old_ids=""
  if [[ -f $root$esp/limine.conf ]]; then
    old_ids=$(grep -o 'machine-id=[0-9a-f]\{32\}' "$root$esp/limine.conf" | cut -d= -f2 | sort -u)
  fi

  for template in "$root/usr/share/omarchy/install/assets/limine/limine.conf" \
                  "$root/usr/share/omarchy/default/limine/limine.conf"; do
    if [[ -f $template ]]; then
      cp "$template" "$root$esp/limine.conf"
      found=1
      break
    fi
  done
  [[ -n $found ]] || fail "no limine.conf template in $root/usr/share/omarchy"

  local machine_id old_id
  machine_id=$(cat "$root/etc/machine-id" 2>/dev/null || true)
  for old_id in $old_ids; do
    [[ $old_id == "$machine_id" ]] && continue
    rm -rf "${root:?}$esp/$old_id"
  done
}

# Every UKI entry in limine.conf carries a #blake2b hash of the file it
# points at; a mismatch makes Limine stop at a scary warning. Never hand over
# a staged reset in that state.
verify_limine_hashes() {
  local root="$1" esp="$2" line path hash file
  while IFS= read -r line; do
    path=${line#*boot():}
    path=${path%%#*}
    hash=${line##*#}
    file="$root$esp$path"
    [[ -f $file ]] || fail "limine.conf points at missing $path"
    [[ $(b2sum "$file" | cut -d' ' -f1) == "$hash" ]] ||
      fail "limine.conf hash for $path does not match the rebuilt file"
  done < <(grep -o 'boot():/EFI/Linux/[^#]*#[0-9a-f]*' "$root$esp/limine.conf")
}

# Rebuild the UKI from inside the next root so the deployed kernel/initramfs
# match the factory root's modules (the seller may have updated kernels since
# install) and embed the auto-unlock keyfile on encrypted machines.
rebuild_next_boot() {
  local next="$1"

  local esp_source esp_mount
  esp_mount=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $2; exit }' "$next/etc/fstab")
  esp_source=$(awk '$3 == "vfat" && $2 ~ /^\/(boot|efi)/ { print $1; exit }' "$next/etc/fstab")
  [[ -n $esp_mount && -n $esp_source ]] || fail "could not find the ESP in $next/etc/fstab"

  local esp_device=$esp_source
  if [[ $esp_source == UUID=* ]]; then
    esp_device="/dev/disk/by-uuid/${esp_source#UUID=}"
  fi
  [[ -e $esp_device ]] || fail "ESP device $esp_device not found"

  mount "$esp_device" "$next$esp_mount"

  local dir
  for dir in proc sys dev run; do
    mount --rbind "/$dir" "$next/$dir"
    mount --make-rslave "$next/$dir"
  done

  # Btrfs snapshots do not recurse into nested subvolumes, so the factory
  # root carries only an empty /swap directory where the hibernation swapfile
  # lived. Recreate it and refresh the resume offset before the UKI build
  # bakes the cmdline; the empty placeholder directory has to go first so the
  # subvolume can be recreated.
  if [[ -x $next/usr/bin/omarchy-hibernation-setup ]] && grep -q "/swap/swapfile" "$next/etc/fstab"; then
    log "Recreating the hibernation swapfile in the factory system"
    if [[ -d $next/swap ]] && ! btrfs subvolume show "$next/swap" >/dev/null 2>&1; then
      rmdir "$next/swap" 2>/dev/null || true
    fi
    # The factory root still carries the resume drop-ins from install, which
    # make hibernation-setup conclude it has nothing to do; with the swapfile
    # gone they must go so it reconfigures from scratch. The resume-offset
    # drop-in especially: hibernation-setup only recomputes the physical
    # offset when it is absent, so a surviving one bakes the old swapfile's
    # offset into the rebuilt UKI and breaks resume.
    if [[ ! -f $next/swap/swapfile ]]; then
      rm -f "$next/etc/mkinitcpio.conf.d/omarchy_resume.conf" \
        "$next/etc/limine-entry-tool.d/resume.conf"
    fi
    if ! chroot "$next" env OMARCHY_PATH=/usr/share/omarchy \
      /usr/bin/omarchy-hibernation-setup --force --no-rebuild >>"$LOG_FILE" 2>&1; then
      fail "hibernation setup failed in the factory root (see $LOG_FILE)"
    fi
    [[ -f $next/swap/swapfile ]] ||
      fail "hibernation setup did not recreate $next/swap/swapfile (see $LOG_FILE)"
  fi

  reset_limine_config "$next" "$esp_mount"

  log "Rebuilding boot files from the factory system (this can take a minute)"
  if ! chroot "$next" /usr/bin/limine-update >>"$LOG_FILE" 2>&1; then
    fail "limine-update failed in the factory root (see $LOG_FILE)"
  fi

  verify_limine_hashes "$next" "$esp_mount"

  for dir in run dev sys proc; do
    umount -R "$next/$dir" 2>/dev/null || true
  done
  umount "$next$esp_mount"
}

# Remove the seller's account material and machine identity from the retained
# @factory baseline so it can neither be mounted for recovery nor restore the
# seller's account on a future reset. Idempotent (a scrubbed baseline has no
# uid>=1000 accounts left to remove).
sanitize_factory_baseline() {
  local factory="$1" user
  btrfs property set -ts "$factory" ro false

  for user in $(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$factory/etc/passwd"); do
    userdel --root "$factory" "$user" 2>>"$LOG_FILE" || true
    rm -rf "${factory:?}/home/$user"
  done
  passwd --root "$factory" --lock root >>"$LOG_FILE" 2>&1 || true
  rm -f "$factory"/etc/ssh/ssh_host_*
  rm -f "$factory"/etc/NetworkManager/system-connections/*
  rm -rf "$factory"/var/lib/NetworkManager/* "$factory/var/lib/tailscale" "$factory/var/lib/iwd"
  rm -f "$factory/var/lib/sddm/state.conf" "$factory/etc/sddm.conf.d/autologin.conf"
  : >"$factory/etc/machine-id"

  btrfs property set -ts "$factory" ro true
}

stage_full_reset() {
  local top="$TOP_MNT" next="$TOP_MNT/$NEXT_NAME"

  log "Cloning the factory snapshot"
  [[ -d $next ]] && btrfs subvolume delete --recursive "$next" >/dev/null
  btrfs subvolume snapshot "$top/@factory" "$next" >>"$LOG_FILE"

  local unit_src="$next/usr/share/omarchy/install/provisioning"
  [[ -f $unit_src/omarchy-provision-owner.service && -x $next/usr/bin/omarchy-provision-owner ]] ||
    fail "the factory snapshot predates provisioning support; cannot reset from it"

  log "Scrubbing machine identity from the factory system"
  systemd-id128 new >"$next/etc/machine-id"
  rm -f "$next"/etc/ssh/ssh_host_*
  rm -f "$next"/etc/NetworkManager/system-connections/*
  rm -rf "$next"/var/lib/NetworkManager/* "$next/var/lib/tailscale" "$next/var/lib/iwd"
  rm -f "$next/var/lib/sddm/state.conf" "$next/etc/sddm.conf.d/autologin.conf"

  # A factory snapshot from a normal (normal) install contains the original
  # user account; first-boot setup must start from none. A leftover account
  # would keep its password hash and group memberships (including wheel), so
  # failure here has to abort the reset, not be shrugged off.
  local user
  for user in $(awk -F: '$3 >= 1000 && $3 < 60000 { print $1 }' "$next/etc/passwd"); do
    log "Removing user $user from the factory system"
    userdel --root "$next" "$user" 2>>"$LOG_FILE" ||
      fail "could not remove user $user from the factory system (see $LOG_FILE)"
  done
  passwd --root "$next" --lock root >>"$LOG_FILE" 2>&1 || true

  # @factory itself survives the wipe as the baseline for future resets. If it
  # came from a normal install it still holds the seller's account and
  # /etc/shadow, which the new wheel user could mount and read — and a second
  # reset would restore that account. Scrub it once, in place.
  sanitize_factory_baseline "$top/@factory"

  # Keep the Node tarball reachable for offline first-boot finalization.
  if ! compgen -G "$next$PROVISIONING_DIR/packages/node-v*.tar.gz" >/dev/null; then
    if compgen -G "$PROVISIONING_DIR/packages/node-v*.tar.gz" >/dev/null; then
      install -d -m 755 "$next$PROVISIONING_DIR/packages"
      cp "$PROVISIONING_DIR"/packages/node-v*.tar.gz "$next$PROVISIONING_DIR/packages/"
    fi
  fi

  install -d -m 755 "$next$PROVISIONING_DIR"
  touch "$next$PROVISIONING_DIR/pending" "$next$PROVISIONING_DIR/wipe-pending"

  install_provisioning_units "$next" "$unit_src"

  if encrypted_install; then
    stage_luks_rekey "$next"
  fi

  rebuild_next_boot "$next"

  log "Activating the factory system"
  local old="@omarchy-old-$(date +%s)"
  mv "$top/@" "$top/$old"
  mv "$next" "$top/@"
  swap_done=1
  sync
}

stage_degraded_reset() {
  local unit_src="$OMARCHY_PATH/install/provisioning"
  [[ -f $unit_src/omarchy-provision-owner.service && -x /usr/bin/omarchy-provision-owner ]] ||
    fail "this Omarchy version does not ship provisioning; update first"

  # Fallible staging (re-key, boot rebuild) happens before the wipe is armed:
  # a failure must leave the machine untouched, not schedule a wipe for a
  # reset that never finished staging. The degraded path stages into the LIVE
  # root, so a mid-staging failure must also undo the auto-unlock material it
  # already wrote — otherwise a later boot rebuild would silently embed it.
  install -d -m 755 "$PROVISIONING_DIR"
  if encrypted_install; then
    degraded_rekey_staged=1
    stage_luks_rekey ""
    log "Rebuilding boot files with the provisioning unlock key"
    if ! limine-update >>"$LOG_FILE" 2>&1; then
      fail "limine-update failed (see $LOG_FILE)"
    fi
    degraded_rekey_staged=0
  fi

  install_provisioning_units "" "$unit_src"
  touch "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/wipe-pending" "$PROVISIONING_DIR/wipe-degraded"

  swap_done=1
}

# Undo the live-root auto-unlock material a failed degraded rekey left behind.
rollback_degraded_rekey() {
  (( ${degraded_rekey_staged:-0} )) || return 0
  rm -f /etc/omarchy/provisioning.key "$PROVISIONING_DIR/luks-key" \
    /etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf \
    /etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf
  limine-update >>"$LOG_FILE" 2>&1 || true
}

main() {
  omarchy-cmd-present btrfs || { echo "Error: btrfs-progs is required" >&2; exit 1; }

  root_is_btrfs_at || fail "reset requires the standard Omarchy Btrfs layout (subvol=@)"

  touch "$LOG_FILE"
  chmod 600 "$LOG_FILE"
  swap_done=0

  local device
  device=$(root_device)
  [[ -n $device ]] || fail "could not determine the root device"

  mkdir -p "$TOP_MNT"
  mountpoint -q "$TOP_MNT" || mount -o subvolid=5 "$device" "$TOP_MNT"

  local degraded=false
  [[ -d $TOP_MNT/@factory ]] || degraded=true

  confirm_reset "$degraded"

  # The running system's limine-snapper-sync must not rewrite the ESP's
  # limine.conf behind the staged rebuild (subvolume changes below can
  # trigger it). The runtime mask evaporates on the reboot that follows.
  systemctl mask --runtime --now limine-snapper-sync.service >/dev/null 2>&1 || true
  systemctl mask --runtime --now limine-snapper-sync.path >/dev/null 2>&1 || true

  if [[ $degraded == true ]]; then
    stage_degraded_reset
  else
    stage_full_reset
  fi

  umount -R "$TOP_MNT" 2>/dev/null || true

  echo
  gum style --bold "Reset staged. The wipe finishes on the next boot."
  if gum confirm --affirmative "Reboot now" --negative "Reboot later" "Reboot to complete the reset?"; then
    systemctl reboot
  else
    gum style --foreground 3 "Do not keep using this machine — changes made now will be lost."
  fi
}

main "$@"
