#!/usr/bin/env bash
# debian-os-setup 0.9.0 — phases 1–3 of debian-os-setup.md (not GDM login / Settings / Astro).
# Class: pipeable
# Brace group: bash slurps the whole file from a pipe before running (curl | sudo bash).
{
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

VERSION="0.9.0"
STATE_DIR="/var/lib/debian-os-setup"
STATE_FILE="${STATE_DIR}/state"
NODESOURCE_SETUP_URL="https://deb.nodesource.com/setup_24.x"

GNOME_PKGS=(
  gnome-shell gdm3 gnome-terminal gnome-text-editor gnome-calculator
  gnome-system-monitor network-manager network-manager-gnome gnome-keyring
  curl amberol mpv loupe krita cifs-utils git
)
C_PKGS=(build-essential pkgconf cmake meson ninja-build)
CLANG_PKGS=(clang clang-tidy clang-format)
CROSS_PKGS=(gcc-aarch64-linux-gnu)
PYTHON_PKGS=(
  python3 python3-venv python3-dev python3-pip python3-setuptools python3-wheel
)
PYTHON_HELPER_PKGS=(dh-python python3-bandit)
PYTHON_DS_PKGS=(
  python3-numpy python3-pandas python3-scipy python3-matplotlib python3-seaborn
  python3-cartopy python3-xarray python3-geopandas python3-pyzipper python3-sklearn
  python3-plotly python3-pydantic python3-markdown python3-pypandoc python3-ipywidgets
  jupyter-nbconvert
)
PYTHON_DEBUG_PKGS=(
  python3-debugpy python3-ipython python3-jupyter-core python3-ipykernel python3-pytest
)
NODE_APT_PKGS=(nodejs npm dh-nodejs pkg-js-tools)
RUSTUP_PKGS=(rustup libssl-dev)
RUST_APT_PKGS=(rustc cargo dh-cargo debcargo libssl-dev)
PACKAGING_PKGS=(
  dpkg-dev fakeroot debhelper dh-make debmake devscripts quilt lintian sbuild
  mmdebstrap autopkgtest piuparts adequate duck blhc diffoscope git-buildpackage
  git-debpush dgit dput-ng reportbug desktop-file-utils appstream debian-policy
  debmake-doc gnupg
)
FIRMWARE_PKGS=(firmware-linux-free firmware-linux-nonfree)

GNOME=1
C_TOOLCHAIN=0
CLANG=0
CROSS=0
PYTHON=0
PYTHON_DS=0
PYTHON_DEBUG=0
NODE="none"
RUST="none"
PACKAGING=0
FIRMWARE=0
GRUB=1
TEMPLATES=1
HANDOVER=1
REBOOT=1
DRY_RUN=0
AFTER_LOGIN=0
DEV=0
DEV_ALL=0
C_TOOLCHAIN_SET=0
PYTHON_SET=0
NODE_SET=0
RUST_SET=0
HANDOVER_MUTATED=0

if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then
  SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
else
  SCRIPT_PATH=""
fi

err() { printf '%s\n' "$*" >&2; }
log() { printf '%s\n' "$*"; }

die_usage() {
  err "error: $*"
  err "Try --help for flags and invoke."
  exit 1
}

die_run() {
  err "error: $*"
  exit 2
}

usage() {
  cat <<EOF
debian-os-setup ${VERSION} — automate debian-os-setup.md phases 1–3 (not phase 4).

Invoke (root from the start; flags after bash -s --, not on curl):
  curl -fsSL https://debian.reporet.com/setup.sh | sudo bash
  curl -fsSL https://debian.reporet.com/setup.sh | sudo bash -s -- --dev
  curl -fsSL https://debian.reporet.com/setup.sh | sudo bash -s -- --dev-all
  curl -fsSL https://debian.reporet.com/setup.sh | sudo bash -s -- --no-reboot

  curl -fsSL https://debian.reporet.com/setup.sh -o /tmp/debian-os-setup.sh
  sudo bash /tmp/debian-os-setup.sh --dev-all

  sudo bash scripts/setup/setup.sh
  sudo bash scripts/setup/setup.sh --dev-all

Do not use curl | bash without sudo (cannot re-exec a pipe).
Do not write curl …/setup.sh --dev | sudo bash (curl consumes --dev).

Modes:
  standard (default)  GNOME, GRUB colors, templates, handover; no language toolchains
  --dev               standard plus C toolchain, Python, NodeSource 24, rustup
  --dev-all           --dev plus clang, aarch64 cross, Python DS/debug, packaging
                      (firmware stays opt-in; all apt still runs this TTY, no reboot oneshot)

Options:
  --dev                             Developer preset (C, Python, NodeSource 24, rustup)
  --dev-all                         Full developer preset except firmware
  --gnome / --no-gnome              Minimal GNOME apt list (default: on)
  --c-toolchain / --no-c-toolchain  build-essential pkgconf cmake meson ninja-build
                                    (default: off; on with --dev / --dev-all)
  --clang                           clang clang-tidy clang-format
                                    (default: off; on with --dev-all)
  --cross                           gcc-aarch64-linux-gnu (default: off; on with --dev-all)
  --python / --no-python            Python interpreter + dh-python/bandit
                                    (default: off; on with --dev / --dev-all)
  --python-ds                       Optional DS/ML apt list (default: off; on with --dev-all)
  --python-debug                    Optional debug apt list (default: off; on with --dev-all)
  --node=nodesource|apt|none        Node path (default: none; nodesource with --dev / --dev-all)
  --rust=rustup|apt|none            Rust path (default: none; rustup with --dev / --dev-all)
  --packaging                       Debian packaging apt list (default: off; on with --dev-all)
  --firmware                        firmware-linux-free/nonfree if sources allow (default: off)
  --grub-colors / --no-grub-colors  /etc/default/grub colors + update-grub (default: on)
  --templates / --no-templates      Login-user ~/Templates files (default: on)
  --handover / --no-handover        graphical.target, loopback-only interfaces, enable NM (default: on)
  --reboot / --no-reboot            Reboot if handover changed this run (default: on if handover ran)
  --dry-run                         Print commands; no apt, writes, or reboot
  --after-login                     Verify-only (versions, get-default, NM); no installs
  --help                            Print this text and exit 0

Exit codes: 0 success (including scheduled reboot); 1 usage/preflight; 2 runtime.
Canonical lists and NM/GDM rules: debian-os-setup.md
EOF
}

set_node() {
  local v=$1
  case "$v" in
    nodesource | apt | none) ;;
    *) die_usage "invalid --node=${v} (want nodesource|apt|none)" ;;
  esac
  if ((NODE_SET)) && [[ "$NODE" != "$v" ]]; then
    die_usage "conflicting --node values (${NODE} vs ${v})"
  fi
  NODE=$v
  NODE_SET=1
}

