#!/usr/bin/env bash
# rago-server one-shot installer for Debian/Ubuntu on amd64.
#
#   curl -fsSL https://dl.rago.tv/install.sh | sudo bash
#   curl -fsSL https://dl.rago.tv/install.sh | sudo bash -s -- --token 'rc-…'
#
# What it does (idempotent — re-running upgrades in place):
#   * installs the ffmpeg transcoder build (jellyfin-ffmpeg7 from repo.jellyfin.org,
#     reached through the /usr/lib/rago-ffmpeg alias), plus the Noto Sans Medium
#     faces subtitle burn-in expects
#   * creates the `rago` system user and data dir /var/lib/rago-server
#   * installs the sha256-verified rago-server binary to /usr/local/bin
#   * writes the systemd unit, enables + starts it, opens ufw when active
#   * with --token: claims the server to your account before first start
#
# Flags:
#   --token rc-…   claim to your account (mint one at app.rago.tv/claim)
#   --name NAME    server name (default: this machine's hostname)
#   --version V    install the pinned build rago-server-linux-amd64-V
#   --uninstall    remove service + binary; keeps data, user, and ffmpeg
#   --purge        uninstall + delete the data dir and the rago user
#   --yes          skip the --purge confirmation (scripted runs)
#
# Everything is a function; main runs on the last line, so a truncated
# download executes nothing.

[ -n "${BASH_VERSION:-}" ] || { echo "run with bash"; exit 1; }
set -euo pipefail

DL_BASE="${RAGO_DL_BASE:-https://dl.rago.tv}"
BIN=/usr/local/bin/rago-server
DATA_DIR=/var/lib/rago-server
UNIT=/etc/systemd/system/rago-server.service
DEFAULTS=/etc/default/rago-server
JELLYFIN_KEYRING=/etc/apt/keyrings/jellyfin.gpg
JELLYFIN_LIST=/etc/apt/sources.list.d/jellyfin.list
# The path rago-server runs ffmpeg from. The deb's own directory is baked into
# the binary (rpath, libva's driver dir) so it stays where dpkg puts it; this
# alias is what the unit, the Transcoder panel, and a future Rago-built ffmpeg
# share.
FFMPEG_DIR=/usr/lib/rago-ffmpeg
FFMPEG_DEB_DIR=/usr/lib/jellyfin-ffmpeg
FONT_DIR=/usr/share/fonts/truetype/noto
# Frozen upstream commit — same pin as rago-server/Dockerfile.
NOTO_RAW=https://raw.githubusercontent.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSans

TOKEN="" NAME="" PIN="" MODE=install ASSUME_YES=0
BINARY_CHANGED=0 UNIT_CHANGED=0 DID_CLAIM=0 GROUPS_ADDED=0 FRESH_UNIT=0
WANT_VERSION="" TMP_BIN=""

if [ -t 1 ]; then
  say()  { printf '\033[1;35m==>\033[0m %s\n' "$*"; }
  warn() { printf '\033[1;33m==> warning:\033[0m %s\n' "$*" >&2; }
else
  say()  { printf '==> %s\n' "$*"; }
  warn() { printf '==> warning: %s\n' "$*" >&2; }
fi
die() { warn "$*"; exit 1; }

apt_get() { DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 "$@"; }

cleanup() { [ -n "$TMP_BIN" ] && rm -f "$TMP_BIN" || true; }
trap cleanup EXIT

parse_args() {
  while [ $# -gt 0 ]; do
    case "$1" in
      --token)     TOKEN="${2:?--token needs a value}"; shift 2 ;;
      --name)      NAME="${2:?--name needs a value}"; shift 2 ;;
      --version)   PIN="${2:?--version needs a value}"; shift 2 ;;
      --uninstall) MODE=uninstall; shift ;;
      --purge)     MODE=purge; shift ;;
      --yes)       ASSUME_YES=1; shift ;;
      -h|--help)   sed -n '2,23p' "$0" 2>/dev/null || true; exit 0 ;;
      *) die "unknown flag: $1 (see the header of this script for usage)" ;;
    esac
  done
}

