#!/usr/bin/env bash
# menu.sh — Ubuntu/Debian Setup Manager
#
# Interactive menu that fetches and runs the hardened install scripts in this
# Opengist. Each script is downloaded to a temp file (not blindly piped to bash)
# and executed with the appropriate privilege level for that script:
#
#   - "sudo" mode for system-wide installers (apt, /etc, /usr/local/bin)
#   - "user" mode for per-user installers that must NOT run as root
#     (gsettings, ~/.local, JetBrains Toolbox, fonts)
#
# Usage:
#   bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)"
#
# Or save and run locally:
#   curl -fsSLO https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh
#   bash menu.sh

# Note: NOT using `set -e` because we want the menu loop to survive a failed
# sub-script. We do use -u and pipefail to catch real bugs in this file.
set -uo pipefail
IFS=$'\n\t'

readonly SCRIPT_NAME="${0##*/}"
readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD'

log()  { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; }
err()  { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; }
hr()   { printf '%s\n' "------------------------------------------------------"; }

# Refuse to run as root: per-user scripts (jetbrains, fonts, ibus) need a real
# desktop user. Sub-scripts elevate via sudo on their own.
if (( EUID == 0 )); then
  err "Don't run $SCRIPT_NAME as root. Run as your normal user — it will call sudo for installers that need it."
  exit 1
fi

# Sanity-check tools we depend on.
for tool in curl bash mktemp; do
  command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; }
done

# Distro check (warn-only; sub-scripts enforce strictly).
if [[ -r /etc/os-release ]]; then
  # shellcheck disable=SC1091
  . /etc/os-release
  case "${ID:-}:${ID_LIKE:-}" in
    *ubuntu*|*debian*) : ;;
    *) warn "Detected ${PRETTY_NAME:-unknown}. These installers target Debian/Ubuntu; some will refuse to run." ;;
  esac
fi

# Cache sudo credentials up-front so sub-scripts that elevate don't keep
# re-prompting in the middle of a multi-task run.
prime_sudo() {
  if ! sudo -n true 2>/dev/null; then
    log "Caching sudo credentials (you may be prompted)..."
    sudo -v || { err "sudo authentication failed."; return 1; }
  fi
  # Keep sudo timestamp refreshed in the background while the menu runs.
  ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &
  SUDO_KEEPALIVE_PID=$!
  trap 'kill "${SUDO_KEEPALIVE_PID:-}" 2>/dev/null || true' EXIT
}

# run_script <filename> <mode>
#   mode: "sudo" (run as root via sudo) or "user" (run as current user)
# Returns: 0 on success, non-zero on failure (does NOT exit the menu).
run_script() {
  local name="$1" mode="$2"
  local url="$BASE_URL/$name"
  local tmp
  tmp="$(mktemp -t "${name%.sh}.XXXXXX.sh")" || { err "mktemp failed"; return 1; }

  log "Fetching $name..."
  if ! curl -fsSL --max-time 60 --retry 2 "$url" -o "$tmp"; then
    err "Download failed: $url"
    rm -f "$tmp"
    return 1
  fi
  if [[ ! -s "$tmp" ]]; then
    err "Downloaded $name is empty."
    rm -f "$tmp"
    return 1
  fi
  # Cheap sanity: confirm it looks like a shell script.
  if ! head -n1 "$tmp" | grep -qE '^#!.*sh'; then
    warn "$name doesn't start with a shebang; proceeding anyway."
  fi
  chmod +x "$tmp"

  local rc=0
  if [[ "$mode" == "sudo" ]]; then
    sudo bash "$tmp" || rc=$?
  else
    bash "$tmp" || rc=$?
  fi
  rm -f "$tmp"
  if (( rc != 0 )); then
    err "$name exited with status $rc"
  fi
  return "$rc"
}

