#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

APP_NAME="WeltEndzeit BotPanel"
APP_VERSION="0.12.4"
APP_DIR="/opt/weltendzeit-bots"
APP_CODE="$APP_DIR/app"
DATA_DIR="$APP_DIR/data"
RUNTIME_DIR="$APP_DIR/runtime"
ETC_DIR="/etc/weltendzeit-bots"
SERVICE="weltendzeit-botpanel.service"
SERVICE_USER="weltendzeitbot"

# This URL is ONLY the package distribution source.
# It is NOT the domain on which the installed panel must run.
DOWNLOAD_BASE_URL="${WELTENDZEIT_INSTALLER_URL:-https://botzeug.weltendzeit.info/installer}"
PACKAGE="weltendzeit-botpanel.tar.gz"

# May be overridden, e.g.:
# WELTENDZEIT_PORT=8790 bash <(curl ...)
PORT="${WELTENDZEIT_PORT:-8787}"
BIND_HOST="127.0.0.1"

green(){ printf '\033[1;32m%s\033[0m\n' "$*"; }
cyan(){ printf '\033[1;36m%s\033[0m\n' "$*"; }
yellow(){ printf '\033[1;33m%s\033[0m\n' "$*"; }
red(){ printf '\033[1;31m%s\033[0m\n' "$*" >&2; }
die(){ red "FEHLER: $*"; exit 1; }

[[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausführen."
[[ -r /etc/os-release ]] || die "/etc/os-release fehlt."

. /etc/os-release
[[ "${ID:-}" == "debian" ]] || yellow "Hinweis: Dieser Installer ist für Debian 13 vorgesehen."
if [[ "${VERSION_ID:-0}" != "13" ]]; then
  yellow "Erkannt: ${PRETTY_NAME:-unbekannt}. Debian 13 wird empfohlen."
fi

[[ "$PORT" =~ ^[0-9]+$ ]] || die "Ungültiger Port: $PORT"
(( PORT >= 1024 && PORT <= 65535 )) || die "Port muss zwischen 1024 und 65535 liegen."

ARCH="$(uname -m)"
case "$ARCH" in
  x86_64) NODE_ARCH="x64" ;;
  aarch64|arm64) NODE_ARCH="arm64" ;;
  *) die "Nicht unterstützte Architektur: $ARCH" ;;
esac