require_root() {
  [ "$(id -u)" = 0 ] || die "must run as root — rerun with sudo"
}

preflight() {
  [ "$(uname -s)" = Linux ] || die "linux only (for macOS/other, use Docker: app.rago.tv/claim)"
  [ "$(uname -m)" = x86_64 ] || die "amd64 only for now ($(uname -m) detected — use Docker: app.rago.tv/claim)"
  [ -d /run/systemd/system ] || die "systemd is required (no /run/systemd/system)"
  [ -r /etc/os-release ] || die "cannot read /etc/os-release"
  # shellcheck disable=SC1091
  . /etc/os-release
  case " ${ID:-} ${ID_LIKE:-} " in
    *debian*|*ubuntu*) ;;
    *) die "Debian/Ubuntu (apt) only for now — on other distros, use Docker: app.rago.tv/claim" ;;
  esac
}

# Hard cut from the rilo-server era: an enabled old unit keeps :41400 and
# trips the port cross-check in report(). Stop + disable it and drop the
# unit; /var/lib/rilo-server, /usr/local/bin/rilo-server and the rilo user
# stay for the operator (no migration — reinstall, re-claim).
retire_old_install() {
  local old=/etc/systemd/system/rilo-server.service
  [ -f "$old" ] || return 0
  say "retiring old rilo-server.service (its data, binary and user are left in place)"
  systemctl disable --now rilo-server 2>/dev/null || true
  rm -f "$old"
  systemctl daemon-reload
  systemctl reset-failed rilo-server 2>/dev/null || true
}

apt_bootstrap() {
  say "apt bootstrap"
  apt_get update -y || warn "apt-get update had failures (a broken third-party repo?) — continuing"
  apt_get install -y ca-certificates curl gnupg
}

ensure_user() {
  if id -u rago >/dev/null 2>&1; then
    local uid; uid="$(id -u rago)"
    if [ "$uid" -ge 1000 ]; then
      die "a regular user 'rago' (uid $uid) already exists on this machine — refusing to run the service as it. Remove/rename that user or install on another host."
    fi
    say "system user rago exists"
  else
    say "creating system user rago"
    adduser --system --group --home "$DATA_DIR" --no-create-home rago
  fi
  # /dev/dri access for hardware transcoding. Recorded so a re-run after a
  # GPU driver install restarts the service into its new groups.
  local g
  for g in video render; do
    if getent group "$g" >/dev/null && ! id -nG rago | tr ' ' '\n' | grep -x "$g" >/dev/null; then
      usermod -aG "$g" rago
      GROUPS_ADDED=1
      say "added rago to group $g"
    fi
  done
}

ensure_data_dir() {
  if [ ! -d "$DATA_DIR" ]; then
    say "creating $DATA_DIR"
    install -d -o rago -g rago -m 750 "$DATA_DIR"
    return
  fi
  local owner; owner="$(stat -c %u "$DATA_DIR")"
  if [ "$owner" = "$(id -u rago)" ]; then
    return
  elif [ "$owner" = 0 ]; then
    # Heals a data dir created by an earlier root-run `rago-server claim`.
    say "fixing ownership of $DATA_DIR (was root)"
    chown -R rago:rago "$DATA_DIR"
  else
    die "$DATA_DIR exists but is owned by uid $owner — it looks like another install's data. Refusing to touch it."
  fi
}

