#!/usr/bin/env bash
# =============================================================================
#  NebulaPlayer — one-command installer for a fresh Debian / Ubuntu VPS
#
#  Two files are enough. Upload both to the same folder on ANY web host (or to
#  the server itself), keeping these names:
#
#      install.sh
#      panel.zip          <- api/, panel/ and apk/ inside
#
#  Quick start (on the VPS, as root):
#
#      curl -fsSL https://files.nebulaplayer.net/install.sh -o install.sh
#      chmod 777 install.sh
#      ./install.sh
#
#  The script downloads panel.zip from the same folder the installer came from
#  (by default https://<download host>/panel.zip) and unpacks every part to its
#  destination:
#
#      panel/  -> /opt/nebulaplayer/panel
#      api/    -> /opt/nebulaplayer/api
#      apk/    -> /opt/nebulaplayer/apk
#
#  If the two files are not on a web host yet, put them in one folder on the
#  server and run ./install.sh there — the local copy is used first. Point at a
#  different host with --base-url https://example.com/some/folder/
#
#  Fully unattended:
#
#      ./install.sh --yes \
#        --domain nebulaplayer.com \
#        --api-domain api.nebulaplayer.com \
#        --get-domain files.nebulaplayer.net \
#        --email you@example.com
#
#  What it does:
#    * installs Node.js 20, PostgreSQL, Redis, Nginx, Certbot and UFW rules
#    * creates the `nebula` system user and /opt/nebulaplayer
#    * unpacks panel.zip: api/, panel/ and apk/ each into its place
#    * generates /opt/nebulaplayer/api/.env with random secrets
#    * creates the Postgres role + database and pushes the Prisma schema
#    * installs two systemd services (API on :3001, panel on :3000)
#    * configures Nginx vhosts for the panel, the API and the download host
#    * requests Let's Encrypt certificates when DNS already points here
#    * installs the `nebulaplayer` helper (status/logs/restart/update/admin…)
#
#  Re-running is safe and is how you update: services restart, .env secrets
#  and the database are kept.
# =============================================================================

set -Eeuo pipefail

# --------------------------------------------------------------------- config
APP_DIR="/opt/nebulaplayer"
SERVICE_USER="nebula"
PANEL_PORT=3000
API_PORT=3001
WEB_ROOT="/var/www/nebulaplayer-get"
LOG_DIR="/var/log/nebulaplayer"
LOG_FILE="${LOG_DIR}/install.log"
NODE_MAJOR=20
PRISMA_VERSION="5.22.0"

# Payload shipped next to install.sh. Both files live in the same folder on any
# web host, so one URL is enough for both:
#     https://get.nebulaplayer.net/install.sh
#     https://get.nebulaplayer.net/panel.zip
PAYLOAD_NAME="panel.zip"
LEGACY_NAME="nebulaplayer-deploy.zip"

DOMAIN="${DOMAIN:-}"
API_DOMAIN="${API_DOMAIN:-}"
GET_DOMAIN="${GET_DOMAIN:-}"
LE_EMAIL="${LE_EMAIL:-}"
ARCHIVE="${ARCHIVE:-}"
RELEASE_URL="${RELEASE_URL:-}"
BASE_URL="${NP_BASE_URL:-${BASE_URL:-}}"
ASSUME_YES=0
SKIP_TLS=0
SKIP_APT=0

SELF="${BASH_SOURCE[0]:-}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${SELF:-.}")" 2>/dev/null && pwd)" || SCRIPT_DIR="$PWD"
WORK=""
STAGE=""
BACKUP=""

# --------------------------------------------------------------------- pretty
if [[ -t 1 ]]; then
  C_OK=$'\033[32m'; C_ERR=$'\033[31m'; C_WARN=$'\033[33m'
  C_INFO=$'\033[36m'; C_DIM=$'\033[2m'; C_OFF=$'\033[0m'
else
  C_OK=''; C_ERR=''; C_WARN=''; C_INFO=''; C_DIM=''; C_OFF=''
fi

log()  { printf '%s•%s %s\n' "$C_INFO" "$C_OFF" "$*"; }
ok()   { printf '%s✓%s %s\n' "$C_OK" "$C_OFF" "$*"; }
warn() { printf '%s!%s %s\n' "$C_WARN" "$C_OFF" "$*" >&2; }
die()  { printf '%s✗ %s%s\n' "$C_ERR" "$*" "$C_OFF" >&2; exit 1; }
hr()   { printf '%s%s%s\n' "$C_DIM" "-------------------------------------------------------------------" "$C_OFF"; }

on_error() {
  local code=$?
  printf '\n%s✗ Installer stopped (exit %s) near line %s.%s\n' \
    "$C_ERR" "$code" "${1:-?}" "$C_OFF" >&2
  printf '  failed command: %s\n' "${BASH_COMMAND:-unknown}" >&2
  printf '  full log: %s\n' "$LOG_FILE" >&2
  printf '  need help? journalctl -u nebulaplayer-api -u nebulaplayer-panel -n 50\n' >&2
  exit "$code"
}
trap 'on_error $LINENO' ERR
cleanup() { [[ -n "$WORK$STAGE$BACKUP" ]] && rm -rf "$WORK" "$STAGE" "$BACKUP"; }
trap cleanup EXIT

