#!/usr/bin/env bash
# hldeck installer - HomelabDeck mini installer (profiles: starter, media, media-lite)
# Usage: sudo bash install.sh [starter|media|media-lite] [--vpn-provider NAME --vpn-key WG_PRIVATE_KEY] [--dry-run] [--dir PATH] [--no-start]
set -euo pipefail

# ---------- pinned image versions (tested together 2026-11-07; bump here, re-test, release) ----------
V_HOMEPAGE="v2.7.0"
V_KUMA="1.23.17"
V_PORTAINER="2.45.3"
V_NPM="2.16.0"
V_GLUETUN="v3.41.3"
V_QBIT="5.3.0_v2.1.2-ls486"
V_PROWLARR="2.7.0.5723-ls167"
V_SONARR="4.0.20.3014-ls3"
V_RADARR="6.4.4.10685-ls325"
V_JELLYFIN="12.4ubu2604-ls58"
V_SEERR="v3.5.1"

PROFILE="starter"
DIR=""
DRY=0
START=1
VPN_PROVIDER=""
VPN_KEY=""

while [ $# -gt 0 ]; do
  case "$1" in
    starter|media|media-lite) PROFILE="$1" ;;
    --vpn-provider) VPN_PROVIDER="${2:?needs a name}"; shift ;;
    --vpn-key) VPN_KEY="${2:?needs a key}"; shift ;;
    --dry-run) DRY=1; START=0 ;;
    --no-start) START=0 ;;
    --dir) DIR="${2:?--dir needs a path}"; shift ;;
    -h|--help) sed -n 2,3p "$0"; exit 0 ;;
    *) echo "Unknown argument: $1" >&2; exit 2 ;;
  esac
  shift
done
[ -n "$DIR" ] || { [ "$PROFILE" = starter ] && DIR=/opt/hldeck || DIR="/opt/hldeck-$PROFILE"; }
PROJECT="$(basename "$DIR")"

say()  { printf '\033[1;35m[hldeck]\033[0m %s\n' "$*"; }
die()  { printf '\033[1;31m[hldeck] ERROR:\033[0m %s\n' "$*" >&2; exit 1; }

# ---------- 1. preflight checks ----------
[ "$DRY" = 1 ] || [ "$(id -u)" = 0 ] || die "Run as root (sudo bash install.sh)."

if [ -r /etc/os-release ]; then . /etc/os-release; fi
case "${ID:-}${ID_LIKE:-}" in
  *debian*|*ubuntu*) ;;
  *) die "Only Debian/Ubuntu are supported in this version (found: ${PRETTY_NAME:-unknown})." ;;
esac

ARCH="$(uname -m)"
[ "$ARCH" = x86_64 ] || [ "$ARCH" = aarch64 ] || die "Unsupported CPU: $ARCH"

PARENT="$DIR"; while [ ! -d "$PARENT" ]; do PARENT="$(dirname "$PARENT")"; done
FREE_GB=$(( $(df -Pk "$PARENT" | awk 'NR==2{print $4}') / 1024 / 1024 ))
[ "$FREE_GB" -ge 5 ] || die "Need at least 5 GB free on $PARENT (have ${FREE_GB} GB)."

# ports used by the starter profile
case "$PROFILE" in
  starter) PORTS="80 81 443 3000 3001 8000 9443" ;;
  media|media-lite) PORTS="8080 8096 8989 7878 9696 5055" ;;
esac
busy=""
if command -v docker >/dev/null 2>&1 && [ -n "$(docker ps -q --filter label=com.docker.compose.project=$PROJECT 2>/dev/null)" ]; then
  PORTS=""; say "Existing $PROJECT stack found: updating it in place."
fi
for p in $PORTS; do
  if command -v ss >/dev/null 2>&1 && ss -ltnH "sport = :$p" 2>/dev/null | grep -q .; then busy="$busy $p"; fi
done
[ -z "$busy" ] || die "These ports are already in use:$busy. Free them and run again."

IP="$(hostname -I 2>/dev/null | awk '{print $1}')"; IP="${IP:-localhost}"
say "Checks passed (${PRETTY_NAME:-linux}, $ARCH, ${FREE_GB} GB free, IP $IP)."

# ---------- 2. docker ----------
if ! command -v docker >/dev/null 2>&1; then
  if [ "$DRY" = 1 ]; then
    say "[dry-run] Docker is missing; would install it from the official apt repository."
  else
    say "Installing Docker (official get.docker.com script)..."
    curl -fsSL https://get.docker.com | sh >/dev/null
  fi