# LATEST is one sha256sum line: "<sha256>  <filename>". --version V instead
# resolves rago-server-linux-amd64-V against SHA256SUMS (old lines persist).
install_binary() {
  local line want_sha want_file
  if [ -n "$PIN" ]; then
    printf '%s' "$PIN" | grep -Eq '^[A-Za-z0-9._-]{1,64}$' || die "bad --version: $PIN"
    line="$(curl -fsSL "$DL_BASE/rago-server/SHA256SUMS" | awk -v f="rago-server-linux-amd64-$PIN" '$2==f')"
    [ -n "$line" ] || die "no such build in SHA256SUMS: rago-server-linux-amd64-$PIN"
  else
    line="$(curl -fsSL "$DL_BASE/rago-server/LATEST")"
  fi
  read -r want_sha want_file <<<"$line"
  printf '%s' "$want_sha" | grep -Eq '^[0-9a-f]{64}$' || die "malformed sha256 in release manifest"
  printf '%s' "$want_file" | grep -Eq '^rago-server-linux-amd64-[A-Za-z0-9._-]{1,64}$' || die "malformed filename in release manifest: $want_file"
  WANT_VERSION="${want_file#rago-server-linux-amd64-}"

  if [ -f "$BIN" ] && [ "$(sha256sum "$BIN" | awk '{print $1}')" = "$want_sha" ]; then
    say "rago-server build $WANT_VERSION already installed"
    return
  fi
  say "downloading rago-server build $WANT_VERSION"
  TMP_BIN="$(dirname "$BIN")/.rago-server.$$"
  local got attempt
  for attempt in 1 2; do
    curl -fsSL -o "$TMP_BIN" "$DL_BASE/rago-server/$want_file"
    got="$(sha256sum "$TMP_BIN" | awk '{print $1}')"
    [ "$got" = "$want_sha" ] && break
    [ "$attempt" = 2 ] && die "sha256 mismatch for $want_file after retry — refusing to install"
    warn "sha256 mismatch, retrying download once"
  done
  chmod 755 "$TMP_BIN"
  mv -f "$TMP_BIN" "$BIN"   # same-dir rename: atomic; a running service keeps the old inode
  TMP_BIN=""
  BINARY_CHANGED=1
  say "installed $BIN ($WANT_VERSION, sha256 verified)"
}

# Claim tokens live ~4 minutes, so this runs right after the binary lands
# and BEFORE the big ffmpeg install. The claim must not race a running
# server: the run process keeps config in memory and a later in-process
# save would drop a credential written behind its back.
maybe_claim() {
  [ -n "$TOKEN" ] || return 0
  case "$TOKEN" in rc-*) ;; *) warn "token does not look like an rc-… claim token" ;; esac
  if grep -qs '^\[credential\]' "$DATA_DIR/config.toml"; then
    say "already claimed — ignoring --token"
    return 0
  fi
  if systemctl is-active --quiet rago-server 2>/dev/null; then
    say "stopping rago-server for the claim"
    systemctl stop rago-server
  fi
  say "claiming server"
  if runuser -u rago -- env RAGO_DATA_DIR="$DATA_DIR" "$BIN" claim --token "$TOKEN" ${NAME:+--name "$NAME"}; then
    DID_CLAIM=1
    say "claimed"
  else
    warn "claim failed (token expired or already used?) — the install continues; finish at http://<this-machine>:41400/setup"
  fi
}