usage() {
  cat <<'TXT'
NebulaPlayer installer

  ./install.sh [options]

Options
  -y, --yes                never prompt, use defaults / flags
      --domain <name>      public site (default: nebulaplayer.com)
      --api-domain <name>  API host    (default: api.<domain>)
      --get-domain <name>  download host (default: get.<domain>)
      --email <address>    Let's Encrypt account email (enables HTTPS)
      --base-url <url>     folder that holds install.sh + panel.zip
                           (default: https://<download host>/)
      --archive <zip|url>  use this panel.zip instead of downloading it
      --release-url <url>  alias of --archive <url>
      --skip-tls           configure HTTP only
      --skip-apt           skip apt-get (packages already installed)
  -h, --help               show this help

Files
  /opt/nebulaplayer                 api + panel + apk
  /opt/nebulaplayer/panel.zip       payload copy (used by `nebulaplayer update`)
  /opt/nebulaplayer/api/.env        generated secrets (keep this file!)
  /etc/nebulaplayer.conf            domains + email used by `nebulaplayer update`
  /var/www/nebulaplayer-get         public downloads (install.sh, panel.zip, apk)
TXT
}

# ----------------------------------------------------------------------- args
ORIG_ARGS=("$@")
while [[ $# -gt 0 ]]; do
  case "$1" in
    -y|--yes)            ASSUME_YES=1 ;;
    --domain)            DOMAIN="${2:?--domain needs a value}"; shift ;;
    --domain=*)          DOMAIN="${1#*=}" ;;
    --api-domain)        API_DOMAIN="${2:?--api-domain needs a value}"; shift ;;
    --api-domain=*)      API_DOMAIN="${1#*=}" ;;
    --get-domain)        GET_DOMAIN="${2:?--get-domain needs a value}"; shift ;;
    --get-domain=*)      GET_DOMAIN="${1#*=}" ;;
    --email)             LE_EMAIL="${2:?--email needs a value}"; shift ;;
    --email=*)           LE_EMAIL="${1#*=}" ;;
    --archive)           ARCHIVE="${2:?--archive needs a path or URL}"; shift ;;
    --archive=*)         ARCHIVE="${1#*=}" ;;
    --release-url)       RELEASE_URL="${2:?--release-url needs a value}"; shift ;;
    --release-url=*)     RELEASE_URL="${1#*=}" ;;
    --base-url)          BASE_URL="${2:?--base-url needs a value}"; shift ;;
    --base-url=*)        BASE_URL="${1#*=}" ;;
    --skip-tls)          SKIP_TLS=1 ;;
    --skip-apt)          SKIP_APT=1 ;;
    -h|--help)           usage; exit 0 ;;
    *)                   die "Unknown option: $1 (try --help)" ;;
  esac
  shift
done

# ----------------------------------------------------------------- root check
if [[ "${EUID}" -ne 0 ]]; then
  [[ -n "${SELF}" ]] || die "Run as root, or download the script first:
  curl -fsSL https://files.nebulaplayer.net/install.sh -o install.sh && chmod 700 install.sh && ./install.sh"
  if command -v sudo >/dev/null 2>&1; then
    log "Re-running with sudo…"
    exec sudo -E bash "${SELF}" "${ORIG_ARGS[@]}"
  fi
  die "This installer must run as root."
fi

mkdir -p "$LOG_DIR"
chmod 750 "$LOG_DIR"
# Everything below is also written to the log file.
exec > >(tee -a "$LOG_FILE") 2>&1

WORK="$(mktemp -d)"
STAGE="$(mktemp -d)"
BACKUP="$(mktemp -d)"

# ------------------------------------------------------------- sanity checks
[[ -f /etc/os-release ]] || die "Unsupported system (no /etc/os-release)."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}" in
  ubuntu|debian) ;;
  *) die "This installer supports Debian and Ubuntu only (found: ${ID:-unknown})." ;;
esac

case "$(uname -m)" in
  x86_64|aarch64|arm64) ;;
  *) die "Unsupported CPU architecture: $(uname -m)" ;;
esac

command -v systemctl >/dev/null 2>&1 \
  || die "systemd is required (this looks like a container without systemd)."

# Reuse the Let's Encrypt account email from a previous run, if any.
if [[ -z "$LE_EMAIL" && -d /etc/letsencrypt/renewal ]]; then
  LE_EMAIL="$(grep -rhs '^email = ' /etc/letsencrypt/renewal/ | head -n1 | cut -d= -f2- | tr -d ' ' || true)"
fi

# --------------------------------------------------------------- interactive
ask() { # ask <prompt> <default>
  local prompt="$1" default="$2" answer=""
  if [[ $ASSUME_YES -eq 1 ]] || [[ ! -r /dev/tty ]]; then
    printf '%s' "$default"
    return 0
  fi
  read -r -p "$prompt [$default]: " answer </dev/tty || true
  printf '%s' "${answer:-$default}"
}

valid_domain() { [[ "$1" =~ ^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$ ]]; }

if [[ $ASSUME_YES -eq 0 ]] && [[ -r /dev/tty ]]; then
  echo
  printf '%sNebulaPlayer installer%s\n' "$C_INFO" "$C_OFF"
  hr
  DOMAIN="$(ask 'Panel domain' "${DOMAIN:-nebulaplayer.com}")"
  API_DOMAIN="$(ask 'API domain' "${API_DOMAIN:-api.${DOMAIN}}")"
  GET_DOMAIN="$(ask 'Download host (install.sh, APK)' "${GET_DOMAIN:-get.${DOMAIN}}")"
  LE_EMAIL="$(ask "Let's Encrypt email (empty = no HTTPS yet)" "$LE_EMAIL")"
  echo
  printf '  panel:    https://%s\n' "$DOMAIN"
  printf '  api:      https://%s\n' "$API_DOMAIN"
  printf '  download: https://%s\n' "$GET_DOMAIN"
  printf '  https:    %s\n' "$([[ -n "$LE_EMAIL" && $SKIP_TLS -eq 0 ]] && echo "yes ($LE_EMAIL)" || echo 'no, HTTP only')"
  echo
  answer="$(ask 'Continue?' 'yes')"
  case "$answer" in
    y|Y|yes|YES) ;;
    *) die "Cancelled." ;;
  esac
fi

DOMAIN="${DOMAIN:-nebulaplayer.com}"
API_DOMAIN="${API_DOMAIN:-api.${DOMAIN}}"
GET_DOMAIN="${GET_DOMAIN:-get.${DOMAIN}}"

valid_domain "$DOMAIN"     || die "Invalid panel domain: $DOMAIN"
valid_domain "$API_DOMAIN" || die "Invalid API domain: $API_DOMAIN"
valid_domain "$GET_DOMAIN" || die "Invalid download domain: $GET_DOMAIN"
[[ "$API_DOMAIN" != "$DOMAIN" && "$GET_DOMAIN" != "$DOMAIN" ]] \
  || die "The panel, API and download host must be three different names."

