#!/bin/bash
set -e

# =============================================================================
# Ubuntu Server Setup Script
# Single-pass, non-interactive (install-everything) provisioning for a fresh
# Ubuntu server. Run as root: sudo ./setup-ubuntu.sh
# =============================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROGRESS_FILE="$SCRIPT_DIR/.setup-progress"
SUMMARY=()
ERRORS=()
CURRENT_STEP=""
SKIP_STEP=false

# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------

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; }

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"
}

# Trap errors with context
trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║  SETUP FAILED${NC}"; echo -e "${RED}║  Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║  Line: $LINENO${NC}"; echo -e "${RED}║  Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR

# 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
}

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"
}

# =============================================================================
# 1. Pre-flight
# =============================================================================

echo ""
echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║          Ubuntu Server Setup                     ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}"

if [[ -f "$PROGRESS_FILE" ]]; then
  DONE_COUNT=$(wc -l < "$PROGRESS_FILE")
  echo -e "${YELLOW}  Resuming — $DONE_COUNT step(s) already completed${NC}"
  echo -e "${YELLOW}  Progress file: $PROGRESS_FILE${NC}"
  echo -e "${YELLOW}  To start fresh: rm $PROGRESS_FILE${NC}"
fi

if [[ "$EUID" -ne 0 ]]; then
  fail "Please run as root: sudo ./setup-ubuntu.sh"
fi

prompt_value USERNAME "New admin username (or existing)" ""
if [[ -z "$USERNAME" ]]; then
  fail "Username cannot be empty"
fi

USER_HOME="/home/$USERNAME"

# =============================================================================
# 2. System Update & Essentials
# =============================================================================