install_ffmpeg() {
  say "ffmpeg (transcoder)"
  install -d -m 755 /etc/apt/keyrings
  curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --batch --yes --dearmor -o "$JELLYFIN_KEYRING"

  # Derivatives (Mint, Raspbian, Pop!…) must map to the ubuntu/debian pools —
  # repo.jellyfin.org has no dists for their own IDs.
  local repo_distro codename
  case " ${ID:-} ${ID_LIKE:-} " in
    *ubuntu*) repo_distro=ubuntu; codename="${UBUNTU_CODENAME:-${VERSION_CODENAME:-}}" ;;
    *)        repo_distro=debian; codename="${VERSION_CODENAME:-}" ;;
  esac
  # The pool may lag a brand-new release (or codename may be empty on sid) —
  # fall back to the newest LTS/stable, same trick as provision.sh's docker repo.
  if [ -z "$codename" ] || ! curl -fsI "https://repo.jellyfin.org/$repo_distro/dists/$codename/Release" >/dev/null 2>&1; then
    local fallback=bookworm; [ "$repo_distro" = ubuntu ] && fallback=noble
    warn "no jellyfin pool for $repo_distro/${codename:-?} — using $fallback"
    codename="$fallback"
  fi
  echo "deb [signed-by=$JELLYFIN_KEYRING arch=$(dpkg --print-architecture)] https://repo.jellyfin.org/$repo_distro $codename main" >"$JELLYFIN_LIST"

  # Hard-update only the jellyfin list; a stranger's dead PPA already got its
  # best-effort chance in apt_bootstrap and must not fail the install here.
  apt_get update -y \
    -o Dir::Etc::sourcelist="$JELLYFIN_LIST" \
    -o Dir::Etc::sourceparts=- \
    -o APT::Get::List-Cleanup=0
  # --no-install-recommends: parity with the Docker image — the deb bundles
  # its own GPU userspace (libva, iHD/radeonsi, vainfo) under $FFMPEG_DEB_DIR.
  apt_get install -y --no-install-recommends jellyfin-ffmpeg7 fontconfig fonts-noto-core libdrm2
  [ -x "$FFMPEG_DEB_DIR/ffmpeg" ] || die "jellyfin-ffmpeg7 installed but $FFMPEG_DEB_DIR/ffmpeg is missing"
  # Alias, idempotent. A REAL directory at $FFMPEG_DIR (an own ffmpeg package
  # some day) is left alone — `ln -sfn` into a directory would nest the link.
  if [ ! -e "$FFMPEG_DIR" ] || [ -L "$FFMPEG_DIR" ]; then
    ln -sfn "$(basename "$FFMPEG_DEB_DIR")" "$FFMPEG_DIR"
  fi
  [ -x "$FFMPEG_DIR/ffmpeg" ] || die "$FFMPEG_DIR/ffmpeg is not executable"
}

# Subtitle burn-in hardcodes "Noto Sans Medium", resolved via fontconfig;
# without these faces libass substitutes silently and burns the wrong font.
# Same two frozen statics as the Docker image (fonts-noto-core stops at
# Regular/Bold on bookworm).
install_fonts() {
  if [ ! -f "$FONT_DIR/NotoSans-Medium.ttf" ] || [ ! -f "$FONT_DIR/NotoSans-MediumItalic.ttf" ]; then
    say "installing Noto Sans Medium faces"
    install -d "$FONT_DIR"
    curl -fsSL -o "$FONT_DIR/NotoSans-Medium.ttf" "$NOTO_RAW/NotoSans-Medium.ttf"
    curl -fsSL -o "$FONT_DIR/NotoSans-MediumItalic.ttf" "$NOTO_RAW/NotoSans-MediumItalic.ttf"
    fc-cache -f >/dev/null
  fi
  # No `grep -q` here: its early exit would SIGPIPE fc-list under pipefail.
  fc-list | grep "NotoSans-Medium.ttf" >/dev/null || die "fontconfig cannot see NotoSans-Medium.ttf — subtitle burn-in would use the wrong font"
}

write_defaults() {
  [ -f "$DEFAULTS" ] && return 0
  say "writing $DEFAULTS"
  cat >"$DEFAULTS" <<'EOF'
# rago-server overrides — read by the systemd unit on start, one KEY=VALUE
# per line. Apply with: sudo systemctl restart rago-server
#
#RAGO_PORT=41400
#RAGO_NAME=my-server
#RAGO_ADVERTISE_ADDRESSES=
#RAGO_TRANSCODE_DIR=
#RAGO_OPENSUBTITLES_API_KEY=
EOF
  chmod 640 "$DEFAULTS"
}

