#!/bin/bash
# =============================================================================
# machine-setup — shared foundation
# =============================================================================
#
# Sourced by machine-setup.sh before anything runs. DEFINITIONS ONLY: this file
# declares state and functions and must never install, write or restart
# anything. Sourcing it has to be safe at any point, including from a step that
# is only being read for its variables.
#
# The one thing it expects from its caller, because they are facts about the
# entry point rather than about this library:
#
#   SCRIPT_DIR     directory of the script being run
#   PROGRESS_FILE  where completed step names are recorded
#
# Everything else below is owned here.

# Guard against being sourced twice — steps will eventually source this
# directly so they can be run on their own, and re-running it would reset
# SUMMARY and lose everything recorded so far.
[[ -n "${MACHINE_SETUP_BASE_LOADED:-}" ]] && return 0
MACHINE_SETUP_BASE_LOADED=1

# -----------------------------------------------------------------------------
# Shared state
# -----------------------------------------------------------------------------

SUMMARY=()      # what was done, printed at the end
ERRORS=()       # non-fatal failures, printed at the end
CURRENT_STEP=""
SKIP_STEP=false

# What machine this is. Filled in by detect_os() before any step runs; every step
# after that branches on these rather than assuming apt on x86_64.
OS=""          # os-release ID: ubuntu | debian | arch | fedora | macos | …
OS_NAME=""     # pretty name, for the banner
OS_VERSION=""  # version id; empty on rolling releases
PM=""          # apt | pacman | dnf | brew
ARCH=""        # amd64 | arm64, normalised — upstream tarballs disagree on spelling
IS_WSL=false

# -----------------------------------------------------------------------------
# Output
# -----------------------------------------------------------------------------

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

info() { echo -e "${CYAN}::${NC} $*"; }
ok() { echo -e "   ${GREEN}OK${NC}: $*"; }
warn() { echo -e "   ${YELLOW}WARN${NC}: $*"; }
fail() {
  echo -e "   ${RED}FAIL${NC}: $*"
  exit 1
}

# -----------------------------------------------------------------------------
# Steps and resume
# -----------------------------------------------------------------------------
#
# A step announces itself, and is skipped when its name is already in the
# progress file. step_ok records it. The pattern at each call site is:
#
#   step "Name"
#   if ! skip; then
#     …
#     step_ok
#   fi

step() {
  CURRENT_STEP="$1"
  if grep -qxF "$1" "$PROGRESS_FILE" 2>/dev/null; then
    echo -e "   ${GREEN}SKIP${NC}: $1 (already done)"
    SKIP_STEP=true
    return
  fi
  SKIP_STEP=false
  echo ""
  echo -e "${BOLD}── $1 ──${NC}"
}

skip() { [[ "$SKIP_STEP" == true ]]; }

step_ok() {
  echo "$CURRENT_STEP" >>"$PROGRESS_FILE"
}

# Try a command, log error but don't exit
try() {
  local label="$1"
  shift
  if "$@" 2>&1; then
    ok "$label"
  else
    warn "$label — failed (non-critical, continuing)"
    ERRORS+=("$label")
  fi
}

# -----------------------------------------------------------------------------
# Input
# -----------------------------------------------------------------------------

prompt_value() {
  local varname="$1" message="$2" default="$3"
  # If env var already set, use it silently
  if [[ -n "${!varname:-}" ]]; then
    return
  fi
  local input
  if [[ -n "$default" ]]; then
    read -rp "$message [$default]: " input
    eval "$varname=\"\${input:-$default}\""
  else
    read -rp "$message: " input
    eval "$varname=\"\$input\""
  fi
}

# Run a block as the created user (login shell, inherits HOME)
as_user() {
  sudo -u "$USERNAME" -i bash -c "$1"
}

# -----------------------------------------------------------------------------
# Operating system detection
# -----------------------------------------------------------------------------
#
# Read one key out of /etc/os-release without leaking the rest of it into this
# script. That file defines NAME, VERSION and ID — all generic enough to collide
# with something here — so it is sourced in a subshell and only the one value
# asked for comes back.
os_release() {
  [[ -r /etc/os-release ]] || return 1
  # shellcheck disable=SC1091
  (
    . /etc/os-release 2>/dev/null
    printf '%s' "${!1:-}"
  )
}

# Identify the machine, or refuse to guess.
#
# /etc/os-release rather than probing for a binary: a box can have more than one
# package manager on PATH (a Homebrew install on Linux, a leftover apt on a
# converted box), and only os-release can say which distribution the machine
# actually IS, or give a version worth reporting.
#
# ID_LIKE is the fallback so derivatives resolve without being listed by name —
# Pop!_OS, Mint and EndeavourOS all answer correctly without appearing below.
detect_os() {
  local kernel like
  kernel="$(uname -s)"

  case "$kernel" in
    Darwin)
      OS="macos"
      OS_VERSION="$(sw_vers -productVersion 2>/dev/null || true)"
      OS_NAME="macOS ${OS_VERSION}"
      PM="brew"
      ;;
    Linux)
      OS="$(os_release ID || true)"
      OS_NAME="$(os_release PRETTY_NAME || true)"
      OS_VERSION="$(os_release VERSION_ID || true)"
      like="$(os_release ID_LIKE || true)"

      case "$OS" in
        ubuntu | debian | linuxmint | pop | raspbian | elementary) PM="apt" ;;
        arch | manjaro | endeavouros | cachyos | garuda) PM="pacman" ;;
        fedora | rhel | centos | rocky | almalinux) PM="dnf" ;;
        *)
          case " $like " in
            *" debian "* | *" ubuntu "*) PM="apt" ;;
            *" arch "*) PM="pacman" ;;
            *" fedora "* | *" rhel "*) PM="dnf" ;;
          esac
          ;;
      esac

      # WSL reports itself as Linux, but has no real systemd session: masking
      # sleep targets, restarting logind and anything touching the boot path
      # either fail or silently do nothing. Worth knowing before those steps run.
      if grep -qi microsoft /proc/version 2>/dev/null; then IS_WSL=true; fi
      ;;
    MINGW* | MSYS* | CYGWIN*)
      fail "Windows is not supported. Run this inside WSL2 with an Ubuntu image instead."
      ;;
    *)
      fail "Unrecognised kernel '$kernel' — cannot tell what this machine is."
      ;;
  esac

  # Normalised once here because upstream projects spell it differently:
  # Neovim ships aarch64, Go and Docker ship arm64, and lazygit ships x86_64.
  case "$(uname -m)" in
    x86_64 | amd64) ARCH="amd64" ;;
    aarch64 | arm64) ARCH="arm64" ;;
    *) fail "Unsupported CPU architecture '$(uname -m)' — this script installs amd64/arm64 binaries only." ;;
  esac

  [[ -n "$OS" ]] || fail "Could not identify this distribution (no readable /etc/os-release)."
  [[ -n "$OS_NAME" ]] || OS_NAME="$OS${OS_VERSION:+ $OS_VERSION}"
}