fi
if [ "$DRY" = 0 ]; then
  docker compose version >/dev/null 2>&1 || die "'docker compose' plugin is missing."
  systemctl enable --now docker >/dev/null 2>&1 || true
fi

# ---------- 3. generate the stack ----------
say "Generating profile '$PROFILE' in $DIR"
if [ "$DRY" = 1 ]; then OUT="$(mktemp -d)"; else OUT="$DIR"; mkdir -p "$OUT"; fi
# services shared by media and media-lite (everything except qBittorrent/gluetun)
media_apps() {
cat <<APPS
  prowlarr:
    <<: *arr
    image: lscr.io/linuxserver/prowlarr:${V_PROWLARR}
    container_name: prowlarr
    ports: ["9696:9696"]
    volumes: ["./config/prowlarr:/config"]

  sonarr:
    <<: *arr
    image: lscr.io/linuxserver/sonarr:${V_SONARR}
    container_name: sonarr
    ports: ["8989:8989"]
    volumes: ["./config/sonarr:/config", "./data:/data"]

  radarr:
    <<: *arr
    image: lscr.io/linuxserver/radarr:${V_RADARR}
    container_name: radarr
    ports: ["7878:7878"]
    volumes: ["./config/radarr:/config", "./data:/data"]

  jellyfin:
    <<: *arr
    image: lscr.io/linuxserver/jellyfin:${V_JELLYFIN}
    container_name: jellyfin
    ports: ["8096:8096"]
    volumes: ["./config/jellyfin:/config", "./data/media:/data/media:ro"]

  seerr:
    image: ghcr.io/seerr-team/seerr:${V_SEERR}
    container_name: seerr
    init: true
    restart: unless-stopped
    user: "1000:1000"
    environment:
      TZ: "${TZ_NAME}"
      PORT: "5055"
    ports: ["5055:5055"]
    volumes: ["./config/seerr:/app/config"]
APPS
}

gen_media() {
mkdir -p "$OUT/data/torrents" "$OUT/data/media/tv" "$OUT/data/media/movies" \
         "$OUT/config/qbittorrent" "$OUT/config/prowlarr" "$OUT/config/sonarr" \
         "$OUT/config/radarr" "$OUT/config/jellyfin" "$OUT/config/seerr" "$OUT/config/gluetun"
[ "$DRY" = 1 ] || chown -R 1000:1000 "$OUT/data" "$OUT/config"

if [ ! -e "$OUT/vpn.env" ]; then
  umask 077
  cat > "$OUT/vpn.env" <<VPNENV
# WireGuard settings for gluetun. Provider list: https://github.com/qdm12/gluetun-wiki
VPN_SERVICE_PROVIDER=${VPN_PROVIDER:-CHANGE_ME}
VPN_TYPE=wireguard
WIREGUARD_PRIVATE_KEY=${VPN_KEY:-CHANGE_ME}
FIREWALL_INPUT_PORTS=8080
VPNENV
  umask 022
fi
if grep -q CHANGE_ME "$OUT/vpn.env"; then VPN_READY=0; else VPN_READY=1; fi

cat > "$OUT/docker-compose.yml" <<COMPOSE
# Generated by hldeck (HomelabDeck). Plain Docker Compose: edit freely.
# qBittorrent has no network of its own: all its traffic goes through gluetun.
# If the VPN drops, gluetun's firewall blocks everything (kill switch).
name: ${PROJECT}
x-arr: &arr
  restart: unless-stopped
  environment: &arr-env
    PUID: "1000"
    PGID: "1000"
    TZ: "${TZ_NAME}"

services:
  gluetun:
    image: qmcgaw/gluetun:${V_GLUETUN}
    container_name: gluetun
    restart: unless-stopped
    cap_add: [NET_ADMIN]
    devices: ["/dev/net/tun:/dev/net/tun"]
    env_file: ./vpn.env
    ports:
      - "8080:8080"   # qBittorrent web UI (published through gluetun)
    volumes:
      - ./config/gluetun:/gluetun

  qbittorrent:
    <<: *arr
    image: lscr.io/linuxserver/qbittorrent:${V_QBIT}
    container_name: qbittorrent
    network_mode: "service:gluetun"
    depends_on:
      gluetun: { condition: service_healthy }
    environment:
      <<: *arr-env
      WEBUI_PORT: "8080"
    volumes:
      - ./config/qbittorrent:/config
      - ./data:/data

$(media_apps)
COMPOSE
}