step "System Update & Essentials"
if ! skip; then
  info "Updating system packages..."
  apt-get update -y && apt-get upgrade -y

  info "Installing essential packages..."
  apt-get install -y \
    curl wget git zip unzip build-essential btop net-tools \
    software-properties-common jq htop tree ripgrep fd-find tmux \
    apt-transport-https ca-certificates gnupg lsb-release

  ok "System updated and essentials installed"

  # lazydocker
  info "Installing lazydocker..."
  curl -fsSL https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | DIR=/usr/local/bin bash
  ok "lazydocker installed"

  # lazygit
  info "Installing lazygit..."
  LAZYGIT_VERSION=$(curl -fsSL "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | jq -r '.tag_name' | tr -d 'v')
  curl -fsSLo /tmp/lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz"
  tar -C /usr/local/bin -xzf /tmp/lazygit.tar.gz lazygit
  rm -f /tmp/lazygit.tar.gz
  ok "lazygit installed"

  # starship
  info "Installing starship..."
  curl -fsSL https://starship.rs/install.sh | sh -s -- -y
  ok "starship installed"

  # fastfetch
  info "Installing fastfetch..."
  add-apt-repository -y ppa:zhangsongcui3371/fastfetch > /dev/null 2>&1 || true
  apt-get update -y > /dev/null 2>&1
  apt-get install -y fastfetch
  ok "fastfetch installed"

  SUMMARY+=("System packages updated and essentials installed")
  step_ok
fi

# =============================================================================
# 3. Locale
# =============================================================================

step "Locale"
if ! skip; then
  info "Setting locale to en_US.UTF-8..."
  locale-gen en_US.UTF-8 || true
  update-locale LANG=en_US.UTF-8

  ok "Locale set"
  SUMMARY+=("Locale: en_US.UTF-8")
  step_ok
fi

# =============================================================================
# 4. Timezone
# =============================================================================

step "Timezone"
if ! skip; then
  info "Select timezone:"
  TZ_OPTIONS=("UTC" "Europe/Lisbon" "Europe/Stockholm" "Europe/London" "US/Eastern" "US/Pacific" "Asia/Tokyo")
  for i in "${!TZ_OPTIONS[@]}"; do
    echo "  [$((i+1))] ${TZ_OPTIONS[$i]}"
  done

  prompt_value TZ_CHOICE "Pick a number or type a timezone" "1"

  # If the input is a number, map to the array; otherwise use as-is
  if [[ "$TZ_CHOICE" =~ ^[0-9]+$ ]] && (( TZ_CHOICE >= 1 && TZ_CHOICE <= ${#TZ_OPTIONS[@]} )); then
    TIMEZONE="${TZ_OPTIONS[$((TZ_CHOICE-1))]}"
  else
    TIMEZONE="$TZ_CHOICE"
  fi

  timedatectl set-timezone "$TIMEZONE"
  ok "Timezone: $TIMEZONE"
  SUMMARY+=("Timezone: $TIMEZONE")
  step_ok
fi

# =============================================================================
# 5. Swap
# =============================================================================

step "Swap"
if ! skip; then
  if ! swapon --show | grep -q '/'; then
    info "Creating swap file..."
    RAM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    if (( RAM_KB <= 2097152 )); then
      SWAP_SIZE="2G"
    elif (( RAM_KB <= 8388608 )); then
      SWAP_SIZE="4G"
    else
      SWAP_SIZE="8G"
    fi

    fallocate -l "$SWAP_SIZE" /swapfile
    chmod 600 /swapfile
    mkswap /swapfile
    swapon /swapfile

    if ! grep -q '/swapfile' /etc/fstab; then
      echo '/swapfile none swap sw 0 0' >> /etc/fstab
    fi

    ok "Swap: $SWAP_SIZE"
    SUMMARY+=("Swap: $SWAP_SIZE created")
  else
    ok "Swap already exists, skipping"
    SUMMARY+=("Swap: already present")
  fi

  # Set swappiness
  sysctl vm.swappiness=10
  if ! grep -q 'vm.swappiness' /etc/sysctl.conf; then
    echo 'vm.swappiness=10' >> /etc/sysctl.conf
  else
    sed -i 's/^vm.swappiness=.*/vm.swappiness=10/' /etc/sysctl.conf
  fi
  step_ok
fi

# =============================================================================
# 6. Auto-Suspend (disable for servers)
# =============================================================================

step "Auto-Suspend (disable for servers)"
if ! skip; then
  info "Disabling auto-suspend and idle actions..."

  # Mask sleep/suspend targets
  systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target 2>/dev/null || true

  # Configure logind — patch individual keys, don't overwrite the file
  set_logind_key() {
    local key="$1" val="$2" file="/etc/systemd/logind.conf"
    if grep -qE "^${key}=" "$file" 2>/dev/null; then
      sed -i "s|^${key}=.*|${key}=${val}|" "$file"
    elif grep -qE "^#${key}=" "$file" 2>/dev/null; then
      sed -i "s|^#${key}=.*|${key}=${val}|" "$file"
    else
      echo "${key}=${val}" >> "$file"
    fi
  }
  set_logind_key HandleLidSwitch ignore
  set_logind_key HandleLidSwitchExternalPower ignore
  set_logind_key HandlePowerKey ignore
  set_logind_key IdleAction none
  set_logind_key RuntimeDirectorySize 10%

  systemctl restart systemd-logind

  ok "Auto-suspend disabled"
  SUMMARY+=("Auto-suspend: disabled")
  step_ok
fi

# =============================================================================
# 6.5 Boot Hang Fix (NetworkManager desktops only) — OPTIONAL, default off
# =============================================================================
#
# On Ubuntu Desktop, NetworkManager owns the NIC and systemd-networkd runs
# nothing — so systemd-networkd-wait-online.service blocks boot for its full
# ~120s timeout. Masking it removes the hang.
#
# On Ubuntu Server / cloud VPS images, systemd-networkd IS the network manager
# (cloud-init + netplan). There the service is load-bearing and completes
# quickly, so there's nothing to fix. The guard below detects which stack is
# live and only acts on the NetworkManager case.
#
# NOTE: do NOT `systemctl disable --now systemd-networkd.{socket,service}` here.
# On a networkd-managed VPS that brings the box up with no network on the next
# boot — no SSH, provider rescue console only. Keep it as a manual, desktop-only
# step if you ever want it.

step "Boot Hang Fix"
if ! skip; then
  prompt_value FIX_BOOT_HANG "Mask systemd-networkd-wait-online on NetworkManager boxes? (y/n)" "n"

  if [[ "$FIX_BOOT_HANG" != "y" ]]; then
    info "Skipping boot hang fix"
    SUMMARY+=("Boot hang fix: skipped")
  else
    # Only mask the spurious wait-online where NetworkManager is active and
    # networkd is not. Anything else (networkd stack, or unclear) is left alone.
    if systemctl is-active --quiet NetworkManager.service \
       && ! systemctl is-active --quiet systemd-networkd.service; then
      systemctl mask --now systemd-networkd-wait-online.service
      ok "Masked systemd-networkd-wait-online (NetworkManager stack)"
      SUMMARY+=("Boot hang fix: wait-online masked (NetworkManager)")
    else
      warn "networkd stack (or unclear) — leaving wait-online alone"
      SUMMARY+=("Boot hang fix: not applied (networkd stack)")
    fi
  fi
  step_ok
fi

# =============================================================================
# 7. User Creation
# =============================================================================

step "User Creation"
if ! skip; then
  if id "$USERNAME" &>/dev/null; then
    info "User '$USERNAME' already exists, skipping creation"
    SUMMARY+=("User: $USERNAME (existing)")
  else
    info "Creating user '$USERNAME'..."
    adduser --gecos "" "$USERNAME"
    usermod -aG sudo "$USERNAME"
    ok "User '$USERNAME' created with sudo access"
    SUMMARY+=("User: $USERNAME created")
  fi

  prompt_value PASSWORDLESS_SUDO "Enable passwordless sudo for $USERNAME? (y/n)" "y"

  if [[ "$PASSWORDLESS_SUDO" == "y" ]]; then
    echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > "/etc/sudoers.d/99-$USERNAME-nopasswd"
    chmod 440 "/etc/sudoers.d/99-$USERNAME-nopasswd"
    visudo -cf "/etc/sudoers.d/99-$USERNAME-nopasswd" || rm -f "/etc/sudoers.d/99-$USERNAME-nopasswd"
    ok "Passwordless sudo enabled (remove /etc/sudoers.d/99-$USERNAME-nopasswd to disable)"
    SUMMARY+=("Sudo: passwordless")
  fi

  # Copy .tmux.conf if present next to this script
  if [[ -f "$SCRIPT_DIR/.tmux.conf" ]]; then
    cp "$SCRIPT_DIR/.tmux.conf" "$USER_HOME/.tmux.conf"
    chown "$USERNAME:$USERNAME" "$USER_HOME/.tmux.conf"
    ok "Copied .tmux.conf to $USER_HOME"
  fi
  step_ok
fi

# =============================================================================
# 8. SSH Keys
# =============================================================================

step "SSH Keys"
if ! skip; then
  info "SSH key setup for '$USERNAME':"
  echo "  [1] Extract from ssh-keys.zip (next to this script)"
  echo "  [2] Paste key content"
  echo "  [3] Generate new ed25519 key"

  prompt_value SSH_CHOICE "Pick an option" "1"

  SSH_DIR="$USER_HOME/.ssh"
  mkdir -p "$SSH_DIR"

  case "$SSH_CHOICE" in
    1)
      ZIP_FILE="$SCRIPT_DIR/ssh-keys.zip"
      if [[ ! -f "$ZIP_FILE" ]]; then
        warn "ssh-keys.zip not found at $ZIP_FILE"
        warn "Skipping SSH key setup"
      else
        read -rsp "Zip password: " ZIP_PASS
        echo
        if [[ -n "$ZIP_PASS" ]]; then
          unzip -o -P "$ZIP_PASS" "$ZIP_FILE" -d "$SSH_DIR"
        else
          unzip -o "$ZIP_FILE" -d "$SSH_DIR"
        fi
        ok "SSH keys extracted from zip"
        SUMMARY+=("SSH keys: extracted from zip")
      fi
      ;;
    2)
      echo "Paste your PUBLIC key (then press Enter, then Ctrl-D):"
      cat > "$SSH_DIR/id_ed25519.pub"
      echo "Paste your PRIVATE key (then press Enter, then Ctrl-D):"
      cat > "$SSH_DIR/id_ed25519"
      # Also add public key to authorized_keys
      cat "$SSH_DIR/id_ed25519.pub" >> "$SSH_DIR/authorized_keys"
      ok "SSH keys written from input"
      SUMMARY+=("SSH keys: pasted manually")
      ;;
    3)
      prompt_value SSH_EMAIL "Email/comment for key" ""
      sudo -u "$USERNAME" ssh-keygen -t ed25519 -C "$SSH_EMAIL" -f "$SSH_DIR/id_ed25519" -N ""
      cat "$SSH_DIR/id_ed25519.pub" >> "$SSH_DIR/authorized_keys"
      ok "SSH key generated"
      echo "   Public key:"
      cat "$SSH_DIR/id_ed25519.pub"
      SUMMARY+=("SSH keys: generated new ed25519")
      ;;
  esac

  chmod 700 "$SSH_DIR"
  find "$SSH_DIR" -type f -exec chmod 600 {} \;
  chown -R "$USERNAME:$USERNAME" "$SSH_DIR"
  step_ok
fi

# =============================================================================
# 9. SSH Hardening
# =============================================================================

step "SSH Hardening"
if ! skip; then
  info "Hardening SSH..."
  sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
  sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config
  sed -i 's/^#\?ChallengeResponseAuthentication .*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
  systemctl restart ssh

  ok "SSH hardened (password auth disabled, root login disabled)"
  SUMMARY+=("SSH: hardened")
  step_ok
fi

# =============================================================================
# 10. DNS
# =============================================================================

step "DNS"
if ! skip; then
  info "Configuring DNS (Cloudflare + Google)..."
  PRIMARY_DNS=("1.1.1.1" "8.8.8.8")
  FALLBACK_DNS=("1.0.0.1" "8.8.4.4")

  if systemctl is-active --quiet systemd-resolved; then
    cp /etc/systemd/resolved.conf "/etc/systemd/resolved.conf.bak.$(date +%s)" 2>/dev/null || true

    cat > /etc/systemd/resolved.conf <<EOF
[Resolve]
DNS=${PRIMARY_DNS[*]}
FallbackDNS=${FALLBACK_DNS[*]}
EOF

    # Ensure resolv.conf points to the stub
    if [[ -L /etc/resolv.conf ]]; then
      TARGET=$(readlink -f /etc/resolv.conf || true)
      if [[ "$TARGET" != "/run/systemd/resolve/stub-resolv.conf" && "$TARGET" != "/run/systemd/resolve/resolv.conf" ]]; then
        ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
      fi
    else
      cp /etc/resolv.conf "/etc/resolv.conf.bak.$(date +%s)"
      ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
    fi

    systemctl restart systemd-resolved
    ok "DNS configured (systemd-resolved)"
  else
    if [[ -e /etc/resolv.conf ]] || [[ -L /etc/resolv.conf ]]; then
      cp -a /etc/resolv.conf "/etc/resolv.conf.bak.$(date +%s)" 2>/dev/null || true
    fi
    if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i-'; then
      chattr -i /etc/resolv.conf
    fi

    {
      echo "# Generated by setup-ubuntu.sh on $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
      for ns in "${PRIMARY_DNS[@]}"; do
        echo "nameserver $ns"
      done
    } > /etc/resolv.conf

    ok "DNS configured (static resolv.conf)"
  fi

  SUMMARY+=("DNS: Cloudflare + Google")

  # Test DNS
  if command -v dig &>/dev/null; then
    dig +short google.com > /dev/null 2>&1 && ok "DNS resolution works" || warn "DNS test failed"
  else
    getent hosts google.com > /dev/null 2>&1 && ok "DNS resolution works" || warn "DNS test failed"
  fi
  step_ok
fi

# =============================================================================
# 11. Static IP
# =============================================================================

step "Static IP"
if ! skip; then
  # Detect primary interface and current IP
  IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
  CURRENT_IP=$(ip -4 addr show "$IFACE" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}/\d+' | head -1)
  GATEWAY=$(ip route | grep default | awk '{print $3}' | head -1)

  if [[ -z "$IFACE" || -z "$CURRENT_IP" || -z "$GATEWAY" ]]; then
    warn "Could not detect network interface, IP, or gateway — skipping static IP"
    SUMMARY+=("Static IP: skipped (detection failed)")
  else
    # Check if a static Netplan config already exists for this interface
    EXISTING_STATIC=$(grep -rl "dhcp4: false\|dhcp4: no" /etc/netplan/ 2>/dev/null | head -1)
    if [[ -n "$EXISTING_STATIC" ]]; then
      warn "Static IP already configured in $EXISTING_STATIC — skipping"
      SUMMARY+=("Static IP: already static (skipped)")
    else
      info "Detected network configuration:"
      echo "    Interface : $IFACE"
      echo "    IP        : $CURRENT_IP"
      echo "    Gateway   : $GATEWAY"
      echo ""
      echo -e "${RED}${BOLD}╔══════════════════════════════════════════════════╗${NC}"
      echo -e "${RED}${BOLD}║  STOP — DO NOT DO THIS ON A VPS / CLOUD SERVER   ║${NC}"
      echo -e "${RED}${BOLD}╚══════════════════════════════════════════════════╝${NC}"
      echo -e "${YELLOW}  This freezes the CURRENT DHCP lease into netplan and applies"
      echo -e "  it immediately. On a VPS or cloud instance that can strand the box:${NC}"
      echo ""
      echo -e "    ${RED}!${NC} Providers hand out addresses by DHCP and may reassign them —"
      echo -e "      the hardcoded IP then belongs to someone else."
      echo -e "    ${RED}!${NC} cloud-init regenerates netplan config on boot; the two configs"
      echo -e "      can conflict and leave the interface with no usable address."
      echo -e "    ${RED}!${NC} The gateway/netmask on cloud networks is often NOT what a"
      echo -e "      simple route lookup suggests (point-to-point, /32 routing)."
      echo -e "    ${RED}!${NC} If any value is wrong, ${BOLD}netplan apply kills the SSH session"
      echo -e "      you are on right now${NC} and you are locked out until the provider's"
      echo -e "      rescue console."
      echo ""
      echo -e "${YELLOW}  Only answer 'y' on a machine you physically control (LAN box,"
      echo -e "  homelab, bare metal) where the address is yours to pin — and even"
      echo -e "  then, a DHCP reservation on the router is the safer way to do it.${NC}"
      echo ""
      prompt_value CONFIRM_STATIC "Set this as static IP? (y/n)" "n"

      if [[ "$CONFIRM_STATIC" != "y" ]]; then
        info "Skipping static IP configuration"
        SUMMARY+=("Static IP: skipped (user declined)")
      else
        NETPLAN_FILE="/etc/netplan/99-static-ip.yaml"
        cat > "$NETPLAN_FILE" <<EOF
network:
  version: 2
  ethernets:
    $IFACE:
      dhcp4: false
      addresses:
        - $CURRENT_IP
      routes:
        - to: default
          via: $GATEWAY
      nameservers:
        addresses: [1.1.1.1, 8.8.8.8]
EOF
        chmod 600 "$NETPLAN_FILE"
        netplan apply
        ok "Static IP set: $CURRENT_IP on $IFACE (gateway: $GATEWAY)"
        SUMMARY+=("Static IP: $CURRENT_IP on $IFACE")
      fi
    fi
  fi
  step_ok
fi

# =============================================================================
# 12. Fail2ban
# =============================================================================

step "Fail2ban"
if ! skip; then
  info "Installing fail2ban..."
  apt-get install -y fail2ban
  systemctl enable fail2ban
  systemctl start fail2ban

  ok "Fail2ban active"
  SUMMARY+=("Fail2ban: installed and active")
  step_ok
fi

# =============================================================================
# 13. Unattended Upgrades
# =============================================================================

step "Unattended Upgrades"
if ! skip; then
  info "Configuring unattended upgrades..."
  apt-get install -y unattended-upgrades

  cat > /etc/apt/apt.conf.d/20auto-upgrades <<EOF
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF

  ok "Unattended upgrades enabled"
  SUMMARY+=("Unattended upgrades: enabled")
  step_ok
fi

# =============================================================================
# 14. Git Config
# =============================================================================

step "Git Config"
if ! skip; then
  info "Git configuration for '$USERNAME'..."
  prompt_value GIT_NAME "Git user.name" ""
  prompt_value GIT_EMAIL "Git user.email" ""

  as_user "git config --global user.name $(printf '%q' "$GIT_NAME")"
  as_user "git config --global user.email $(printf '%q' "$GIT_EMAIL")"
  as_user "git config --global init.defaultBranch main"
  as_user "git config --global core.editor nvim"

  ok "Git configured"
  SUMMARY+=("Git: $GIT_NAME <$GIT_EMAIL>")
  step_ok
fi

# =============================================================================
# 15. Docker
# =============================================================================

step "Docker"
if ! skip; then
  info "Installing Docker CE..."
  install -m 0755 -d /etc/apt/keyrings
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
  chmod a+r /etc/apt/keyrings/docker.gpg

  echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
    https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \
    > /etc/apt/sources.list.d/docker.list

  apt-get update -y
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

  usermod -aG docker "$USERNAME"

  # Create shared docker network
  DOCKER_NETWORK="${SETUP_DOCKER_NETWORK:-services}"
  docker network create "$DOCKER_NETWORK" 2>/dev/null || true

  ok "Docker installed, '$USERNAME' added to docker group"
  SUMMARY+=("Docker: installed")
  step_ok
fi
# =============================================================================
# 16. Oh My Zsh + Prompt (as user)
# =============================================================================

step "Oh My Zsh + Prompt"
if ! skip; then
  info "Installing Zsh and Oh My Zsh..."

  apt-get install -y zsh

  as_user 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended'

  ZSHRC="$USER_HOME/.zshrc"

  cat >> "$ZSHRC" <<'EOF'

# Starship prompt
eval "$(starship init zsh)"
EOF

  chsh -s /bin/zsh "$USERNAME"

  # Starship config — disable runtime/language version modules
  mkdir -p "$USER_HOME/.config"
  cat > "$USER_HOME/.config/starship.toml" <<'STARSHIP_EOF'
add_newline = true
command_timeout = 200

[bun]
disabled = true

[nodejs]
disabled = true

[python]
disabled = true

[rust]
disabled = true

[golang]
disabled = true

[package]
disabled = true
STARSHIP_EOF
  chown -R "$USERNAME:$USERNAME" "$USER_HOME/.config"

  ok "Zsh + Oh My Zsh installed, starship prompt set"
  SUMMARY+=("Shell: zsh with Oh My Zsh + Starship")
  step_ok
fi

# =============================================================================
# 17. Tailscale (as user)
# =============================================================================

step "Tailscale"
if ! skip; then
  prompt_value INSTALL_TAILSCALE "Install Tailscale? (y/n)" "y"

  if [[ "$INSTALL_TAILSCALE" != "y" || "${SETUP_SKIP_TAILSCALE:-}" == "true" ]]; then
    info "Skipping Tailscale"
    SUMMARY+=("Tailscale: skipped")
  else
    info "Installing Tailscale..."
    curl -fsSL https://tailscale.com/install.sh | sh

    # Enable IP forwarding
    sed -i '/^net.ipv4.ip_forward/d' /etc/sysctl.conf
    sed -i '/^net.ipv6.conf.all.forwarding/d' /etc/sysctl.conf
    echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
    echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
    sysctl -p

    prompt_value TS_LOGIN_SERVER "Tailscale login server" "https://headscale.pastilhas.eu"
    prompt_value TS_AUTHKEY "Tailscale auth key" ""

    prompt_value TS_EXIT_NODE "Act as exit node? (y/n)" "n"
    TS_ADVERTISE=""

    if [[ "$TS_EXIT_NODE" == "y" ]]; then
      TS_ADVERTISE="--advertise-exit-node"

      cat > /etc/sysctl.d/99-tailscale-exit.conf <<EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
      sysctl --system > /dev/null

      # NIC offload optimizations via networkd-dispatcher
      if ! command -v networkd-dispatcher &>/dev/null; then
        apt-get install -y networkd-dispatcher
      fi
      mkdir -p /etc/networkd-dispatcher/routable.d

      cat > /etc/networkd-dispatcher/routable.d/50-tailscale-exit <<'EXITSCRIPT'
#!/usr/bin/env bash
set -Eeuo pipefail

IF="${IFACE:-}"
if [[ -z "${IF}" ]]; then
  IF="$(ip -o route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')"
fi

[[ -n "${IF}" ]] || exit 0
command -v ethtool >/dev/null 2>&1 || exit 0

ethtool -k "${IF}" 2>/dev/null | grep -q "^generic-receive-offload: " && ethtool -K "${IF}" gro on || true
ethtool -k "${IF}" 2>/dev/null | grep -q "^rx-udp-gro-forwarding: " && ethtool -K "${IF}" rx-udp-gro-forwarding on || true
ethtool -k "${IF}" 2>/dev/null | grep -q "^large-receive-offload: " && ethtool -K "${IF}" lro off || true

exit 0
EXITSCRIPT

      chmod 755 /etc/networkd-dispatcher/routable.d/50-tailscale-exit
      systemctl enable --now networkd-dispatcher > /dev/null 2>&1 || true

      # Run once now
      CURRENT_IF="$(ip -o route get 8.8.8.8 | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')"
      IFACE="$CURRENT_IF" bash /etc/networkd-dispatcher/routable.d/50-tailscale-exit || true
      ok "Exit node NIC optimizations applied on: ${CURRENT_IF:-unknown}"
    fi

    tailscale up --login-server "$TS_LOGIN_SERVER" --authkey "$TS_AUTHKEY" ${TS_ADVERTISE:+"$TS_ADVERTISE"}

    TS_IP=$(tailscale ip -4 2>/dev/null || echo "unknown")
    ok "Tailscale connected (IP: $TS_IP)"
    SUMMARY+=("Tailscale: connected ($TS_IP)")
  fi
  step_ok
fi

# =============================================================================
# 18. Neovim + LazyVim (as user)
# =============================================================================

step "Neovim + LazyVim"
if ! skip; then
  ZSHRC="$USER_HOME/.zshrc"

  info "Installing Neovim..."
  NVIM_ARCH=$(uname -m)
  case $NVIM_ARCH in
    aarch64) NVIM_ARCH=aarch64 ;;
    *)       NVIM_ARCH=x86_64 ;;
  esac
  curl -LO "https://github.com/neovim/neovim/releases/latest/download/nvim-linux-${NVIM_ARCH}.tar.gz"
  rm -rf "/opt/nvim-linux-${NVIM_ARCH}"
  tar -C /opt -xzf "nvim-linux-${NVIM_ARCH}.tar.gz"
  rm -f "nvim-linux-${NVIM_ARCH}.tar.gz"
  ln -sf "/opt/nvim-linux-${NVIM_ARCH}/bin/nvim" /usr/local/bin/nvim

  # Add to PATH in .zshrc
  cat >> "$ZSHRC" <<EOF