set_rust() {
  local v=$1
  case "$v" in
    rustup | apt | none) ;;
    *) die_usage "invalid --rust=${v} (want rustup|apt|none)" ;;
  esac
  if ((RUST_SET)) && [[ "$RUST" != "$v" ]]; then
    die_usage "conflicting --rust values (${RUST} vs ${v})"
  fi
  RUST=$v
  RUST_SET=1
}

apply_mode_defaults() {
  if ((DEV == 0 && DEV_ALL == 0)); then
    return 0
  fi
  ((C_TOOLCHAIN_SET)) || C_TOOLCHAIN=1
  ((PYTHON_SET)) || PYTHON=1
  ((NODE_SET)) || NODE=nodesource
  ((RUST_SET)) || RUST=rustup
  if ((DEV_ALL)); then
    CLANG=1
    CROSS=1
    PYTHON_DS=1
    PYTHON_DEBUG=1
    PACKAGING=1
  fi
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --help)
        usage
        exit 0
        ;;
      --dry-run) DRY_RUN=1 ;;
      --after-login) AFTER_LOGIN=1 ;;
      --dev) DEV=1 ;;
      --dev-all)
        DEV=1
        DEV_ALL=1
        ;;
      --gnome) GNOME=1 ;;
      --no-gnome) GNOME=0 ;;
      --c-toolchain)
        C_TOOLCHAIN=1
        C_TOOLCHAIN_SET=1
        ;;
      --no-c-toolchain)
        C_TOOLCHAIN=0
        C_TOOLCHAIN_SET=1
        ;;
      --clang) CLANG=1 ;;
      --cross) CROSS=1 ;;
      --python)
        PYTHON=1
        PYTHON_SET=1
        ;;
      --no-python)
        PYTHON=0
        PYTHON_SET=1
        ;;
      --python-ds) PYTHON_DS=1 ;;
      --python-debug) PYTHON_DEBUG=1 ;;
      --packaging) PACKAGING=1 ;;
      --firmware) FIRMWARE=1 ;;
      --grub-colors) GRUB=1 ;;
      --no-grub-colors) GRUB=0 ;;
      --templates) TEMPLATES=1 ;;
      --no-templates) TEMPLATES=0 ;;
      --handover) HANDOVER=1 ;;
      --no-handover) HANDOVER=0 ;;
      --reboot) REBOOT=1 ;;
      --no-reboot) REBOOT=0 ;;
      --node=*) set_node "${1#--node=}" ;;
      --node)
        [[ $# -ge 2 ]] || die_usage "--node requires nodesource|apt|none"
        set_node "$2"
        shift
        ;;
      --rust=*) set_rust "${1#--rust=}" ;;
      --rust)
        [[ $# -ge 2 ]] || die_usage "--rust requires rustup|apt|none"
        set_rust "$2"
        shift
        ;;
      --*) die_usage "unknown option: $1" ;;
      *) die_usage "unexpected argument: $1" ;;
    esac
    shift
  done
}