gen_media_lite() {
mkdir -p "$OUT/data/torrents" "$OUT/data/media/tv" "$OUT/data/media/movies" \
         "$OUT/config/qbittorrent" "$OUT/config/prowlarr" "$OUT/config/sonarr" \
         "$OUT/config/radarr" "$OUT/config/jellyfin" "$OUT/config/seerr"
[ "$DRY" = 1 ] || chown -R 1000:1000 "$OUT/data" "$OUT/config"

cat > "$OUT/docker-compose.yml" <<COMPOSE
# Generated by hldeck (HomelabDeck), profile media-lite. Plain Docker Compose: edit freely.
# media-lite has NO VPN: qBittorrent talks to the internet directly.
# Use it for your own and legally obtained media. Want a VPN kill switch? Use the 'media' profile.
name: ${PROJECT}
x-arr: &arr
  restart: unless-stopped
  environment: &arr-env
    PUID: "1000"
    PGID: "1000"
    TZ: "${TZ_NAME}"

services:
  qbittorrent:
    <<: *arr
    image: lscr.io/linuxserver/qbittorrent:${V_QBIT}
    container_name: qbittorrent
    environment:
      <<: *arr-env
      WEBUI_PORT: "8080"
    ports: ["8080:8080", "6881:6881", "6881:6881/udp"]
    volumes:
      - ./config/qbittorrent:/config
      - ./data:/data

$(media_apps)
COMPOSE
}

gen_starter() {
mkdir -p "$OUT/homepage/config" "$OUT/homepage/images" "$OUT/npm/data" "$OUT/npm/letsencrypt"

cat > "$OUT/docker-compose.yml" <<COMPOSE
# Generated by hldeck (HomelabDeck). Plain Docker Compose: edit freely.
name: ${PROJECT}
services:
  homepage:
    image: ghcr.io/gethomepage/homepage:${V_HOMEPAGE}
    container_name: homepage
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      HOMEPAGE_ALLOWED_HOSTS: "localhost:3000,${IP}:3000"
    volumes:
      - ./homepage/config:/app/config
      - ./homepage/images:/app/public/images

  uptime-kuma:
    image: louislam/uptime-kuma:${V_KUMA}
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "3001:3001"
    volumes:
      - uptime-kuma-data:/app/data

  portainer:
    image: portainer/portainer-ce:${V_PORTAINER}
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    ports:
      - "8000:8000"
      - "9443:9443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer-data:/data

  nginx-proxy-manager:
    image: jc21/nginx-proxy-manager:${V_NPM}
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "81:81"
      - "443:443"
    volumes:
      - ./npm/data:/data
      - ./npm/letsencrypt:/etc/letsencrypt

volumes:
  uptime-kuma-data:
  portainer-data:
COMPOSE

# Homepage ships empty: give it a ready-made services list
if [ ! -s "$OUT/homepage/config/services.yaml" ]; then
cat > "$OUT/homepage/config/services.yaml" <<SERVICES
- HomelabDeck starter:
    - Uptime Kuma:
        href: http://${IP}:3001
        description: Monitor your services
    - Portainer:
        href: https://${IP}:9443
        description: Manage containers
    - Nginx Proxy Manager:
        href: http://${IP}:81
        description: Reverse proxy and SSL
SERVICES
fi

}

TZ_NAME="$(cat /etc/timezone 2>/dev/null || echo UTC)"
VPN_READY=1
if [ -e "$OUT/docker-compose.yml" ] && [ "$DRY" = 0 ]; then
  cp "$OUT/docker-compose.yml" "$OUT/docker-compose.yml.bak.$(date +%s)"
  say "Existing docker-compose.yml backed up."
fi

case "$PROFILE" in starter) gen_starter ;; media) gen_media ;; media-lite) gen_media_lite ;; esac

if [ "$DRY" = 1 ]; then
  say "[dry-run] Files written to $OUT (not installed). Compose:"
  [ -x "$(command -v docker)" ] && docker compose -f "$OUT/docker-compose.yml" config -q && say "[dry-run] compose syntax OK"
  exit 0
fi

# ---------- 3b. hldeck CLI ----------
if [ "$DRY" = 0 ]; then
  SELF="$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")"
  if [ -f "$SELF/hldeck" ]; then install -m 755 "$SELF/hldeck" /usr/local/bin/hldeck
  else curl -fsSL "${HLDECK_URL:-https://homelabdeck.dev/hldeck.sh}" -o /usr/local/bin/hldeck && chmod 755 /usr/local/bin/hldeck \
       || say "Could not install the hldeck command (optional)."; fi