# Neovim
export PATH="\$PATH:/opt/nvim-linux-${NVIM_ARCH}/bin"
EOF

  info "Choose Neovim configuration:"
  echo "  [1] Default LazyVim starter"
  echo "  [2] Custom repo"

  prompt_value NVIM_CHOICE "Pick an option" "1"

  NVIM_CONFIG="$USER_HOME/.config/nvim"

  if [[ "$NVIM_CHOICE" == "2" ]]; then
    prompt_value NVIM_REPO "Git repo for nvim config" "git@gogs:andrepadez/nvim-config.git"
  else
    NVIM_REPO="https://github.com/LazyVim/starter"
  fi

  if [[ -d "$NVIM_CONFIG" ]]; then
    warn "$NVIM_CONFIG already exists, backing up to ${NVIM_CONFIG}.bak"
    mv "$NVIM_CONFIG" "${NVIM_CONFIG}.bak"
  fi

  sudo -u "$USERNAME" -H git clone "$NVIM_REPO" "$NVIM_CONFIG"
  sudo -u "$USERNAME" rm -rf "$NVIM_CONFIG/.git" 2>/dev/null || true

  chown -R "$USERNAME:$USERNAME" "$USER_HOME/.config"
  chown "$USERNAME:$USERNAME" "$ZSHRC"

  ok "Neovim + config installed"
  SUMMARY+=("Neovim: installed with LazyVim")
  step_ok