login_user() {
  if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != root ]]; then
    printf '%s\n' "$SUDO_USER"
    return 0
  fi
  local u
  u=$(logname 2>/dev/null || true)
  if [[ -n "$u" && "$u" != root ]]; then
    printf '%s\n' "$u"
    return 0
  fi
  if [[ -n "${USER:-}" && "${USER}" != root ]]; then
    printf '%s\n' "$USER"
    return 0
  fi
  printf 'root\n'
}

login_home() {
  local u home
  u=$(login_user)
  home=$(getent passwd "$u" | awk -F: '{print $6}')
  if [[ -n "$home" ]]; then
    printf '%s\n' "$home"
    return 0
  fi
  printf '/home/%s\n' "$u"
}

run_as_login() {
  local user
  user=$(login_user)
  if [[ "$user" == root ]] || [[ "$(id -un)" == "$user" ]]; then
    "$@"
    return
  fi
  sudo -u "$user" -H -- "$@"
}

pkg_installed() {
  local st
  st=$(dpkg-query -W -f='${Status}' "$1" 2>/dev/null) || return 1
  [[ "$st" == "install ok installed" ]]
}

all_installed() {
  local p
  for p in "$@"; do
    pkg_installed "$p" || return 1
  done
  return 0
}

apt_install() {
  if [[ $# -eq 0 ]]; then
    return 0
  fi
  if all_installed "$@"; then
    log "Already installed: $*"
    return 0
  fi
  if ((DRY_RUN)); then
    printf 'dry-run: apt-get install -y'
    printf ' %q' "$@"
    printf '\n'
    return 0
  fi
  apt-get install -y "$@" </dev/null || die_run "apt-get install failed"
}

need_package_phase() {
  if ((GNOME || C_TOOLCHAIN || CLANG || CROSS || PYTHON || PYTHON_DS || PYTHON_DEBUG || PACKAGING || FIRMWARE)); then
    return 0
  fi
  [[ "$NODE" != none || "$RUST" != none ]]
}

only_firmware_requested() {
  ((FIRMWARE)) || return 1
  ((GNOME == 0 && C_TOOLCHAIN == 0 && CLANG == 0 && CROSS == 0 && PYTHON == 0 && PYTHON_DS == 0 && PYTHON_DEBUG == 0 && PACKAGING == 0 && GRUB == 0 && TEMPLATES == 0 && HANDOVER == 0)) || return 1
  [[ "$NODE" == none && "$RUST" == none ]]
}

sources_have_nonfree_firmware() {
  grep -Rqs -- 'non-free-firmware' /etc/apt/sources.list /etc/apt/sources.list.d
}

have_network() {
  if command -v ip >/dev/null 2>&1 && ip route show default 2>/dev/null | grep -q .; then
    return 0
  fi
  local d iface carrier
  for d in /sys/class/net/*; do
    [[ -e "$d" ]] || continue
    iface=$(basename "$d")
    [[ "$iface" != lo ]] || continue
    carrier=$(cat "$d/carrier" 2>/dev/null || printf '0')
    if [[ "$carrier" == 1 ]]; then
      return 0
    fi
  done
  return 1
}

node_is_v24() {
  local v
  command -v node >/dev/null 2>&1 || return 1
  v=$(node --version 2>/dev/null || true)
  [[ "$v" == v24* ]]
}

interfaces_is_loopback_only() {
  local f=/etc/network/interfaces
  local got
  [[ -f "$f" ]] || return 1
  got=$(grep -vE '^[[:space:]]*(#|$)' "$f" | sed 's/[[:space:]]*$//')
  [[ "$got" == $'auto lo\niface lo inet loopback' ]]
}

record_state() {
  local fence=$1
  ((DRY_RUN)) && return 0
  ((EUID == 0)) || return 0
  mkdir -p "$STATE_DIR"
  if [[ -f "$STATE_FILE" ]] && grep -qx "$fence" "$STATE_FILE"; then
    return 0
  fi
  printf '%s\n' "$fence" >>"$STATE_FILE"
}

preflight() {
  if [[ ! -f /etc/os-release ]]; then
    die_usage "cannot read /etc/os-release; Debian is required"
  fi
  # ID=debian only; Ubuntu and derivatives set a different ID.
  # shellcheck source=/dev/null
  . /etc/os-release
  if [[ "${ID:-}" != debian ]]; then
    die_usage "this script requires Debian (ID=debian); refusing ID=${ID:-unknown} (Ubuntu is not supported)"
  fi
  export DEBIAN_FRONTEND=noninteractive
  if ((AFTER_LOGIN)); then
    return 0
  fi
  if ! have_network; then
    if ((DRY_RUN)); then
      err "warn: no default route or carrier; dry-run continues"
    else
      die_usage "no network (no default route). Keep ifupdown until every download finishes"
    fi
  fi
}

apt_update_once() {
  if ((DRY_RUN)); then
    printf 'dry-run: apt-get update\n'
    return 0
  fi
  apt-get update </dev/null || die_run "apt-get update failed"
}

install_gnome() {
  ((GNOME)) || return 0
  log "Fence: GNOME (will not enable or start gdm3)"
  apt_install "${GNOME_PKGS[@]}"
  record_state gnome
}

install_c_toolchain() {
  ((C_TOOLCHAIN)) || return 0
  log "Fence: C/C++ toolchain"
  apt_install "${C_PKGS[@]}"
  record_state c-toolchain
}

install_clang() {
  ((CLANG)) || return 0
  log "Fence: clang"
  apt_install "${CLANG_PKGS[@]}"
  record_state clang
}

install_cross() {
  ((CROSS)) || return 0
  log "Fence: aarch64 cross gcc"
  apt_install "${CROSS_PKGS[@]}"
  record_state cross
}

install_python() {
  ((PYTHON)) || return 0
  log "Fence: Python (apt only; no pip into /usr)"
  apt_install "${PYTHON_PKGS[@]}"
  apt_install "${PYTHON_HELPER_PKGS[@]}"
  record_state python
}

install_python_ds() {
  ((PYTHON_DS)) || return 0
  log "Fence: optional Python DS/ML"
  apt_install "${PYTHON_DS_PKGS[@]}"
  record_state python-ds
}

install_python_debug() {
  ((PYTHON_DEBUG)) || return 0
  log "Fence: optional Python debug"
  apt_install "${PYTHON_DEBUG_PKGS[@]}"
  record_state python-debug
}

install_node() {
  local ns_tmp ver
  case "$NODE" in
    none)
      if ((NODE_SET)); then
        log "Fence: Node skipped (--node=none)"
      fi
      return 0
      ;;
    apt)
      log "Fence: Node from Debian apt"
      apt_install "${NODE_APT_PKGS[@]}"
      record_state node-apt
      ;;
    nodesource)
      log "Fence: Node 24 via NodeSource extra repo"
      if node_is_v24; then
        log "node $(node --version) already present; skipping NodeSource setup"
        record_state node-nodesource
        return 0
      fi
      if ! command -v curl >/dev/null 2>&1; then
        apt_install curl
      fi
      if ((DRY_RUN)); then
        printf 'dry-run: curl -fsSL %s -o <tempfile>\n' "$NODESOURCE_SETUP_URL"
        printf 'dry-run: bash <tempfile>\n'
        printf 'dry-run: apt-get install -y nodejs\n'
        return 0
      fi
      ns_tmp=$(mktemp)
      if ! curl -fsSL "$NODESOURCE_SETUP_URL" -o "$ns_tmp"; then
        rm -f "$ns_tmp"
        die_run "failed to download NodeSource setup_24.x"
      fi
      if ! bash "$ns_tmp" </dev/null; then
        rm -f "$ns_tmp"
        die_run "NodeSource setup_24.x failed"
      fi
      rm -f "$ns_tmp"
      apt-get install -y nodejs </dev/null || die_run "apt-get install nodejs failed"
      command -v node >/dev/null 2>&1 || die_run "node missing after NodeSource install"
      ver=$(node --version)
      if [[ "$ver" != v24* ]]; then
        die_run "expected node v24.x, got ${ver}"
      fi
      log "node ${ver}"
      record_state node-nodesource
      ;;
  esac
}

install_rust() {
  local user
  user=$(login_user)
  case "$RUST" in
    none)
      if ((RUST_SET)); then
        log "Fence: Rust skipped (--rust=none)"
      fi
      return 0
      ;;
    apt)
      log "Fence: Rust from Debian apt (do not prepend ~/.cargo/bin)"
      apt_install "${RUST_APT_PKGS[@]}"
      record_state rust-apt
      ;;
    rustup)
      log "Fence: rustup as login user ${user}"
      apt_install "${RUSTUP_PKGS[@]}"
      if ((DRY_RUN)); then
        printf 'dry-run: sudo -u %q -H rustup default stable\n' "$user"
        return 0
      fi
      run_as_login rustup default stable || die_run "rustup default stable failed for ${user}"
      record_state rust-rustup
      ;;
  esac
}

install_packaging() {
  ((PACKAGING)) || return 0
  log "Fence: Debian packaging toolchain"
  apt_install "${PACKAGING_PKGS[@]}"
  record_state packaging
}

install_firmware() {
  ((FIRMWARE)) || return 0
  log "Fence: optional firmware"
  if ! sources_have_nonfree_firmware; then
    err "warn: APT sources lack non-free-firmware; skipping firmware packages"
    err "Add that component, run apt-get update once, and retry --firmware (debian-os-setup.md)."
    return 0
  fi
  if all_installed "${FIRMWARE_PKGS[@]}"; then
    log "Already installed: ${FIRMWARE_PKGS[*]}"
    record_state firmware
    return 0
  fi
  if ((DRY_RUN)); then
    printf 'dry-run: apt-get install -y'
    printf ' %q' "${FIRMWARE_PKGS[@]}"
    printf '\n'
    return 0
  fi
  if ! apt-get install -y "${FIRMWARE_PKGS[@]}" </dev/null; then
    err "Firmware packages missing. Add non-free-firmware to APT sources if needed, apt-get update once, retry --firmware."
    if only_firmware_requested; then
      die_run "firmware fence failed"
    fi
    return 0
  fi
  record_state firmware
}

configure_grub() {
  ((GRUB)) || return 0
  local file=/etc/default/grub
  log "Fence: GRUB colors"
  if [[ ! -f "$file" ]]; then
    err "warn: ${file} missing; skipping GRUB colors"
    return 0
  fi
  if grep -qx 'GRUB_COLOR_NORMAL="light-gray/black"' "$file" &&
    grep -qx 'GRUB_COLOR_HIGHLIGHT="white/black"' "$file"; then
    log "GRUB colors already set"
    record_state grub
    return 0
  fi
  if ((DRY_RUN)); then
    printf 'dry-run: set GRUB_COLOR_NORMAL and GRUB_COLOR_HIGHLIGHT in %s\n' "$file"
    printf 'dry-run: update-grub\n'
    return 0
  fi
  if grep -q '^GRUB_COLOR_NORMAL=' "$file"; then
    sed -i 's|^GRUB_COLOR_NORMAL=.*|GRUB_COLOR_NORMAL="light-gray/black"|' "$file"
  elif grep -q '^#GRUB_COLOR_NORMAL=' "$file"; then
    sed -i 's|^#GRUB_COLOR_NORMAL=.*|GRUB_COLOR_NORMAL="light-gray/black"|' "$file"
  else
    printf 'GRUB_COLOR_NORMAL="light-gray/black"\n' >>"$file"
  fi
  if grep -q '^GRUB_COLOR_HIGHLIGHT=' "$file"; then
    sed -i 's|^GRUB_COLOR_HIGHLIGHT=.*|GRUB_COLOR_HIGHLIGHT="white/black"|' "$file"
  elif grep -q '^#GRUB_COLOR_HIGHLIGHT=' "$file"; then
    sed -i 's|^#GRUB_COLOR_HIGHLIGHT=.*|GRUB_COLOR_HIGHLIGHT="white/black"|' "$file"
  else
    printf 'GRUB_COLOR_HIGHLIGHT="white/black"\n' >>"$file"
  fi
  update-grub || die_run "update-grub failed"
  record_state grub
}

configure_templates() {
  ((TEMPLATES)) || return 0
  local user home dir
  user=$(login_user)
  home=$(login_home)
  dir="${home}/Templates"
  log "Fence: Nautilus templates in ${dir} (user ${user})"
  if ((DRY_RUN)); then
    printf 'dry-run: mkdir -p %q\n' "$dir"
    printf 'dry-run: touch %q/markdown.md %q/python.py %q/shell.sh %q/text.txt\n' "$dir" "$dir" "$dir" "$dir"
    return 0
  fi
  mkdir -p "$dir" || die_run "mkdir ${dir} failed"
  touch "${dir}/markdown.md" "${dir}/python.py" "${dir}/shell.sh" "${dir}/text.txt" || die_run "touch templates failed"
  if [[ "$user" != root ]]; then
    chown "${user}:" "$dir" "${dir}/markdown.md" "${dir}/python.py" "${dir}/shell.sh" "${dir}/text.txt" || die_run "chown templates to ${user} failed"
  fi
  record_state templates
}

write_interfaces_loopback() {
  if ((DRY_RUN)); then
    printf 'dry-run: write loopback-only /etc/network/interfaces\n'
    HANDOVER_MUTATED=1
    return 0
  fi
  tee /etc/network/interfaces >/dev/null <<'EOF'
# The loopback network interface
auto lo
iface lo inet loopback
EOF
}

configure_handover() {
  ((HANDOVER)) || return 0
  local def en
  log "Fence: handover (enable NetworkManager, no --now / no restart)"
  if ((DRY_RUN)); then
    printf 'dry-run: systemctl set-default graphical.target\n'
    printf 'dry-run: cp /etc/network/interfaces /etc/network/interfaces.bak  # if bak missing\n'
    printf 'dry-run: write loopback-only /etc/network/interfaces\n'
    printf 'dry-run: systemctl enable NetworkManager\n'
    HANDOVER_MUTATED=1
    record_state handover
    return 0
  fi
  def=$(systemctl get-default)
  if [[ "$def" != graphical.target ]]; then
    systemctl set-default graphical.target || die_run "systemctl set-default graphical.target failed"
    HANDOVER_MUTATED=1
  else
    log "graphical.target already default"
  fi
  if [[ -f /etc/network/interfaces && ! -e /etc/network/interfaces.bak ]]; then
    cp /etc/network/interfaces /etc/network/interfaces.bak
    HANDOVER_MUTATED=1
  fi
  if interfaces_is_loopback_only; then
    log "/etc/network/interfaces already loopback-only"
  else
    write_interfaces_loopback
    HANDOVER_MUTATED=1
  fi
  en=$(systemctl is-enabled NetworkManager 2>/dev/null || true)
  if [[ "$en" != enabled ]]; then
    systemctl enable NetworkManager || die_run "systemctl enable NetworkManager failed"
    HANDOVER_MUTATED=1
  else
    log "NetworkManager already enabled"
  fi
  record_state handover
}

verify_handover() {
  ((HANDOVER)) || return 0
  local def en
  if ((DRY_RUN)); then
    printf 'dry-run: systemctl get-default  # expect graphical.target\n'
    printf 'dry-run: systemctl is-enabled NetworkManager  # expect enabled\n'
    printf 'dry-run: check /etc/network/interfaces is loopback-only\n'
    return 0
  fi
  def=$(systemctl get-default)
  [[ "$def" == graphical.target ]] || die_run "get-default is ${def}, expected graphical.target"
  en=$(systemctl is-enabled NetworkManager 2>/dev/null || true)
  [[ "$en" == enabled ]] || die_run "NetworkManager is-enabled=${en}, expected enabled"
  interfaces_is_loopback_only || die_run "/etc/network/interfaces is not loopback-only"
  log "Handover verify: graphical.target, NetworkManager enabled, interfaces loopback-only"
}

print_phase4() {
  cat <<'EOF'

Phase 4 is not automated (GDM login, GNOME Settings, optional Astro).
After reboot:
  1. Log in at GDM.
  2. Configure wifi or ethernet in GNOME Settings so NetworkManager owns the connection.
  3. Optional Astro only after NM has network, and only on the NodeSource 24 path:
       npm create astro@latest
     Do not npm install -g.
  4. Verify: bash /tmp/debian-os-setup.sh --after-login   (or scripts/setup/setup.sh --after-login)

Do not start GDM or restart NetworkManager on this TTY before reboot.
EOF
}

maybe_reboot() {
  if ((REBOOT == 0 || HANDOVER == 0)); then
    log "Not rebooting (--no-reboot or handover skipped). Reboot when ready: systemctl reboot"
    return 0
  fi
  if ((DRY_RUN)); then
    printf 'dry-run: systemctl reboot  # if handover changed this run\n'
    return 0
  fi
  if ((HANDOVER_MUTATED == 0)); then
    log "Handover already in place; not rebooting. Use systemctl reboot when ready."
    return 0
  fi
  log "Rebooting now (handover changed this run)..."
  systemctl reboot
}

# rustup's PATH rustc exits 1 when HOME has no toolchain; do not abort --after-login.
try_tool_version() {
  local exe=$1
  shift
  local user resolved out
  user=$(login_user)
  if [[ "$exe" == /* ]]; then
    resolved=$exe
  elif ((EUID == 0)) && [[ "$user" != root ]]; then
    resolved=$(sudo -u "$user" -H -- bash -c 'command -v "$1"' _ "$exe" 2>/dev/null) || return 1
  else
    resolved=$(command -v "$exe" 2>/dev/null) || return 1
  fi
  [[ -n "$resolved" ]] || return 1
  if ((EUID == 0)) && [[ "$user" != root ]]; then
    out=$(sudo -u "$user" -H -- "$resolved" "$@" 2>/dev/null) || return 1
  else
    [[ -x "$resolved" ]] || return 1
    out=$("$resolved" "$@" 2>/dev/null) || return 1
  fi
  [[ -n "$out" ]] || return 1
  printf '%s\n' "$out"
}

print_tool_version() {
  local bin=$1
  local extra_bin=${2:-}
  local -a vargs=(--version)
  if [[ "$bin" == rustc ]]; then
    vargs=(--version --verbose)
  fi
  if try_tool_version "$bin" "${vargs[@]}"; then
    return 0
  fi
  if [[ -n "$extra_bin" ]] && try_tool_version "$extra_bin" "${vargs[@]}"; then
    return 0
  fi
  log "${bin}: not installed"
}

nm_has_connection() {
  command -v nmcli >/dev/null 2>&1 || return 1
  local out
  out=$(nmcli -t -f TYPE,DEVICE con show --active 2>/dev/null || true)
  [[ -n "$out" ]] || return 1
  printf '%s\n' "$out" | grep -vq '^loopback:'
}

after_login_flow() {
  preflight
  local home
  home=$(login_home)
  log "After-login verify (no installs, no handover rewrite, no reboot)"
  print_tool_version python3
  print_tool_version node
  print_tool_version npm
  print_tool_version rustc "${home}/.cargo/bin/rustc"
  if command -v systemctl >/dev/null 2>&1; then
    log "get-default: $(systemctl get-default 2>/dev/null || printf 'unknown')"
    log "NetworkManager: $(systemctl is-enabled NetworkManager 2>/dev/null || printf 'unknown')"
  fi
  if ! nm_has_connection; then
    log "No NetworkManager connection. Configure wifi or ethernet in GNOME Settings so NetworkManager owns the connection."
  fi
}

maybe_sudo_reexec() {
  ((EUID == 0)) && return 0
  command -v sudo >/dev/null 2>&1 || die_usage "root or sudo is required"
  if [[ -n "$SCRIPT_PATH" && -f "$SCRIPT_PATH" ]]; then
    exec sudo bash "$SCRIPT_PATH" "$@"
  fi
  die_usage "piped invoke requires root. Use: curl -fsSL https://debian.reporet.com/setup.sh | sudo bash"
}

run_setup() {
  preflight
  if ((DEV_ALL)); then
    log "Mode: dev-all"
  elif ((DEV)); then
    log "Mode: dev"
  else
    log "Mode: standard"
  fi
  if need_package_phase; then
    log "Phase 1: packages (apt-get update once)"
    apt_update_once
    install_gnome
    install_c_toolchain
    install_clang
    install_cross
    install_python
    install_python_ds
    install_python_debug
    install_node
    install_rust
    install_packaging
    install_firmware
  else
    log "Phase 1: no package fences requested"
  fi
  log "Phase 2: local config"
  configure_grub
  configure_templates
  log "Phase 3: handover"
  configure_handover
  verify_handover
  print_phase4
  maybe_reboot
}

parse_args "$@"
apply_mode_defaults

if ((AFTER_LOGIN)); then
  if [[ ! -t 0 ]]; then
    err "error: stdin is not a TTY. Do not use curl | bash (sudo/apt can eat the script)."
    err "Save the file, then: bash setup.sh --after-login"
    exit 1
  fi
  after_login_flow
  exit 0
fi

if ((DRY_RUN == 0)) && [[ ! -t 0 ]] && ((EUID != 0)); then
  err "error: stdin is not a TTY and you are not root."
  err "Pipe as root: curl -fsSL https://debian.reporet.com/setup.sh | sudo bash"
  err "Or save a file, then: sudo bash /tmp/debian-os-setup.sh"
  exit 1
fi

if ((DRY_RUN == 0)); then
  maybe_sudo_reexec "$@"
fi

run_setup
exit 0
}