SCHEME="https"
[[ -n "$LE_EMAIL" && $SKIP_TLS -eq 0 ]] || SCHEME="http"

echo
hr
printf '  Installing NebulaPlayer on %s\n' "$(hostname -f 2>/dev/null || hostname)"
printf '  Panel    %s://%s\n' "$SCHEME" "$DOMAIN"
printf '  API      %s://%s\n' "$SCHEME" "$API_DOMAIN"
printf '  Download %s://%s\n' "$SCHEME" "$GET_DOMAIN"
hr
echo

# ----------------------------------------------------------------- 1. system
if [[ $SKIP_APT -eq 0 ]]; then
  log "Installing system packages (this takes a few minutes)…"
  export DEBIAN_FRONTEND=noninteractive
  apt-get update -y -qq
  apt-get install -y -qq --no-install-recommends \
    ca-certificates curl gnupg unzip git openssl \
    build-essential python3 \
    postgresql postgresql-contrib redis-server \
    nginx certbot python3-certbot-nginx ufw
  ok "System packages ready"
else
  log "Skipping apt-get (--skip-apt)"
fi

# A 1 GB VPS runs out of memory during npm install; add swap if needed.
if command -v swapon >/dev/null 2>&1 && [[ ! -f /swapfile ]] \
   && ! swapon --show 2>/dev/null | grep -q .; then
  MEM_KB="$(awk '/MemTotal/{print $2}' /proc/meminfo)"
  if [[ "${MEM_KB:-0}" -lt 2000000 ]]; then
    log "Low RAM detected — adding a 2 GB swap file"
    dd if=/dev/zero of=/swapfile bs=1M count=2048 status=none
    chmod 600 /swapfile
    mkswap /swapfile >/dev/null
    swapon /swapfile
    grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
    ok "Swap enabled"
  fi
fi

# ------------------------------------------------------------------- 2. node
node_major() { node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
if ! command -v node >/dev/null 2>&1 || [[ "$(node_major)" -lt 20 ]]; then
  log "Installing Node.js ${NODE_MAJOR}.x…"
  curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/dev/null
  apt-get install -y -qq nodejs
  ok "Node.js $(node -v) installed"
else
  ok "Node.js $(node -v) already present"
fi

NODE_BIN="$(command -v node)"

# ------------------------------------------------------------------- 3. user
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
  useradd --system --create-home --home-dir "/home/${SERVICE_USER}" \
    --shell /usr/sbin/nologin "$SERVICE_USER"
  ok "Created system user ${SERVICE_USER}"
fi

# ---------------------------------------------------------------- 4. release
download() { # download <url> <dest>
  local url="$1" dest="$2"
  case "$url" in
    https://*|http://*)
      if curl -fsSL --retry 3 --connect-timeout 15 -o "$dest" "$url"; then return 0; fi
      return 1 ;;
    *) return 1 ;;
  esac
}

first_existing() { # first_existing <filename> -> prints the path
  local dir
  for dir in "$SCRIPT_DIR" "$PWD" "/root"; do
    if [[ -f "$dir/$1" ]]; then printf '%s' "$dir/$1"; return 0; fi
  done
  return 1
}

fetch_named() { # fetch_named <filename> -> prints a local path, or fails
  local name="$1" found url base
  local bases=()

  if found="$(first_existing "$name")"; then
    printf '%s' "$found"; return 0
  fi

  # Both files are published in the same folder on the download host, so the
  # same base URL that serves install.sh also serves panel.zip.
  [[ -n "$BASE_URL" ]] && bases+=("${BASE_URL%/}")
  bases+=("https://${GET_DOMAIN}" "http://${GET_DOMAIN}")

  for base in "${bases[@]}"; do
    url="${base}/${name}"
    log "Trying ${url}…" >&2
    if download "$url" "${WORK}/${name}"; then printf '%s' "${WORK}/${name}"; return 0; fi
  done

  return 1
}