fi

# ---------- 4. start ----------
fail=0
COMPOSE_F="docker compose -f $OUT/docker-compose.yml"
if [ "$PROFILE" = starter ]; then
  CHECKS="Homepage:3000 Uptime-Kuma:3001 Portainer:9443 Nginx-Proxy-Manager:81"; SVCS=""
else
  CHECKS="Jellyfin:8096 Sonarr:8989 Radarr:7878 Prowlarr:9696 Seerr:5055"; SVCS="prowlarr sonarr radarr jellyfin seerr"
  # Without a VPN key we never start qBittorrent: no key = no torrent traffic.
  if [ "$PROFILE" = media ] && [ "$VPN_READY" = 1 ]; then SVCS="$SVCS gluetun qbittorrent"; CHECKS="$CHECKS qBittorrent:8080"; fi
  if [ "$PROFILE" = media-lite ]; then SVCS="$SVCS qbittorrent"; CHECKS="$CHECKS qBittorrent:8080"; fi
fi
if [ "$START" = 1 ]; then
  say "Pulling images and starting (first run takes a few minutes)..."
  $COMPOSE_F up -d $SVCS
  say "Waiting for the services..."
  for pair in $CHECKS; do
    name="${pair%:*}"; port="${pair##*:}"; ok=0
    for _ in $(seq 1 60); do
      if curl -ksf -o /dev/null --max-time 3 "http://127.0.0.1:$port" || curl -ksf -o /dev/null --max-time 3 "https://127.0.0.1:$port"; then ok=1; break; fi
      sleep 2
    done
    if [ "$ok" = 1 ]; then say "  OK   ${name//-/ }"; else say "  FAIL ${name//-/ } (docker logs ${name,,})"; fail=1; fi
  done
fi

# Connect the apps to each other (needs qBittorrent, so only when the VPN key is set)
if [ "${PROFILE#media}" != "$PROFILE" ] && [ "$START" = 1 ] && [ "$VPN_READY" = 1 ] && [ "$fail" = 0 ]; then
  /usr/local/bin/hldeck wire "$OUT" || say "Auto-connect failed; run 'hldeck wire' again later."
fi

# ---------- 5. summary ----------
if [ "$PROFILE" = starter ]; then
cat <<SUMMARY

  HomelabDeck starter is ready.
    Homepage            http://${IP}:3000
    Uptime Kuma         http://${IP}:3001   (create the admin account on first visit)
    Portainer           https://${IP}:9443  (create the admin account within 5 minutes!)
    Nginx Proxy Manager http://${IP}:81     (create the admin account on first visit)

  Files: $OUT   (stop: $COMPOSE_F down)
  Manage: hldeck status | update | backup | export
  Guides: https://homelabdeck.dev/en/guides

SUMMARY
else
cat <<SUMMARY

  HomelabDeck media stack is up.
    Jellyfin   http://${IP}:8096     Sonarr   http://${IP}:8989
    Radarr     http://${IP}:7878     Prowlarr http://${IP}:9696
    Seerr      http://${IP}:5055     (search/request movies and series; sign in with your Jellyfin account)
SUMMARY
  if [ "$PROFILE" = media-lite ]; then
    echo "    qBittorrent http://${IP}:8080   (temporary password: docker logs qbittorrent)"
    echo
    echo "  NOTE: media-lite has no VPN. qBittorrent connects to the internet directly."
    echo "  Use it for your own and legally obtained media. For a VPN kill switch use: install.sh media"
  elif [ "$VPN_READY" = 1 ]; then
    echo "    qBittorrent http://${IP}:8080   (temporary password: docker logs qbittorrent)"
  else
    echo
    echo "  qBittorrent + VPN are NOT started yet (no VPN key)."
    echo "  Edit $OUT/vpn.env (provider + WireGuard key), then run:  $COMPOSE_F up -d && hldeck wire"
  fi
cat <<SUMMARY

  Manage: hldeck status | update | backup | export
  Media folders: $OUT/data/torrents, data/media/tv, data/media/movies (one /data mount = hardlinks work)
  Files: $OUT   Guide: https://homelabdeck.dev/en/guides/jellyfin-sonarr-radarr-stack-proxmox-lxc

SUMMARY
fi
exit "$fail"
