#!/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.resetrix.work/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)"
#
# Or save and run locally:
#   curl -fsSLO https://opengist.resetrix.work/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.resetrix.work/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 | category | label | script-filename | mode | include-in-all
# (Edit here to add/remove options — the menu loop is data-driven.)
OPTIONS=(
  "1|Browsers & Mail|Install Google Chrome|install-chrome.sh|sudo|yes"
  "2|Browsers & Mail|Install Firefox (Mozilla APT)|install-firefox.sh|sudo|yes"
  "3|Browsers & Mail|Install Thunderbird|install-thunderbird.sh|sudo|yes"
  "4|Communication|Install Telegram Desktop|install-telegram.sh|sudo|yes"
  "5|Communication|Install Discord|install-discord.sh|sudo|yes"
  "6|Files, Downloads & Storage|Install LocalSend|install-localsend.sh|sudo|yes"
  "7|Files, Downloads & Storage|Install qBittorrent (latest AppImage)|install-qbittorrent.sh|sudo|yes"
  "8|Files, Downloads & Storage|Mount Synology Network Drive|install-network_drive.sh|sudo|yes"
  "9|Productivity & Security|Install LibreOffice (latest stable .deb)|install-libreoffice.sh|sudo|yes"
  "10|Productivity & Security|Install Obsidian|install-obsidian.sh|sudo|yes"
  "11|Productivity & Security|Install MarkText (Markdown editor)|install-marktext.sh|sudo|yes"
  "12|Productivity & Security|Install draw.io Desktop|install-drawio.sh|sudo|yes"
  "13|Productivity & Security|Install Espanso (text expander)|install-espanso.sh|sudo|yes"
  "14|Productivity & Security|Install 1Password|install-1password.sh|sudo|yes"
  "15|Development Tools|Install Visual Studio Code|install-vscode.sh|sudo|yes"
  "16|Development Tools|Install JetBrains Toolbox|install-jetbrains-toolbox.sh|user|yes"
  "17|Development Tools|Install Bruno (API client)|install-bruno.sh|sudo|yes"
  "18|Development Tools|Install IPATool|install-ipatool.sh|sudo|yes"
  "19|Desktop & Localization|Install IBus Intelligent Pinyin|install-ibus-pinyin.sh|user|yes"
  "20|Desktop & Localization|Install Nerd Fonts|install-font.sh|user|yes"
  "21|System & Maintenance|Install Screenshots Cleanup Cron|install-screenshot-cleanup-cron.sh|user|yes"
  "22|System & Maintenance|Fix Dual-Boot Time (RTC to UTC)|timedatectl-fix.sh|sudo|yes"
  "23|System & Maintenance|Disable GRUB Countdown Timer|disable-grub-countdown.sh|sudo|yes"
  "24|Backup|Run Thunderbird Synology Backup|backup-thunderbird-to-synology.sh|user|no"
  "25|Backup|Install CCUsage Multi-Device Sync|install-ccusage-sync.sh|user|no"
  "26|System & Maintenance|Update All Software|update-all|group|no"
)

# Software installers that are safe to re-run to fetch current packages and
# releases. Configuration, maintenance, backup, and sync tasks are excluded.
SOFTWARE_NUMS=(1 2 3 4 5 6 7 9 10 11 12 13 14 15 16 17 18 19 20)

print_menu() {
  local entry num category label filename mode include_in_all
  local last_category="" note display_label

  hr
  echo "                  Ubuntu / Debian Setup Manager"
  hr
  for entry in "${OPTIONS[@]}"; do
    IFS='|' read -r num category label filename mode include_in_all <<< "$entry"

    if [[ "$category" != "$last_category" ]]; then
      printf '%s\n' "--- [ $category ] ---"
      last_category="$category"
    fi

    note=""
    if [[ "$mode" == "group" ]]; then
      note="runs software options 1-7 and 9-20"
    elif [[ "$mode" == "user" ]]; then
      note="runs as you, not root"
    fi
    if [[ "$include_in_all" != "yes" && "$mode" != "group" ]]; then
      [[ -n "$note" ]] && note+="; "
      note+="not in ALL"
    fi

    display_label="$label"
    [[ -n "$note" ]] && display_label+=" ($note)"
    printf '%4s) %s\n' "$num" "$display_label"
  done
  hr
  echo "   0) Run ALL setup options (1-23)"
  echo "  -1) Exit"
  hr
}

# Resolve a numeric choice to its catalog entry; print "name|mode" to stdout.
resolve_choice() {
  local want="$1" entry num category label filename mode include_in_all
  for entry in "${OPTIONS[@]}"; do
    IFS='|' read -r num category label filename mode include_in_all <<< "$entry"
    if [[ "$num" == "$want" ]]; then
      printf '%s|%s' "$filename" "$mode"
      return 0
    fi
  done
  return 1
}

ALL_NUMS=()
for entry in "${OPTIONS[@]}"; do
  IFS='|' read -r num category label filename mode include_in_all <<< "$entry"
  [[ "$include_in_all" == "yes" ]] && ALL_NUMS+=("$num")
done

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

  selected_choices=()

  # Expand "0" to all options
  if [[ "$choices" == "0" ]]; then
    selected_choices=("${ALL_NUMS[@]}")
  else
    IFS=$' \t\n' read -r -a selected_choices <<< "$choices"
  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 "${selected_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 "${selected_choices[@]}"; do
    if [[ "$c" == "26" ]]; then
      hr
      log "[26] Updating all software..."
      for software_choice in "${SOFTWARE_NUMS[@]}"; do
        spec="$(resolve_choice "$software_choice")"
        name="${spec%|*}"
        mode="${spec##*|}"
        hr
        log "[$software_choice] Running $name ($mode)..."
        if ! run_script "$name" "$mode"; then
          failures+=("$software_choice:$name")
        fi
      done
      continue
    fi

    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