fetch_payload() {
  if [[ -n "$ARCHIVE" ]]; then
    if [[ "$ARCHIVE" =~ ^https?:// ]]; then
      log "Downloading ${ARCHIVE}…" >&2
      download "$ARCHIVE" "${WORK}/${PAYLOAD_NAME}" || die "Could not download $ARCHIVE"
      printf '%s' "${WORK}/${PAYLOAD_NAME}"; return 0
    fi
    [[ -f "$ARCHIVE" ]] || die "Archive not found: $ARCHIVE"
    printf '%s' "$ARCHIVE"; return 0
  fi

  if [[ -n "$RELEASE_URL" ]]; then
    log "Downloading ${RELEASE_URL}…" >&2
    download "$RELEASE_URL" "${WORK}/${PAYLOAD_NAME}" || die "Could not download $RELEASE_URL"
    printf '%s' "${WORK}/${PAYLOAD_NAME}"; return 0
  fi

  # 1. the folder this script was run from (most reliable)
  if fetch_named "$PAYLOAD_NAME"; then return 0; fi

  # 2. legacy bundle name, so old uploads keep working
  if fetch_named "$LEGACY_NAME"; then
    warn "Using the legacy archive name ${LEGACY_NAME}" >&2
    return 0
  fi

  return 1
}

log "Locating ${PAYLOAD_NAME}…"
PAYLOAD_ZIP="$(fetch_payload)" || die \
  "Could not find ${PAYLOAD_NAME}.

  Put install.sh and ${PAYLOAD_NAME} in the same folder:
    * on the server  -> upload both to /root and run ./install.sh
    * on a web host  -> upload both to one folder, then run
                        ./install.sh --base-url https://your-host/that/folder/

  Or point straight at the files:
    ./install.sh --archive /root/${PAYLOAD_NAME}
    ./install.sh --release-url https://your-host/${PAYLOAD_NAME}"

[[ -s "$PAYLOAD_ZIP" ]] || die "${PAYLOAD_NAME} is empty: $PAYLOAD_ZIP"
unzip -tqq "$PAYLOAD_ZIP" >/dev/null 2>&1 || die "${PAYLOAD_NAME} is not a valid zip: $PAYLOAD_ZIP"
ok "$(basename "$PAYLOAD_ZIP") ($(du -h "$PAYLOAD_ZIP" | cut -f1)) found"

# ------------------------------------------------------------- 5. unpack files
# The archive may hold api/, panel/ and apk/ (bundle), or a single app tree.
# Each piece is copied to its own destination.
place_payload() { # place_payload <extracted-dir>
  local stage="$1"
  local api_src="" panel_src="" apk_src="" nested=""

  # Some zip tools wrap everything in one top-level folder; step into it.
  if [[ ! -e "$stage/api" && ! -e "$stage/panel" && ! -e "$stage/apk" \
        && ! -e "$stage/.next" && ! -e "$stage/package.json" \
        && ! -e "$stage/dist" && ! -e "$stage/prisma" ]]; then
    nested="$(find "$stage" -mindepth 1 -maxdepth 1 -type d | head -n2)"
    if [[ -n "$nested" && "$(printf '%s\n' "$nested" | wc -l)" -eq 1 ]]; then
      log "Using the ${nested##*/}/ folder inside the archive"
      stage="$nested"
    fi
  fi

  if [[ -d "$stage/api" || -d "$stage/panel" || -d "$stage/apk" ]]; then
    [[ -d "$stage/api" ]]   && api_src="$stage/api"
    [[ -d "$stage/panel" ]] && panel_src="$stage/panel"
    [[ -d "$stage/apk" ]]   && apk_src="$stage/apk"
  elif [[ -d "$stage/.next" || -f "$stage/package.json" ]]; then
    panel_src="$stage"
  elif [[ -d "$stage/dist" || -d "$stage/prisma" ]]; then
    api_src="$stage"
  fi

  if [[ -z "$api_src$panel_src$apk_src" ]]; then
    die "The archive has no api/, panel/ or apk/ folder. Re-download ${PAYLOAD_NAME}."
  fi

  if [[ -n "$panel_src" ]]; then
    mkdir -p "${APP_DIR}/panel"
    rm -rf "${APP_DIR}/panel/.next" \
           "${APP_DIR}/panel/next.config.js" \
           "${APP_DIR}/panel/next.config.mjs"
    cp -a "${panel_src}/." "${APP_DIR}/panel/"
    ok "panel  -> ${APP_DIR}/panel  ($(find "$panel_src" -type f | wc -l | tr -d ' ') files)"
  fi

  if [[ -n "$api_src" ]]; then
    mkdir -p "${APP_DIR}/api"
    rm -rf "${APP_DIR}/api/dist"
    cp -a "${api_src}/." "${APP_DIR}/api/"
    ok "api    -> ${APP_DIR}/api  ($(find "$api_src" -type f | wc -l | tr -d ' ') files)"
  fi

  if [[ -n "$apk_src" ]]; then
    mkdir -p "${APP_DIR}/apk"
    cp -a "${apk_src}/." "${APP_DIR}/apk/"
    ok "apk    -> ${APP_DIR}/apk  ($(find "$apk_src" -type f | wc -l | tr -d ' ') files)"
  fi
}

log "Unpacking $(basename "$PAYLOAD_ZIP")…"
unzip -q "$PAYLOAD_ZIP" -d "$STAGE"

[[ -f "${APP_DIR}/api/.env" ]] && cp -a "${APP_DIR}/api/.env" "${BACKUP}/api.env"
[[ -f "${APP_DIR}/panel/.env" ]] && cp -a "${APP_DIR}/panel/.env" "${BACKUP}/panel.env"

mkdir -p "$APP_DIR"
place_payload "$STAGE"

# If panel.zip only carried the web panel, allow a separate api.zip next to it.
if [[ ! -f "${APP_DIR}/api/dist/src/main.js" ]]; then
  log "No api/ in ${PAYLOAD_NAME} - looking for api.zip…"
  if API_ZIP="$(fetch_named "api.zip")"; then
    unzip -q "$API_ZIP" -d "${STAGE}/api-only"
    place_payload "${STAGE}/api-only"
  fi
fi

[[ -f "${APP_DIR}/api/dist/src/main.js" ]] || die \
  "API files are missing (expected api/dist/src/main.js).
  Include an api/ folder in ${PAYLOAD_NAME}, or upload api.zip next to it."
[[ -d "${APP_DIR}/panel/.next" ]] || die \
  "Panel build is missing (expected panel/.next).
  Include a panel/ folder in ${PAYLOAD_NAME}, or upload a panel-only zip."

[[ -f "${BACKUP}/api.env" ]]   && cp -a "${BACKUP}/api.env"   "${APP_DIR}/api/.env"
[[ -f "${BACKUP}/panel.env" ]] && cp -a "${BACKUP}/panel.env" "${APP_DIR}/panel/.env"

if [[ -f "${SELF}" && -f "${SCRIPT_DIR}/install.sh" ]]; then
  install -m 0755 "${SCRIPT_DIR}/install.sh" "${APP_DIR}/install.sh"
fi

# keep the archive beside the installed script so a reinstall works offline
install -m 0644 "$PAYLOAD_ZIP" "${APP_DIR}/${PAYLOAD_NAME}" 2>/dev/null || true

ok "Application files in place"

# ------------------------------------------------------------- 6. secrets/env
ENV_FILE="${APP_DIR}/api/.env"
rand_hex() { openssl rand -hex "$1"; }

read_env() { # read_env <KEY>
  [[ -f "$ENV_FILE" ]] || return 0
  sed -n "s|^$1=||p" "$ENV_FILE" | head -n1 | tr -d '"'
}

DB_PASS=""; JWT_SECRET=""; JWT_REFRESH_SECRET=""; ENCRYPTION_MASTER_KEY=""
if [[ -f "$ENV_FILE" ]]; then
  DB_PASS="$(sed -n 's|^DATABASE_URL="\?postgresql://[^:]*:\([^@]*\)@.*|\1|p' "$ENV_FILE" | head -n1)"
  JWT_SECRET="$(read_env JWT_SECRET)"
  JWT_REFRESH_SECRET="$(read_env JWT_REFRESH_SECRET)"
  ENCRYPTION_MASTER_KEY="$(read_env ENCRYPTION_MASTER_KEY)"
fi

[[ -n "$DB_PASS" ]]               || DB_PASS="$(rand_hex 18)"
[[ -n "$JWT_SECRET" ]]            || JWT_SECRET="$(rand_hex 32)"
[[ -n "$JWT_REFRESH_SECRET" ]]    || JWT_REFRESH_SECRET="$(rand_hex 32)"
[[ -n "$ENCRYPTION_MASTER_KEY" ]] || ENCRYPTION_MASTER_KEY="$(rand_hex 32)"

log "Writing ${ENV_FILE}…"
umask 077
cat > "$ENV_FILE" <<ENVEOF
# Generated by the NebulaPlayer installer. Keep this file secret and backed up:
# ENCRYPTION_MASTER_KEY decrypts stored Xtream passwords, and changing it makes
# them unreadable.
DATABASE_URL="postgresql://nebulaplayer:${DB_PASS}@127.0.0.1:5432/nebulaplayer?schema=public"
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
JWT_SECRET=${JWT_SECRET}
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
JWT_REFRESH_EXPIRES_IN=30d
ENCRYPTION_MASTER_KEY=${ENCRYPTION_MASTER_KEY}
PORT=${API_PORT}
HOST=127.0.0.1
NODE_ENV=production
FRONTEND_URL=${SCHEME}://${DOMAIN}
RATE_LIMIT_TTL=60
RATE_LIMIT_MAX=100
DEVICE_KEY_RATE_LIMIT_MAX=5
DEVICE_KEY_RATE_LIMIT_TTL=300
CORS_ORIGIN=${SCHEME}://${DOMAIN}
COOKIE_SECURE=$([[ "$SCHEME" == "https" ]] && echo true || echo false)
COOKIE_SAME_SITE=$([[ "$SCHEME" == "https" ]] && echo none || echo lax)
ENVEOF
chmod 600 "$ENV_FILE"
umask 022
ok "Secrets written (kept across re-runs)"

# --------------------------------------------------------------- 7. database
log "Configuring PostgreSQL…"
systemctl enable --now postgresql >/dev/null 2>&1 || die "PostgreSQL failed to start"

if ! runuser -u postgres -- psql -tAc \
      "SELECT 1 FROM pg_roles WHERE rolname='nebulaplayer'" | grep -q 1; then
  runuser -u postgres -- psql -c \
    "CREATE ROLE nebulaplayer LOGIN PASSWORD '${DB_PASS}'" >/dev/null
  ok "Created database role nebulaplayer"
fi
runuser -u postgres -- psql -c \
  "ALTER ROLE nebulaplayer WITH LOGIN PASSWORD '${DB_PASS}'" >/dev/null

if ! runuser -u postgres -- psql -tAc \
      "SELECT 1 FROM pg_database WHERE datname='nebulaplayer'" | grep -q 1; then
  runuser -u postgres -- createdb -O nebulaplayer nebulaplayer
  ok "Created database nebulaplayer"
fi

log "Configuring Redis…"
systemctl enable --now redis-server >/dev/null 2>&1 || die "Redis failed to start"
redis-cli ping 2>/dev/null | grep -q PONG || die "Redis is not answering on 127.0.0.1"
ok "PostgreSQL and Redis are running"

# ------------------------------------------------------------- 8. npm install
npm_install() { # npm_install <dir> [args...]
  local dir="$1"; shift
  local attempt
  ( cd "$dir"
    for attempt in 1 2 3; do
      if npm install --no-audit --no-fund --loglevel=error "$@"; then exit 0; fi
      warn "npm install failed (attempt ${attempt}/3), retrying in 5s…"
      sleep 5
    done
    warn "Retrying with --legacy-peer-deps (the bundle has a peer dependency conflict)."
    npm install --no-audit --no-fund --loglevel=error --legacy-peer-deps "$@"
  )
}

log "Installing API dependencies (this can take a while)…"
npm_install "${APP_DIR}/api" --omit=dev
npm_install "${APP_DIR}/api" --no-save "prisma@${PRISMA_VERSION}"
ok "API dependencies installed"

log "Installing panel dependencies…"
npm_install "${APP_DIR}/panel" --omit=dev
ok "Panel dependencies installed"

# --------------------------------------------------------------- 9. prisma db
log "Creating database schema…"
(
  cd "${APP_DIR}/api"
  set -a
  # shellcheck disable=SC1091
  . ./.env
  set +a
  ./node_modules/.bin/prisma generate --schema=prisma/schema.prisma >/dev/null
  ./node_modules/.bin/prisma db push --schema=prisma/schema.prisma --skip-generate
)
ok "Schema is up to date"

# ----------------------------------------------------------------- 10. systemd
log "Installing systemd services…"
cat > /etc/systemd/system/nebulaplayer-api.service <<UNIT
[Unit]
Description=NebulaPlayer API (NestJS)
After=network-online.target postgresql.service redis-server.service
Wants=network-online.target

[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_USER}
WorkingDirectory=${APP_DIR}/api
EnvironmentFile=${APP_DIR}/api/.env
ExecStart=${NODE_BIN} dist/src/main.js
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
UNIT

cat > /etc/systemd/system/nebulaplayer-panel.service <<UNIT
[Unit]
Description=NebulaPlayer web panel (Next.js)
After=network-online.target nebulaplayer-api.service
Wants=network-online.target

[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_USER}
WorkingDirectory=${APP_DIR}/panel
Environment=NODE_ENV=production
Environment=NEXT_TELEMETRY_DISABLED=1
ExecStart=${NODE_BIN} node_modules/next/dist/bin/next start -H 127.0.0.1 -p ${PANEL_PORT}
Restart=always
RestartSec=5
TimeoutStopSec=20
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
UNIT

chown -R "${SERVICE_USER}:${SERVICE_USER}" "$APP_DIR"
chmod 600 "$ENV_FILE"

systemctl daemon-reload
systemctl enable nebulaplayer-api nebulaplayer-panel >/dev/null 2>&1
systemctl restart nebulaplayer-api
systemctl restart nebulaplayer-panel
ok "Services enabled and started"

# --------------------------------------------------------------- 11. downloads
log "Publishing downloads to ${WEB_ROOT}…"
mkdir -p "$WEB_ROOT"
install -m 0755 "${APP_DIR}/install.sh" "${WEB_ROOT}/install.sh" 2>/dev/null || true
for f in "${APP_DIR}"/apk/*; do
  [[ -f "$f" ]] && install -m 0644 "$f" "${WEB_ROOT}/$(basename "$f")"
done
install -m 0644 "$PAYLOAD_ZIP" "${WEB_ROOT}/${PAYLOAD_NAME}"

cat > "${WEB_ROOT}/index.html" <<'HTMLEOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>NebulaPlayer downloads</title>
<style>
  :root { color-scheme: dark; }
  body { margin:0; min-height:100vh; display:grid; place-items:center;
         background:#05050a; color:#e8e8f0;
         font-family:ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif; }
  main { width:min(560px,92vw); }
  h1 { font-size:1.4rem; letter-spacing:.18em; text-transform:uppercase; }
  p  { color:#94a3b8; font-size:.9rem; line-height:1.6; }
  a  { display:block; padding:.9rem 1rem; margin:.55rem 0; text-decoration:none;
       color:#e8e8f0; border:1px solid rgba(255,255,255,.12); border-radius:12px;
       background:rgba(255,255,255,.03); }
  a:hover { border-color:rgba(0,217,255,.45); color:#fff; }
  code { color:#67e8f9; font-size:.78rem; word-break:break-all; }
</style>
</head>
<body>
<main>
  <h1>NebulaPlayer</h1>
  <p>Downloads and server bootstrap. No channels or content are distributed here.</p>
  <a href="/nebulaplayer.apk">Android app (APK)</a>
  <a href="/SHA256.txt">APK checksum</a>
  <a href="/install.sh">install.sh</a>
  <a href="/panel.zip">panel.zip (server files)</a>
  <p>Install on a fresh Ubuntu/Debian VPS:</p>
  <code>curl -fsSL https://files.nebulaplayer.net/install.sh -o install.sh &amp;&amp; chmod 700 install.sh &amp;&amp; ./install.sh</code>
</main>
</body>
</html>
HTMLEOF

chown -R www-data:www-data "$WEB_ROOT"
ok "Downloads published"

# ------------------------------------------------------------------ 12. nginx
log "Configuring Nginx…"

write_vhost() { # write_vhost <file> <<'NGINX'
  local target="$1"
  sed -e "s|@@DOMAIN@@|${DOMAIN}|g" \
      -e "s|@@API_DOMAIN@@|${API_DOMAIN}|g" \
      -e "s|@@GET_DOMAIN@@|${GET_DOMAIN}|g" \
      -e "s|@@PANEL_PORT@@|${PANEL_PORT}|g" \
      -e "s|@@API_PORT@@|${API_PORT}|g" \
      -e "s|@@WEB_ROOT@@|${WEB_ROOT}|g" > "$target"
}

write_vhost /etc/nginx/sites-available/nebulaplayer-panel <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name @@DOMAIN@@;

    client_max_body_size 16m;

    # Same-origin API (optional shortcut for the panel)
    location /api/ {
        proxy_pass http://127.0.0.1:@@API_PORT@@/api/;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }

    # APK + release files on the main domain
    location /download/ {
        alias @@WEB_ROOT@@/;
        autoindex off;
        types { application/vnd.android.package-archive apk; }
        default_type application/octet-stream;
        add_header Cache-Control "public, max-age=86400";
    }

    # Let's Encrypt HTTP-01 challenges
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        proxy_pass http://127.0.0.1:@@PANEL_PORT@@;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name www.@@DOMAIN@@;
    return 301 http://@@DOMAIN@@$request_uri;
}
NGINX

write_vhost /etc/nginx/sites-available/nebulaplayer-api <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name @@API_DOMAIN@@;

    client_max_body_size 4m;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        proxy_pass http://127.0.0.1:@@API_PORT@@;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}
NGINX

write_vhost /etc/nginx/sites-available/nebulaplayer-get <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name @@GET_DOMAIN@@;

    root @@WEB_ROOT@@;
    index index.html;

    client_max_body_size 64m;

    location = /install.sh {
        default_type text/x-shellscript;
        add_header Cache-Control "no-cache";
        add_header Content-Disposition "inline";
    }

    location ~* \.apk$ {
        default_type application/vnd.android.package-archive;
        add_header Cache-Control "public, max-age=86400";
    }

    location / {
        try_files $uri $uri/ =404;
    }

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }
}
NGINX

ln -sfn /etc/nginx/sites-available/nebulaplayer-panel /etc/nginx/sites-enabled/nebulaplayer-panel
ln -sfn /etc/nginx/sites-available/nebulaplayer-api   /etc/nginx/sites-enabled/nebulaplayer-api
ln -sfn /etc/nginx/sites-available/nebulaplayer-get   /etc/nginx/sites-enabled/nebulaplayer-get
rm -f /etc/nginx/sites-enabled/default

nginx -t >/dev/null 2>&1 || { nginx -t; die "Nginx configuration is invalid"; }
systemctl enable nginx >/dev/null 2>&1 || true
systemctl reload nginx
ok "Nginx vhosts active"

# -------------------------------------------------------------------- 13. tls
resolve_ip() { getent ahostsv4 "$1" 2>/dev/null | awk 'NR==1{print $1}'; }
public_ip() {
  curl -fsS --max-time 8 https://api.ipify.org 2>/dev/null \
    || curl -fsS --max-time 8 https://ifconfig.me 2>/dev/null \
    || true
}

TLS_ACTIVE=0
if [[ $SKIP_TLS -eq 1 ]]; then
  warn "HTTPS skipped (--skip-tls). Cookies/panel work over HTTP only."
elif [[ -z "$LE_EMAIL" ]]; then
  warn "No email given — HTTPS not configured. Run: nebulaplayer tls you@example.com"
else
  SERVER_IP="$(public_ip)"
  DNS_OK=1
  for d in "$DOMAIN" "$API_DOMAIN" "$GET_DOMAIN"; do
    ip="$(resolve_ip "$d")"
    if [[ -z "$ip" ]]; then
      warn "${d} does not resolve yet"
      DNS_OK=0
    elif [[ -n "$SERVER_IP" && "$ip" != "$SERVER_IP" ]]; then
      warn "${d} points to ${ip}, this server is ${SERVER_IP}"
      DNS_OK=0
    fi
  done

  if [[ $DNS_OK -eq 1 ]]; then
    log "Requesting Let's Encrypt certificates…"
    if certbot --nginx --non-interactive --agree-tos -m "$LE_EMAIL" --redirect \
         -d "$DOMAIN" -d "$API_DOMAIN" -d "$GET_DOMAIN" >/dev/null 2>&1; then
      TLS_ACTIVE=1
      ok "HTTPS enabled for ${DOMAIN}, ${API_DOMAIN}, ${GET_DOMAIN}"
    else
      warn "certbot failed. Re-run later with: nebulaplayer tls ${LE_EMAIL}"
    fi
  else
    warn "DNS is not pointing here yet — HTTPS deferred."
    warn "Fix the A records, then run: nebulaplayer tls ${LE_EMAIL}"
    [[ -n "$SERVER_IP" ]] && warn "This server's public IP is ${SERVER_IP}"
  fi
fi

# The scheme actually in use decides cookie and CORS settings, so reconcile the
# env file with reality (certbot may have succeeded or been deferred).
LIVE_SCHEME="http"
[[ $TLS_ACTIVE -eq 1 ]] && LIVE_SCHEME="https"
if [[ "$LIVE_SCHEME" == "https" ]]; then
  COOKIE_SECURE=true; COOKIE_SAME_SITE=none
else
  COOKIE_SECURE=false; COOKIE_SAME_SITE=lax
fi
sed -i "s|^FRONTEND_URL=.*|FRONTEND_URL=${LIVE_SCHEME}://${DOMAIN}|" "$ENV_FILE"
sed -i "s|^CORS_ORIGIN=.*|CORS_ORIGIN=${LIVE_SCHEME}://${DOMAIN}|"   "$ENV_FILE"
sed -i "s|^COOKIE_SECURE=.*|COOKIE_SECURE=${COOKIE_SECURE}|"         "$ENV_FILE"
sed -i "s|^COOKIE_SAME_SITE=.*|COOKIE_SAME_SITE=${COOKIE_SAME_SITE}|" "$ENV_FILE"
systemctl restart nebulaplayer-api
SCHEME="$LIVE_SCHEME"

# ---------------------------------------------------------------- 14. firewall
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
  ufw allow OpenSSH >/dev/null 2>&1 || true
  ufw allow 'Nginx Full' >/dev/null 2>&1 || true
  ok "UFW allows SSH, HTTP and HTTPS"
fi

# ------------------------------------------------------------ 15. health checks
wait_http() { # wait_http <url> [tries]
  local url="$1" tries="${2:-30}" i code
  for ((i = 1; i <= tries; i++)); do
    code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "$url" 2>/dev/null || true)"
    if [[ -n "$code" && "$code" != "000" ]]; then printf '%s' "$code"; return 0; fi
    sleep 2
  done
  return 1
}

log "Waiting for services…"
API_CODE="$(wait_http "http://127.0.0.1:${API_PORT}/api/docs" 40 || true)"
PANEL_CODE="$(wait_http "http://127.0.0.1:${PANEL_PORT}/" 40 || true)"

if [[ "$API_CODE" == "200" ]]; then ok "API answers on 127.0.0.1:${API_PORT}"
else
  warn "API did not answer as expected (HTTP ${API_CODE:-none}) — last log lines:"
  journalctl -u nebulaplayer-api -n 15 --no-pager 2>/dev/null || true
fi

if [[ "$PANEL_CODE" == "200" ]]; then ok "Panel answers on 127.0.0.1:${PANEL_PORT}"
else
  warn "Panel did not answer as expected (HTTP ${PANEL_CODE:-none}) — last log lines:"
  journalctl -u nebulaplayer-panel -n 15 --no-pager 2>/dev/null || true
fi

# -------------------------------------------------------------- 16. helper CLI
log "Installing the nebulaplayer helper…"
cat > /etc/nebulaplayer.conf <<CONF
DOMAIN=${DOMAIN}
API_DOMAIN=${API_DOMAIN}
GET_DOMAIN=${GET_DOMAIN}
LE_EMAIL=${LE_EMAIL}
APP_DIR=${APP_DIR}
CONF
chmod 644 /etc/nebulaplayer.conf

cat > /usr/local/bin/nebulaplayer <<'CLI'
#!/usr/bin/env bash
# NebulaPlayer server helper — installed by install.sh
set -euo pipefail
CONF=/etc/nebulaplayer.conf
[[ -r $CONF ]] && . "$CONF"
APP_DIR=${APP_DIR:-/opt/nebulaplayer}
DOMAIN=${DOMAIN:-nebulaplayer.com}
API_DOMAIN=${API_DOMAIN:-api.nebulaplayer.com}
GET_DOMAIN=${GET_DOMAIN:-files.nebulaplayer.net}
LE_EMAIL=${LE_EMAIL:-}

usage() {
  cat <<'TXT'
nebulaplayer <command>

  status              service state and ports
  restart             restart API + panel
  logs [service]      follow logs (api | panel, default: both)
  update              re-run the installer from /opt/nebulaplayer/install.sh
  tls [email]         request/renew Let's Encrypt certificates
  admin <email>       promote a registered panel account to ADMIN
  env                 show the API .env (secrets included)
  db                  open psql on the nebulaplayer database
  domain              print the configured URLs
TXT
}

case "${1:-help}" in
  status)
    systemctl --no-pager --full status nebulaplayer-api nebulaplayer-panel | sed -n '1,40p' || true
    echo
    ss -ltnp 2>/dev/null | grep -E ':(3000|3001)\b' || true
    ;;
  restart)
    systemctl restart nebulaplayer-api nebulaplayer-panel
    echo "restarted"
    ;;
  logs)
    case "${2:-both}" in
      api)    journalctl -u nebulaplayer-api -n 200 -f ;;
      panel)  journalctl -u nebulaplayer-panel -n 200 -f ;;
      *)      journalctl -u nebulaplayer-api -u nebulaplayer-panel -n 200 -f ;;
    esac
    ;;
  update)
    # Pull a fresh install.sh + panel.zip from the download host, so updating is
    # just `nebulaplayer update` after you upload new files there.
    tmp="$(mktemp -d)"
    rc=0
    got_installer=0
    got_payload=0
    for scheme in https http; do
      if [[ $got_installer -eq 0 ]] && curl -fsSL --retry 2 --connect-timeout 10 \
           "${scheme}://${GET_DOMAIN}/install.sh" -o "$tmp/install.sh" 2>/dev/null; then
        got_installer=1
      fi
      if [[ $got_payload -eq 0 ]] && curl -fsSL --retry 2 --connect-timeout 10 \
           "${scheme}://${GET_DOMAIN}/panel.zip" -o "$tmp/panel.zip" 2>/dev/null; then
        got_payload=1
      fi
      [[ $got_installer -eq 1 && $got_payload -eq 1 ]] && break
    done

    if [[ $got_installer -eq 0 ]]; then
      echo "could not download install.sh from ${GET_DOMAIN}; using the local copy" >&2
      if [[ -f ${APP_DIR}/install.sh ]]; then
        cp -a "${APP_DIR}/install.sh" "$tmp/install.sh"; got_installer=1
      fi
    fi
    [[ $got_installer -eq 1 ]] || { rm -rf "$tmp"; echo "no installer available" >&2; exit 1; }

    payload_args=()
    if [[ $got_payload -eq 1 ]]; then
      payload_args=(--archive "$tmp/panel.zip")
    elif [[ -f ${APP_DIR}/panel.zip ]]; then
      echo "could not download panel.zip; using the local copy" >&2
      payload_args=(--archive "${APP_DIR}/panel.zip")
    else
      rm -rf "$tmp"
      echo "could not download panel.zip and no local copy exists" >&2
      exit 1
    fi

    extra=()
    [[ -n $LE_EMAIL ]] && extra=(--email "$LE_EMAIL")

    bash "$tmp/install.sh" --yes \
      --domain "$DOMAIN" --api-domain "$API_DOMAIN" --get-domain "$GET_DOMAIN" \
      "${payload_args[@]}" "${extra[@]}" || rc=$?
    rm -rf "$tmp"
    exit "$rc"
    ;;
  tls)
    email="${2:-$LE_EMAIL}"
    [[ -n $email ]] || { echo "usage: nebulaplayer tls you@example.com" >&2; exit 1; }
    certbot --nginx --non-interactive --agree-tos -m "$email" --redirect \
      -d "$DOMAIN" -d "$API_DOMAIN" -d "$GET_DOMAIN"
    sed -i 's|^COOKIE_SECURE=.*|COOKIE_SECURE=true|'  "${APP_DIR}/api/.env"
    sed -i 's|^COOKIE_SAME_SITE=.*|COOKIE_SAME_SITE=none|' "${APP_DIR}/api/.env"
    systemctl restart nebulaplayer-api
    echo "HTTPS enabled"
    ;;
  admin)
    email="${2:-}"
    [[ -n $email ]] || { echo "usage: nebulaplayer admin you@example.com" >&2; exit 1; }
    runuser -u postgres -- psql -d nebulaplayer -v email="$email" -c \
      "UPDATE \"User\" SET role='ADMIN', \"emailVerified\"=true WHERE email = :'email';"
    ;;
  env)
    cat "${APP_DIR}/api/.env"
    ;;
  db)
    exec runuser -u postgres -- psql -d nebulaplayer
    ;;
  domain)
    echo "panel    https://${DOMAIN}"
    echo "api      https://${API_DOMAIN}/api/docs"
    echo "download https://${GET_DOMAIN}"
    ;;
  help|-h|--help)
    usage
    ;;
  *)
    usage
    exit 1
    ;;
esac
CLI

chmod 755 /usr/local/bin/nebulaplayer
ok "Helper installed: nebulaplayer status|logs|restart|update|tls|admin"

# ------------------------------------------------------------------ 17. summary
echo
hr
printf '%sNebulaPlayer is installed%s\n' "$C_OK" "$C_OFF"
hr
if [[ $TLS_ACTIVE -eq 1 ]]; then
  printf '  Panel       https://%s\n' "$DOMAIN"
  printf '  Activate    https://%s/activate\n' "$DOMAIN"
  printf '  API docs    https://%s/api/docs\n' "$API_DOMAIN"
  printf '  APK         https://%s/download/nebulaplayer.apk\n' "$DOMAIN"
  printf '  Installer   https://%s/install.sh\n' "$GET_DOMAIN"
else
  printf '  Panel       http://%s\n' "$DOMAIN"
  printf '  Activate    http://%s/activate\n' "$DOMAIN"
  printf '  API docs    http://%s/api/docs\n' "$API_DOMAIN"
  printf '  APK         http://%s/download/nebulaplayer.apk\n' "$DOMAIN"
  printf '  Installer   http://%s/install.sh\n' "$GET_DOMAIN"
  printf '\n  HTTPS is not active yet. Point the DNS A records here, then:\n'
  printf '     nebulaplayer tls you@example.com\n'
fi
cat <<TXT

  Next steps
    1. Open /register on the panel and create your own account.
    2. Promote it to admin:
         nebulaplayer admin you@example.com
    3. Sign in, open /admin, and use Orders -> "Mark paid" to activate
       a customer device after you confirm the payment.

  Day-to-day
    nebulaplayer status      service state and listening ports
    nebulaplayer logs api    follow the API log
    nebulaplayer update      re-run this installer (safe, keeps secrets)
    nebulaplayer db          psql on the nebulaplayer database

  Important
    * Secrets live in ${APP_DIR}/api/.env — back it up. Losing
      ENCRYPTION_MASTER_KEY makes stored Xtream passwords unreadable.
    * This project uses "prisma db push" (no migrations). Re-running the
      installer applies schema changes for you.
    * Installer log: ${LOG_FILE}
TXT
hr
echo