fi

# =============================================================================
# 19. JS/TS Runtimes (system-wide for servers, optional nvm for dev)
# =============================================================================

step "JS/TS Runtimes"
if ! skip; then
  ZSHRC="$USER_HOME/.zshrc"

  if [[ "${SETUP_SKIP_RUNTIMES:-}" == "true" ]]; then
    info "Skipping JS/TS runtimes (SETUP_SKIP_RUNTIMES=true)"
    SUMMARY+=("Runtimes: skipped")
  else
    info "Installing JS/TS runtimes..."

    # Node 22 — always system-wide via NodeSource
    info "Installing system-wide Node 22..."
    curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
    apt-get install -y nodejs
    npm config set prefix /usr/local
    ok "System Node 22 installed ($(node -v))"
    SUMMARY+=("Node: system-wide v22 via NodeSource")

    # Bun (user-local install)
    as_user 'curl -fsSL https://bun.sh/install | bash'
    cat >> "$ZSHRC" <<'EOF'

# Bun
export PATH="$HOME/.bun/bin:$PATH"
EOF
    ok "Bun installed"

    # pm2 (system-wide, via system npm)
    npm install -g pm2
    ok "pm2 installed (system-wide)"

    # Deno (user-local install)
    as_user 'curl -fsSL https://deno.land/install.sh | sh'
    cat >> "$ZSHRC" <<'EOF'