write_unit() {
  local before=""
  [ -f "$UNIT" ] && before="$(sha256sum "$UNIT" | awk '{print $1}')" || FRESH_UNIT=1
  cat >"$UNIT" <<'EOF'
[Unit]
Description=Rago Media Server
After=network-online.target
Wants=network-online.target

[Service]
User=rago
Group=rago
Environment=RAGO_DATA_DIR=/var/lib/rago-server
Environment=RAGO_FFMPEG=/usr/lib/rago-ffmpeg/ffmpeg
Environment=RAGO_FFPROBE=/usr/lib/rago-ffmpeg/ffprobe
EnvironmentFile=-/etc/default/rago-server
ExecStart=/usr/local/bin/rago-server run
WorkingDirectory=/var/lib/rago-server
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
SyslogIdentifier=rago-server
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=full
# Deliberately NOT hardened further: media libraries live anywhere (incl.
# /home), transcoding needs /dev/dri + /dev/nvidia*, and the pure-Go sqlite
# JIT needs writable+executable pages. Do not add ProtectHome,
# PrivateDevices/DeviceAllow, or MemoryDenyWriteExecute.

[Install]
WantedBy=multi-user.target
EOF
  if [ "$FRESH_UNIT" = 1 ] || [ "$before" != "$(sha256sum "$UNIT" | awk '{print $1}')" ]; then
    UNIT_CHANGED=1
  fi
}

start_service() {
  systemctl daemon-reload
  systemctl enable rago-server >/dev/null 2>&1 || systemctl enable rago-server
  if ! systemctl is-active --quiet rago-server; then
    say "starting rago-server"
    systemctl start rago-server
  elif [ "$BINARY_CHANGED" = 1 ] || [ "$UNIT_CHANGED" = 1 ] || [ "$DID_CLAIM" = 1 ] || [ "$GROUPS_ADDED" = 1 ]; then
    say "restarting rago-server"
    systemctl restart rago-server
  else
    say "rago-server unchanged — leaving it running"
  fi
}

resolve_port() {
  local p=""
  if [ -f "$DEFAULTS" ]; then
    p="$(sed -n 's/^RAGO_PORT=//p' "$DEFAULTS" | tail -n1 | tr -d '"'"'" || true)"
  fi
  if [ -z "$p" ] && [ -f "$DATA_DIR/config.toml" ]; then
    p="$(awk -F= '/^\[/{s=$0} s=="[server]" && $1 ~ /^[ \t]*port[ \t]*$/ {gsub(/[ \t]/,"",$2); print $2; exit}' "$DATA_DIR/config.toml" || true)"
  fi
  printf '%s' "${p:-41400}"
}

configure_firewall() {
  command -v ufw >/dev/null 2>&1 || return 0
  ufw status 2>/dev/null | grep '^Status: active' >/dev/null || return 0
  local port; port="$(resolve_port)"
  say "ufw: allowing $port/tcp + 5353/udp (mDNS)"
  ufw allow "$port/tcp" comment 'rago-server' >/dev/null
  ufw allow 5353/udp comment 'rago-server mDNS' >/dev/null
}

lan_ip() {
  ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<NF;i++) if($i=="src"){print $(i+1); exit}}' \
    || hostname -I 2>/dev/null | awk '{print $1}' || true
}