clear || true
cat <<'BANNER'
 __        __    _ _   _____           _          _ _
 \ \      / /__| | |_| ____|_ __   __| |_______(_) |_
  \ \ /\ / / _ \ | __|  _| | '_ \ / _` |_  / _ \ | __|
   \ V  V /  __/ | |_| |___| | | | (_| |/ /  __/ | |_
    \_/\_/ \___|_|\__|_____|_| |_|\__,_/___\___|_|\__|

             BOT CONTROL SELFHOSTED // INSTALLER 0.12.4
BANNER

cyan "Installationsziel: Debian 13"

echo
green "Lizenzsystem:"
echo "  Premium/Hoster werden zentral über https://lizenz.weltendzeit.info verwaltet."
echo "  Auf diesem Kundenserver wird KEIN Lizenzserver und KEINE BTC-Wallet installiert."
echo
yellow "Wichtig: Dieser Installer verändert KEINE Konfiguration von KeyHelp, Plesk, cPanel, Apache oder nginx."
yellow "Er führt außerdem KEIN apt upgrade / full-upgrade aus."
echo
green "Betriebsart: SELFHOSTED"
echo "  Discord Login : DEAKTIVIERT"
echo "  Gift-Bot      : DEAKTIVIERT"
echo "  Bots          : nur eigene Discord Bot-Tokens"
echo

# Detect common control panels only to inform the admin.
PANEL_HINTS=()
[[ -d /etc/keyhelp || -d /home/keyhelp || -e /usr/local/keyhelp ]] && PANEL_HINTS+=("KeyHelp")
[[ -d /usr/local/psa || -d /opt/psa || -e /usr/sbin/plesk ]] && PANEL_HINTS+=("Plesk")
[[ -d /usr/local/cpanel || -e /usr/local/cpanel/cpanel ]] && PANEL_HINTS+=("cPanel")

if (( ${#PANEL_HINTS[@]} > 0 )); then
  green "Erkanntes Hosting-/Serverpanel: ${PANEL_HINTS[*]}"
  green "Die Panel-Konfiguration bleibt vollständig unangetastet."
else
  cyan "Kein bekanntes Hosting-Panel eindeutig erkannt. Das ist kein Problem."
fi

EXISTING_INSTALL=0
if [[ -f "$APP_CODE/server.mjs" ]] || systemctl cat "$SERVICE" >/dev/null 2>&1; then
  EXISTING_INSTALL=1
fi

OLD_APP_VERSION=""
if [[ "$EXISTING_INSTALL" -eq 1 && -f "$APP_CODE/lib/version.mjs" ]]; then
  OLD_APP_VERSION="$(sed -n "s/.*APP_VERSION = '\([^']*\)'.*/\1/p" "$APP_CODE/lib/version.mjs" | head -n1)"
fi

if command -v ss >/dev/null 2>&1 && ss -ltn | awk '{print $4}' | grep -Eq "[:.]${PORT}$"; then
  if [[ "$EXISTING_INSTALL" -eq 1 ]]; then
    yellow "Port $PORT ist bereits belegt – vorhandene WeltEndzeit-Installation erkannt."
    cyan "Das ist bei einem Update normal. Der bestehende Dienst wird später kontrolliert gestoppt."
  else
    die "Port $PORT ist bereits durch einen anderen Dienst belegt. Verwende z. B.: WELTENDZEIT_PORT=8790 bash <(curl -fsSL $DOWNLOAD_BASE_URL/install-script.sh)"
  fi
fi

cyan "Installiere nur notwendige Systempakete ..."
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  ca-certificates curl xz-utils ffmpeg python3 >/dev/null

cyan "Installiere aktuelle offizielle yt-dlp Nightly-Binary ..."
YTDLP_TMP="$(mktemp)"
if ! curl -fL --retry 4 --retry-delay 2 --retry-all-errors --connect-timeout 15 \
  "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" \
  -o "$YTDLP_TMP"; then
  rm -f "$YTDLP_TMP"
  die "Offizielle yt-dlp Binary konnte nicht von GitHub geladen werden."
fi
install -m 0755 "$YTDLP_TMP" /usr/local/bin/yt-dlp
rm -f "$YTDLP_TMP"

if ! /usr/local/bin/yt-dlp --update-to nightly >/dev/null 2>&1; then
  yellow "yt-dlp Nightly-Update nicht möglich – verwende die aktuelle Stable-Binary."
fi
command -v yt-dlp >/dev/null 2>&1 || die "yt-dlp wurde nicht korrekt installiert."
YTDLP_VERSION="$(yt-dlp --version 2>/dev/null || true)"
green "yt-dlp bereit: ${YTDLP_VERSION:-Version unbekannt}"


command -v ffmpeg >/dev/null || die "FFmpeg konnte nicht installiert werden."

if ! id "$SERVICE_USER" >/dev/null 2>&1; then
  useradd --system --home-dir "$APP_DIR" --create-home --shell /usr/sbin/nologin "$SERVICE_USER"
fi

mkdir -p "$APP_CODE" "$DATA_DIR" "$RUNTIME_DIR" "$ETC_DIR"
chown -R "$SERVICE_USER:$SERVICE_USER" "$APP_DIR"
chmod 750 "$APP_DIR" "$DATA_DIR"

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

cyan "Lade Anwendungspaket herunter ..."
cyan "Quelle: $DOWNLOAD_BASE_URL/$PACKAGE"
curl -fL --retry 3 --connect-timeout 15 "$DOWNLOAD_BASE_URL/$PACKAGE" -o "$TMP/$PACKAGE"

ACTUAL_SHA256="$(sha256sum "$TMP/$PACKAGE" | awk '{print $1}')"
EXPECTED_SHA256="06b0bc11d670b8feb2058a462b17c5243298fd97eb452d15b3b7cd5cb1265bb8"
[[ "$ACTUAL_SHA256" == "$EXPECTED_SHA256" ]] || \
  die "SHA256-Prüfung fehlgeschlagen. Erwartet: $EXPECTED_SHA256 / Erhalten: $ACTUAL_SHA256"
green "Paketintegrität OK."

if [[ "$EXISTING_INSTALL" -eq 1 ]]; then
  cyan "Vorhandene Installation erkannt. Anwendung wird aktualisiert; persistente Daten bleiben erhalten."
  systemctl stop "$SERVICE" 2>/dev/null || true

  # Give the old Node process a moment to release the listening socket.
  for _ in 1 2 3 4 5 6 7 8 9 10; do
    if ! command -v ss >/dev/null 2>&1 || ! ss -ltn | awk '{print $4}' | grep -Eq "[:.]${PORT}$"; then
      break
    fi
    sleep 0.5
  done

  # If the port is still occupied after stopping our service, another process owns it.
  if command -v ss >/dev/null 2>&1 && ss -ltn | awk '{print $4}' | grep -Eq "[:.]${PORT}$"; then
    red "Port $PORT ist nach dem Stop von $SERVICE weiterhin belegt."
    if command -v ss >/dev/null 2>&1; then
      ss -ltnp 2>/dev/null | grep -E "[:.]${PORT}[[:space:]]" || true
    fi
    die "Ein anderer Prozess blockiert Port $PORT. Diesen Prozess zuerst prüfen oder WELTENDZEIT_PORT ändern."
  fi

  if [[ -d "$APP_CODE" ]]; then
    find "$APP_CODE" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
  fi
fi

tar -xzf "$TMP/$PACKAGE" -C "$APP_CODE"
chown -R "$SERVICE_USER:$SERVICE_USER" "$APP_CODE"

NODE_DIR="$RUNTIME_DIR/node"
need_node=1
if [[ -x "$NODE_DIR/bin/node" ]]; then
  CURRENT="$("$NODE_DIR/bin/node" -p "process.versions.node")"
  if "$NODE_DIR/bin/node" -e \
    "const [a,b]=process.versions.node.split('.').map(Number);process.exit(a>24||(a===24&&b>=17)?0:1)"; then
    green "Lokales Node.js $CURRENT ist ausreichend."
    need_node=0
  fi
fi

if [[ "$need_node" -eq 1 ]]; then
  cyan "Installiere aktuelles Node.js 24.x ausschließlich unter $NODE_DIR ..."
  NODE_VERSION="$(curl -fsSL https://nodejs.org/dist/index.json | \
    python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(x["version"] for x in d if x["version"].startswith("v24.")))')"

  [[ "$NODE_VERSION" =~ ^v24\. ]] || die "Konnte Node.js 24.x nicht ermitteln."
  curl -fL --retry 3 \
    "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$NODE_ARCH.tar.xz" \
    -o "$TMP/node.tar.xz"

  rm -rf "$NODE_DIR"
  mkdir -p "$NODE_DIR"
  tar -xJf "$TMP/node.tar.xz" --strip-components=1 -C "$NODE_DIR"
  green "Node.js $("$NODE_DIR/bin/node" -v) lokal installiert."
fi

export PATH="$NODE_DIR/bin:$PATH"

cyan "Installiere Node-Abhängigkeiten ..."
cd "$APP_CODE"
runuser -u "$SERVICE_USER" -- env PATH="$PATH" \
  npm install --omit=dev --no-audit --no-fund >/dev/null

if [[ ! -f "$ETC_DIR/master.key" ]]; then
  head -c 32 /dev/urandom | base64 > "$ETC_DIR/master.key"
fi

if [[ ! -f "$ETC_DIR/session.secret" ]]; then
  head -c 48 /dev/urandom | base64 > "$ETC_DIR/session.secret"
fi

chown "$SERVICE_USER:$SERVICE_USER" "$ETC_DIR/master.key" "$ETC_DIR/session.secret"
chmod 600 "$ETC_DIR/master.key" "$ETC_DIR/session.secret"


# ----------------------------------------------------------------------
# Central WeltEndzeit licensing
# ----------------------------------------------------------------------
INSTALLATION_ID_FILE="$ETC_DIR/installation.id"
INSTALLATION_SECRET_FILE="$ETC_DIR/installation.secret"
LICENSE_PUBLIC_KEY_FILE="$ETC_DIR/license-public.pem"
LICENSE_API_URL="https://lizenz.weltendzeit.info"

if [[ ! -s "$INSTALLATION_ID_FILE" ]]; then
  if [[ -r /proc/sys/kernel/random/uuid ]]; then
    cat /proc/sys/kernel/random/uuid > "$INSTALLATION_ID_FILE"
  else
    "$NODE_DIR/bin/node" -e "console.log(require('node:crypto').randomUUID())" > "$INSTALLATION_ID_FILE"
  fi
fi

if [[ ! -s "$INSTALLATION_SECRET_FILE" ]]; then
  head -c 48 /dev/urandom | base64 | tr -d '\n' > "$INSTALLATION_SECRET_FILE"
  printf '\n' >> "$INSTALLATION_SECRET_FILE"
fi

chown root:"$SERVICE_USER" "$INSTALLATION_ID_FILE" "$INSTALLATION_SECRET_FILE"
chmod 640 "$INSTALLATION_ID_FILE" "$INSTALLATION_SECRET_FILE"

# Refresh the public verification key on every install/update.
# A failed refresh never deletes an already valid local key.
cyan "Prüfe öffentlichen WeltEndzeit-Lizenzschlüssel ..."
if curl -fsSL --connect-timeout 10 "$LICENSE_API_URL/public-key" -o "$TMP/license-public.pem" \
    && grep -q "BEGIN PUBLIC KEY" "$TMP/license-public.pem" \
    && grep -q "END PUBLIC KEY" "$TMP/license-public.pem"; then
  install -o root -g "$SERVICE_USER" -m 640 "$TMP/license-public.pem" "$LICENSE_PUBLIC_KEY_FILE"
  green "WeltEndzeit Lizenz-Public-Key installiert/aktualisiert."
elif [[ -s "$LICENSE_PUBLIC_KEY_FILE" ]] \
    && grep -q "BEGIN PUBLIC KEY" "$LICENSE_PUBLIC_KEY_FILE" \
    && grep -q "END PUBLIC KEY" "$LICENSE_PUBLIC_KEY_FILE"; then
  yellow "Zentraler Lizenzserver war beim Key-Refresh nicht erreichbar."
  yellow "Vorhandener lokaler Public Key bleibt erhalten."
else
  yellow "WeltEndzeit Lizenz-Public-Key fehlt weiterhin."
  yellow "Prüfe: $LICENSE_API_URL/public-key"
  yellow "Free funktioniert, BotShop/Premium/Hoster aber erst nach erfolgreichem Key-Abruf."
fi

cat > "$ETC_DIR/app.env" <<EOF
HOST=$BIND_HOST
PORT=$PORT
DATA_DIR=$DATA_DIR
MASTER_KEY_FILE=$ETC_DIR/master.key
SESSION_SECRET_FILE=$ETC_DIR/session.secret
COOKIE_SECURE=1
ALLOW_REGISTRATION=0
SESSION_TTL_SECONDS=604800
LICENSE_API_URL=https://lizenz.weltendzeit.info
INSTALLATION_ID_FILE=$ETC_DIR/installation.id
INSTALLATION_SECRET_FILE=$ETC_DIR/installation.secret
LICENSE_PUBLIC_KEY_FILE=$ETC_DIR/license-public.pem
MODS_CATALOG_URL=https://botzeug.weltendzeit.info/mods/catalog.json
CONTROL_API_URL=https://botsuperadmin.weltendzeit.info
EXTENSIONS_DIR=$DATA_DIR/extensions
CONTROL_REPORT_SECONDS=30
EXTENSION_REFRESH_SECONDS=900
DEPLOYMENT_MODE=selfhosted
HOSTED_MODE=0
EOF

chown root:"$SERVICE_USER" "$ETC_DIR/app.env"
chmod 640 "$ETC_DIR/app.env"

cat > "/etc/systemd/system/$SERVICE" <<EOF
[Unit]
Description=WeltEndzeit Discord Bot Control Panel
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$SERVICE_USER
Group=$SERVICE_USER
WorkingDirectory=$APP_CODE
EnvironmentFile=$ETC_DIR/app.env
Environment=PATH=$NODE_DIR/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=$NODE_DIR/bin/node $APP_CODE/server.mjs
Restart=always
RestartSec=4
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
ReadWritePaths=$DATA_DIR
ReadOnlyPaths=$ETC_DIR

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload

DB_FILE="$DATA_DIR/weltendzeit.sqlite3"

# Load runtime environment once. The DB/schema may already exist from an
# interrupted installation, so admin existence must be checked explicitly.
set -a
# shellcheck disable=SC1090
source "$ETC_DIR/app.env"
set +a

HAS_ADMIN=0
if runuser -u "$SERVICE_USER" -- env \
    HOST="$HOST" \
    PORT="$PORT" \
    DATA_DIR="$DATA_DIR" \
    MASTER_KEY_FILE="$MASTER_KEY_FILE" \
    SESSION_SECRET_FILE="$SESSION_SECRET_FILE" \
    COOKIE_SECURE="$COOKIE_SECURE" \
    ALLOW_REGISTRATION="$ALLOW_REGISTRATION" \
    SESSION_TTL_SECONDS="$SESSION_TTL_SECONDS" \
    PATH="$PATH" \
    "$NODE_DIR/bin/node" "$APP_CODE/scripts/has-admin.mjs"; then
  HAS_ADMIN=1
fi

if [[ "$HAS_ADMIN" -eq 0 ]]; then
  cyan "Kein Administrator-Konto vorhanden. Administrator wird erstellt."
  read -r -p "Admin-Benutzername [admin]: " ADMIN_USER </dev/tty || true
  ADMIN_USER="${ADMIN_USER:-admin}"

  while true; do
    read -r -s -p "Admin-Passwort (mind. 10 Zeichen): " ADMIN_PASS </dev/tty
    printf '
' >/dev/tty
    if [[ "${#ADMIN_PASS}" -ge 10 ]]; then break; fi
    yellow "Passwort ist zu kurz."
  done

  chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR"

  printf '%s
%s
' "$ADMIN_USER" "$ADMIN_PASS" | \
    runuser -u "$SERVICE_USER" -- env \
      HOST="$HOST" \
      PORT="$PORT" \
      DATA_DIR="$DATA_DIR" \
      MASTER_KEY_FILE="$MASTER_KEY_FILE" \
      SESSION_SECRET_FILE="$SESSION_SECRET_FILE" \
      COOKIE_SECURE="$COOKIE_SECURE" \
      ALLOW_REGISTRATION="$ALLOW_REGISTRATION" \
      SESSION_TTL_SECONDS="$SESSION_TTL_SECONDS" \
      PATH="$PATH" \
      "$NODE_DIR/bin/node" "$APP_CODE/scripts/create-admin.mjs"

  unset ADMIN_PASS
else
  green "Administrator-Konto bereits vorhanden."
fi

chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR"

# Nur ein echter Versionswechsel erzeugt die Discord-Update-Neustartmeldung.
if [[ "$EXISTING_INSTALL" -eq 1 && -n "$OLD_APP_VERSION" && "$OLD_APP_VERSION" != "$APP_VERSION" ]]; then
  UPDATE_RESTART_ID="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || date +%s%N)"
  UPDATE_CREATED_AT_MS="$(date +%s%3N)"
  printf '{"id":"%s","fromVersion":"%s","toVersion":"%s","createdAt":%s}\n' \
    "$UPDATE_RESTART_ID" "$OLD_APP_VERSION" "$APP_VERSION" "$UPDATE_CREATED_AT_MS" \
    > "$DATA_DIR/update-restart.json"
  chown "$SERVICE_USER:$SERVICE_USER" "$DATA_DIR/update-restart.json"
  chmod 600 "$DATA_DIR/update-restart.json"
  find "$DATA_DIR/update-restart-claims" -mindepth 1 -maxdepth 1 -type d -mtime +30 -exec rm -rf {} + 2>/dev/null || true
  green "Discord-Update-Neustartmeldung vorgemerkt: $OLD_APP_VERSION -> $APP_VERSION"
fi

systemctl enable --now "$SERVICE"

if [[ -s "$LICENSE_PUBLIC_KEY_FILE" ]] && \
   runuser -u "$SERVICE_USER" -- test -r "$LICENSE_PUBLIC_KEY_FILE"; then
  green "Lizenz-Public-Key ist für den BotPanel-Dienst lesbar."
else
  yellow "WARNUNG: Lizenz-Public-Key fehlt oder ist für $SERVICE_USER nicht lesbar."
fi
sleep 2

if ! systemctl is-active --quiet "$SERVICE"; then
  systemctl status "$SERVICE" --no-pager -l || true
  die "Dienst konnte nicht gestartet werden. Prüfe: journalctl -u $SERVICE -n 100 --no-pager"
fi

if ! curl -fsS --connect-timeout 5 "http://127.0.0.1:$PORT/api/config" | \
  python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("deploymentMode")=="selfhosted" and d.get("hostedMode") is False and d.get("discordOAuth") is False'; then
  systemctl status "$SERVICE" --no-pager -l || true
  die "Selfhosted-Modus-Selbsttest fehlgeschlagen. Discord Login/Gift-Bot müssen deaktiviert sein."
fi
green "Deployment-Selbsttest: SELFHOSTED · Discord Login AUS · Gift-Bot AUS."

echo
green "=============================================================="
green " $APP_NAME $APP_VERSION wurde erfolgreich installiert."
green "=============================================================="
echo
echo "Interner Dienst:"
echo "  http://$BIND_HOST:$PORT"
echo
echo "Der Dienst ist absichtlich NICHT an eine öffentliche Domain gebunden."
echo
echo "Nächster Schritt:"
echo "  Lege in deinem vorhandenen Web-/Hosting-Panel eine eigene Domain,"
echo "  Subdomain oder einen Pfad an und richte dort einen Reverse Proxy"
echo "  auf http://$BIND_HOST:$PORT ein."
echo
echo "Geeignet sind z. B. vorhandene Installationen mit:"
echo "  - KeyHelp"
echo "  - Plesk"
echo "  - cPanel"
echo "  - Apache"
echo "  - nginx"
echo "  - Caddy"
echo
yellow "Der Installer verändert KEINE dieser Konfigurationen automatisch."
yellow "Port $PORT NICHT öffentlich freigeben, solange ein Reverse Proxy verwendet wird."
echo
echo "Wichtige Befehle:"
echo "  systemctl status $SERVICE --no-pager -l"
echo "  systemctl restart $SERVICE"
echo "  journalctl -u $SERVICE -f"
echo
echo
yellow "Nur wenn du !yt/Prefix-Befehle aktivierst:"
echo "  Discord Developer Portal -> Bot -> Privileged Gateway Intents"
echo "  MESSAGE CONTENT INTENT = EIN"

echo "Reverse-Proxy-Beispiele stehen in der Installer-Dokumentation."