# Deno
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
EOF
    ok "Deno installed"

    chown "$USERNAME:$USERNAME" "$ZSHRC"
  fi
  step_ok
fi

# =============================================================================
# 19.5 Dev Tools (system-wide for servers, local for dev users)
# =============================================================================

step "Dev Tools"
if ! skip; then
  if [[ "${SETUP_SKIP_DEVTOOLS:-}" == "true" ]]; then
    info "Skipping dev tools (SETUP_SKIP_DEVTOOLS=true)"
    SUMMARY+=("Dev tools: skipped")
  else
    info "Select dev tools to install:"
    echo "  [1] Claude Code"
    echo "  [2] Opencode"
    echo "  [3] PI"

    prompt_value DEV_TOOLS "Enter numbers separated by spaces, or 'all'" "all"

    if [[ "$DEV_TOOLS" == "all" ]]; then
      DEV_TOOLS="1 2 3"
    fi

    DEV_INSTALLED=()

    for tool in $DEV_TOOLS; do
      case "$tool" in
        1)
          info "Installing Claude Code..."
          npm install -g @anthropic-ai/claude-code
          ok "Claude Code installed (system-wide)"
          DEV_INSTALLED+=("Claude Code")
          ;;
        2)
          info "Installing Opencode..."
          as_user 'curl -fsSL https://opencode.ai/install | bash'
          ok "Opencode installed"
          DEV_INSTALLED+=("Opencode")
          ;;
        3)
          info "Installing PI..."
          npm install -g @mariozechner/pi-coding-agent
          ok "PI installed (system-wide)"
          DEV_INSTALLED+=("PI")
          ;;
      esac
    done

    if [[ ${#DEV_INSTALLED[@]} -gt 0 ]]; then
      ok "Dev tools: ${DEV_INSTALLED[*]}"
      SUMMARY+=("Dev tools: ${DEV_INSTALLED[*]}")
    else
      SUMMARY+=("Dev tools: none selected")
    fi
  fi
  step_ok
fi

# =============================================================================
# 20. UFW (last, after everything)
# =============================================================================

step "UFW Firewall"
if ! skip; then
  info "Configuring UFW firewall..."

  ufw default deny incoming
  ufw default allow outgoing
  ufw allow OpenSSH

  # Allow all traffic on Tailscale interface
  ufw allow in on tailscale0

  # Docker + UFW fix
  UFW_RULES="/etc/ufw/after.rules"
  DOCKER_RULES="$SCRIPT_DIR/ufw-docker-rules.conf"

  if [[ -f "$DOCKER_RULES" ]]; then
    if ! grep -q 'DOCKER-USER' "$UFW_RULES" 2>/dev/null; then
      info "Appending Docker UFW rules..."
      echo "" >> "$UFW_RULES"
      cat "$DOCKER_RULES" >> "$UFW_RULES"
      ok "Docker UFW rules added"
    else
      ok "Docker UFW rules already present"
    fi
  else
    warn "ufw-docker-rules.conf not found, skipping Docker UFW fix"
  fi

  ufw --force enable
  ok "UFW enabled"
  SUMMARY+=("UFW: enabled (SSH, Tailscale, Docker rules)")
  step_ok
fi

# =============================================================================
# 21. Final .zshrc additions
# =============================================================================

step "Final .zshrc additions"
if ! skip; then
  ZSHRC="$USER_HOME/.zshrc"
  cat >> "$ZSHRC" <<'EOF'

# Neovim aliases
alias n='nvim'
alias vim='nvim'

# Default editor
export EDITOR='nvim'
export VISUAL='nvim'
export SUDO_EDITOR='nvim'

# Quick reload
alias sz="source ~/.zshrc"
EOF
  chown "$USERNAME:$USERNAME" "$ZSHRC"
  step_ok
fi

# =============================================================================
# 22. Emergency Disk Ballast
# =============================================================================
#
# Pre-allocates a junk file sized at 10% of currently-available disk. It holds
# no data — its only job is to be deleted when the filesystem is about to fill,
# buying enough headroom to SSH in and clean up properly instead of hitting a
# wedged box (Docker/journald/postgres all misbehave badly at 100% full).
#
# A root cron checks free space every 10 minutes and removes the ballast if it
# drops below 10%. This is a ONE-SHOT valve: once spent, recreate it with the
# same fallocate command below.

step "Emergency Disk Ballast"
if ! skip; then
  DOCKERS_DIR="$USER_HOME/dockers"
  BALLAST_FILE="$DOCKERS_DIR/emergency_empty_file.log"
  LOCAL_DIR="$USER_HOME/.local"
  LOCAL_BIN="$LOCAL_DIR/bin"
  CHECK_SCRIPT="$LOCAL_BIN/emergency-disk-check"
  CRON_FILE="/etc/cron.d/emergency-disk-check"

  # Name $LOCAL_DIR explicitly. `install -d` applies -o/-g ONLY to the directories given as
  # operands — any parent it has to invent on the way gets the caller's ownership, which here
  # is root. This step is the first thing to touch ~/.local, so leaving it implicit created a
  # root:root ~/.local with a user-owned bin/ inside it, and locked the user out of their own
  # ~/.local: anything unpacking or writing directly there (a Go tarball, npm's --prefix,
  # GOPATH) fails with a bare "Permission denied" far from this line.
  install -d -o "$USERNAME" -g "$USERNAME" "$DOCKERS_DIR" "$LOCAL_DIR" "$LOCAL_BIN"

  # --- the ballast file ---
  if [[ -f "$BALLAST_FILE" ]]; then
    ok "Ballast already exists at $BALLAST_FILE ($(du -h "$BALLAST_FILE" | cut -f1))"
    SUMMARY+=("Disk ballast: already present")
  else
    info "Allocating ballast file (10% of available disk)..."
    AVAIL_KB=$(df -Pk "$DOCKERS_DIR" | awk 'NR==2 {print $4}')
    BALLAST_KB=$(( AVAIL_KB / 10 ))

    # fallocate reserves real blocks (a sparse/truncate file would reserve
    # nothing and free nothing when deleted). dd is the fallback for
    # filesystems that don't support fallocate.
    if ! fallocate -l "${BALLAST_KB}K" "$BALLAST_FILE" 2>/dev/null; then
      warn "fallocate unsupported here — falling back to dd (slower)"
      dd if=/dev/zero of="$BALLAST_FILE" bs=1K count="$BALLAST_KB" status=none
    fi

    chown "$USERNAME:$USERNAME" "$BALLAST_FILE"
    chmod 644 "$BALLAST_FILE"
    ok "Ballast created: $BALLAST_FILE ($(du -h "$BALLAST_FILE" | cut -f1))"
    SUMMARY+=("Disk ballast: $(du -h "$BALLAST_FILE" | cut -f1) at $BALLAST_FILE")
  fi

  # --- the checker script ---
  info "Installing disk check script..."
  cat > "$CHECK_SCRIPT" <<'CHECKEOF'
#!/usr/bin/env bash
#
# Emergency disk ballast checker.
# Deletes the pre-allocated ballast file when free space falls below the
# threshold, buying headroom to log in and clean up. Installed by setup-ubuntu.sh.
#
# Run manually with --status to see current usage without changing anything.

set -euo pipefail

BALLAST="__BALLAST_PATH__"
THRESHOLD=10          # act when free space drops below this percentage
TAG="emergency-disk"

MOUNT_DIR="$(dirname "$BALLAST")"
USE_PCT="$(df -P "$MOUNT_DIR" | awk 'NR==2 {gsub(/%/,"",$5); print $5}')"
FREE_PCT=$(( 100 - USE_PCT ))

if [[ "${1:-}" == "--status" ]]; then
  echo "Mount:     $(df -P "$MOUNT_DIR" | awk 'NR==2 {print $6}')"
  echo "Free:      ${FREE_PCT}% (threshold: ${THRESHOLD}%)"
  if [[ -f "$BALLAST" ]]; then
    echo "Ballast:   present, $(du -h "$BALLAST" | cut -f1) — $BALLAST"
  else
    echo "Ballast:   ABSENT (already spent) — $BALLAST"
  fi
  exit 0
fi

(( FREE_PCT < THRESHOLD )) || exit 0

if [[ -f "$BALLAST" ]]; then
  FREED="$(du -h "$BALLAST" | cut -f1)"
  rm -f "$BALLAST"
  logger -t "$TAG" -p user.crit \
    "Free space ${FREE_PCT}% below ${THRESHOLD}% — deleted ballast, reclaimed ${FREED}. CLEAN UP NOW: this valve is spent."
else
  logger -t "$TAG" -p user.crit \
    "Free space ${FREE_PCT}% below ${THRESHOLD}% — ballast already spent, no headroom left to reclaim."
fi
CHECKEOF

  sed -i "s|__BALLAST_PATH__|$BALLAST_FILE|" "$CHECK_SCRIPT"
  chown "$USERNAME:$USERNAME" "$CHECK_SCRIPT"
  chmod 755 "$CHECK_SCRIPT"
  ok "Check script: $CHECK_SCRIPT"

  # --- the cron entry (root, so it can always delete) ---
  cat > "$CRON_FILE" <<EOF
# Emergency disk ballast — delete the ballast file if free space drops below 10%
# Installed by setup-ubuntu.sh. Check status: $CHECK_SCRIPT --status
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/10 * * * * root $CHECK_SCRIPT
EOF
  chmod 644 "$CRON_FILE"
  ok "Cron installed: every 10 minutes ($CRON_FILE)"

  SUMMARY+=("Disk watchdog: $CHECK_SCRIPT via cron every 10m")
  step_ok
fi

# =============================================================================
# 23. Summary
# =============================================================================

echo ""
echo ""
if [[ ${#ERRORS[@]} -gt 0 ]]; then
  echo -e "${YELLOW}╔══════════════════════════════════════════════════╗${NC}"
  echo -e "${YELLOW}║  Setup Complete (with warnings)                  ║${NC}"
  echo -e "${YELLOW}╚══════════════════════════════════════════════════╝${NC}"
else
  echo -e "${GREEN}╔══════════════════════════════════════════════════╗${NC}"
  echo -e "${GREEN}║  Setup Complete                                  ║${NC}"
  echo -e "${GREEN}╚══════════════════════════════════════════════════╝${NC}"
fi

echo ""
echo -e "${BOLD}  What was done:${NC}"
for item in "${SUMMARY[@]}"; do
  echo -e "    ${GREEN}+${NC} $item"
done

if [[ ${#ERRORS[@]} -gt 0 ]]; then
  echo ""
  echo -e "${BOLD}  Non-critical issues:${NC}"
  for err in "${ERRORS[@]}"; do
    echo -e "    ${YELLOW}!${NC} $err"
  done
fi

echo ""
echo -e "${BOLD}  Server info:${NC}"
echo "    User:       $USERNAME"
echo "    Home:       $USER_HOME"
[[ -n "${TS_IP:-}" && "$TS_IP" != "unknown" ]] && echo "    Tailscale:  $TS_IP"
echo "    Shell:      zsh (Oh My Zsh + Starship)"

echo ""
echo -e "${BOLD}  Next steps:${NC}"
echo "    1. Reconnect as the new user:"
echo -e "       ${CYAN}ssh $USERNAME@<server-ip>${NC}"
echo "    2. Docker group requires a new login session"
echo ""

# Clean up progress file on success
rm -f "$PROGRESS_FILE"