report() {
  local port body="" version="" build="" claimed=""
  port="$(resolve_port)"
  # First boot probes hardware with real test encodes before serving (~25-40s
  # on GPU hosts); the socket accepts early, so poll patiently with short curls.
  say "waiting for rago-server on port $port"
  for _ in $(seq 1 45); do
    body="$(curl --max-time 2 -fsS "http://127.0.0.1:$port/healthz" 2>/dev/null || true)"
    [ -n "$body" ] && break
    sleep 2
  done
  if [ -z "$body" ]; then
    warn "no answer on http://127.0.0.1:$port/healthz after 90s — check: systemctl status rago-server ; journalctl -u rago-server -n 20"
    exit 1
  fi
  version="$(printf '%s' "$body" | grep -o '"version":"[^"]*"' | head -n1 | cut -d'"' -f4)"
  build="$(printf '%s' "$body" | grep -o '"commit":"[^"]*"' | head -n1 | cut -d'"' -f4)"
  # The pinned filename carries the git sha; a stamped server answers with
  # it as "commit". An older unstamped binary only has its version, which
  # mismatches too — the intended outcome for a squatter on the port.
  if [ -n "$WANT_VERSION" ] && [ "${build:-$version}" != "$WANT_VERSION" ]; then
    warn "port $port answered with build '${build:-$version}' but this install put down '$WANT_VERSION' — a DIFFERENT rago-server (container?) is likely bound to this port. Set RAGO_PORT in $DEFAULTS and restart."
    exit 1
  fi
  claimed="$(curl --max-time 2 -fsS "http://127.0.0.1:$port/v1/server" 2>/dev/null | grep -o '"claimed":[a-z]*' | cut -d: -f2 || true)"

  local ip; ip="$(lan_ip)"
  echo
  say "rago-server $version${build:+ ($build)} is running"
  echo "    service   systemctl status rago-server   (logs: journalctl -u rago-server -f)"
  echo "    data      $DATA_DIR"
  echo "    overrides $DEFAULTS"
  if [ "$claimed" = "true" ]; then
    echo "    app       https://app.rago.tv — your server is claimed and connected"
  else
    echo "    setup     http://${ip:-<this-machine>}:$port/setup — finish connecting it to your account"
  fi
  echo
  echo "    Media: give the 'rago' user read access to your library folders, e.g."
  echo "      sudo setfacl -R -m u:rago:rX /path/to/media    (or add rago to the owning group)"
  echo
  echo "    Upgrade later by re-running this installer. Remove with: --uninstall"
}

do_uninstall() {
  local purge="$1" port
  port="$(resolve_port)"
  if [ "$purge" = 1 ] && [ "$ASSUME_YES" != 1 ]; then
    # -r /dev/tty is not enough: the node exists even with no controlling
    # terminal, and only opening it tells the truth.
    if ! { true </dev/tty; } 2>/dev/null; then
      die "--purge needs a terminal to confirm (or pass --yes)"
    fi
    local answer=""
    printf 'This deletes %s (library metadata, watch history, server identity).\nYour media files are untouched. Type "purge" to confirm: ' "$DATA_DIR" >/dev/tty
    read -r answer </dev/tty || true
    [ "$answer" = purge ] || die "aborted — nothing removed"
  fi
  say "removing rago-server service"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl stop rago-server 2>/dev/null || true
    systemctl disable rago-server 2>/dev/null || true
  fi
  rm -f "$UNIT" "$BIN"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl daemon-reload || true
    systemctl reset-failed rago-server 2>/dev/null || true
  fi
  if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep '^Status: active' >/dev/null; then
    ufw --force delete allow "$port/tcp" >/dev/null 2>&1 || true
    ufw --force delete allow 5353/udp >/dev/null 2>&1 || true
  fi
  if [ "$purge" = 1 ]; then
    say "purging data + user"
    rm -rf "$DATA_DIR" "$DEFAULTS"
    deluser --system rago 2>/dev/null || true
    delgroup --system rago 2>/dev/null || true
    [ -L "$FFMPEG_DIR" ] && rm -f "$FFMPEG_DIR"
    say "purged. The ffmpeg package was left installed (remove with: apt-get remove jellyfin-ffmpeg7)"
  else
    say "uninstalled. Kept: $DATA_DIR (identity + credential — a reinstall boots claimed),"
    say "$DEFAULTS, the rago user, and ffmpeg."
    say "To wipe everything: rerun with --purge"
  fi
}

main() {
  parse_args "$@"
  require_root
  case "$MODE" in
    uninstall) do_uninstall 0; exit 0 ;;
    purge)     do_uninstall 1; exit 0 ;;
  esac
  say "rago-server installer"
  preflight
  retire_old_install
  apt_bootstrap
  ensure_user
  ensure_data_dir
  install_binary
  maybe_claim
  install_ffmpeg
  install_fonts
  write_defaults
  write_unit
  start_service
  configure_firewall
  report
}

main "$@"