# Catalog: number | label | script-filename | mode
# (Edit here to add/remove options — the menu loop is data-driven.)
OPTIONS=(
  "1|Install Google Chrome|install-chrome.sh|sudo"
  "2|Install Firefox (Mozilla APT)|install-firefox.sh|sudo"
  "3|Install Thunderbird|install-thunderbird.sh|sudo"
  "4|Install 1Password|install-1password.sh|sudo"
  "5|Install Espanso (text expander)|install-espanso.sh|sudo"
  "6|Install LibreOffice (latest stable .deb)|install-libreoffice.sh|sudo"
  "7|Install Visual Studio Code|install-vscode.sh|sudo"
  "8|Install JetBrains Toolbox (per-user)|install-jetbrains-toolbox.sh|user"
  "9|Install Bruno (API client)|install-bruno.sh|sudo"
  "10|Install IPATool|install-ipatool.sh|sudo"
  "11|Mount Synology Network Drive|install-network_drive.sh|sudo"
  "12|Install IBus Intelligent Pinyin (per-user)|install-ibus-pinyin.sh|user"
  "13|Install Nerd Fonts (per-user)|install-font.sh|user"
  "14|Fix Dual-Boot Time (RTC to UTC)|timedatectl-fix.sh|sudo"
)

print_menu() {
  hr
  echo "                  Ubuntu / Debian Setup Manager"
  hr
  echo "--- [ Browsers & Mail ] ---"
  echo "   1) Install Google Chrome"
  echo "   2) Install Firefox"
  echo "   3) Install Thunderbird"
  echo "--- [ Productivity & Security ] ---"
  echo "   4) Install 1Password"
  echo "   5) Install Espanso"
  echo "   6) Install LibreOffice"
  echo "--- [ Development Tools ] ---"
  echo "   7) Install Visual Studio Code"
  echo "   8) Install JetBrains Toolbox      (runs as you, not root)"
  echo "   9) Install Bruno"
  echo "  10) Install IPATool"
  echo "--- [ System ] ---"
  echo "  11) Mount Synology Network Drive"
  echo "  12) Install IBus Intelligent Pinyin (runs as you, not root)"
  echo "  13) Install Nerd Fonts              (runs as you, not root)"
  echo "  14) Fix Dual-Boot Time (RTC to UTC)"
  hr
  echo "   0) Run ALL options (1-14)"
  echo "  -1) Exit"
  hr
}

# Resolve a numeric choice to its catalog entry; print "name|mode" to stdout.
resolve_choice() {
  local want="$1" entry num
  for entry in "${OPTIONS[@]}"; do
    num="${entry%%|*}"
    if [[ "$num" == "$want" ]]; then
      # Strip the leading "N|label|"; what remains is "filename|mode"
      printf '%s' "${entry#*|*|}"
      return 0
    fi
  done
  return 1
}

ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12 13 14)

prime_sudo || exit 1

while true; do
  print_menu
  read -rp "Enter choices separated by spaces (e.g., 1 7 12), 0 for ALL, -1 to exit: " choices </dev/tty || {
    echo; log "Input closed; exiting."
    break
  }

  # Trim whitespace
  choices="${choices##[[:space:]]}"
  choices="${choices%%[[:space:]]}"

  if [[ -z "$choices" ]]; then
    continue
  fi

  if [[ "$choices" == "-1" ]]; then
    log "Exiting."
    break
  fi

  # Expand "0" to all options
  if [[ "$choices" == "0" ]]; then
    choices="${ALL_NUMS[*]}"
  fi

  # Validate every token first; reject the whole batch if any token is bogus,
  # so the user sees the problem before any work starts.
  bad=""
  for c in $choices; do
    if ! [[ "$c" =~ ^-?[0-9]+$ ]] || ! resolve_choice "$c" >/dev/null; then
      bad+=" $c"
    fi
  done
  if [[ -n "$bad" ]]; then
    err "Invalid option(s):$bad"
    sleep 1
    continue
  fi

  failures=()
  for c in $choices; do
    spec="$(resolve_choice "$c")"
    name="${spec%|*}"
    mode="${spec##*|}"
    hr
    log "[$c] Running $name ($mode)..."
    if ! run_script "$name" "$mode"; then
      failures+=("$c:$name")
    fi
  done

  hr
  if (( ${#failures[@]} == 0 )); then
    log "All selected tasks completed."
  else
    warn "Completed with ${#failures[@]} failure(s): ${failures[*]}"
  fi
  echo
done
