#!/usr/bin/env bash
# Provision a NEW CPU streaming server as an exact sibling of an existing
# one (the London worker). Run from the developer machine:
#
#   deploy/provision-cpu-server.sh root@NEW_IP [phase]
#
# Phases (default: all, in this order; each is idempotent):
#   seed    copy the source box's config files (wine repo key/sources,
#           sshd hardening, fail2ban, sysctl, unattended-upgrades, admin
#           authorized_keys) to /root/w3cs-seed on the new box
#   base    timezone, apt full-upgrade, i386 + WineHQ repo, the source
#           box's manual package set, pinned+held wine-staging, users
#           (danny uid 1000 admin, ubuntu uid 1001 service w/ linger),
#           sudoers, swapfile, sysctl, fail2ban, unattended-upgrades, ufw
#   sync    box-to-box rsync of /opt/war3-runtime (minus the CRIU claim
#           images: CPU-specific, rebuilt by the deploy), /home/ubuntu
#           (lab, replays, profiles, war3-repo), /usr/local/go, the criu
#           binaries, /usr/local/lib/war3-* (replay gate .so, criu python
#           helpers, shims) and /etc/w3cs. Uses a temporary root key that exists
#           only for the transfer.
#   harden  PermitRootLogin no + the rest of 00-war3-hardening.conf, only
#           after the admin login with sudo has been verified
#
# Then deploy the stack:
#   W3CS_CPU_HOST=danny@NEW_IP W3CS_CPU_DOMAIN=<dns name> \
#     deploy/deploy-cpu-server.sh
#
# The source box is reached as its admin user (passwordless sudo). Nothing
# here needs a password; Battle.net or CD-key material is never touched
# (the wine prefixes are copied as-is).
set -euo pipefail

NEW=${1:?usage: provision-cpu-server.sh root@NEW_IP [seed|base|sync|harden|all]}
PHASE=${2:-all}
SOURCE=${W3CS_SOURCE_HOST:-danny@82.39.133.153}
ADMIN_USER=${W3CS_ADMIN_USER:-danny}
SERVICE_USER=${W3CS_SERVICE_USER:-ubuntu}
NEW_IP=${NEW#*@}
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=accept-new)
SRC=(ssh "${SSH_OPTS[@]}" "$SOURCE")
DST=(ssh "${SSH_OPTS[@]}" "$NEW")
# After `harden` root login is closed; later phases go through the admin.
ADM=(ssh "${SSH_OPTS[@]}" "$ADMIN_USER@$NEW_IP")

want() { [ "$PHASE" = all ] || [ "$PHASE" = "$1" ]; }

# Box-1 manual package set (apt-mark showmanual) minus the image/base
# packages every Ubuntu 24.04 cloud image already carries. Wine comes
# separately, pinned.
PACKAGES="build-essential certbot curl eatmydata fail2ban g++ gcc-mingw-w64-i686
gcc-multilib gdb git gstreamer1.0-nice gstreamer1.0-plugins-bad
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-tools htop
imagemagick iotop iperf3 jq libaio-dev libbsd-dev libcap-dev libdrm-dev
libeatmydata1 libegl-mesa0:i386 libegl1:i386 libgl1 libgl1-mesa-dri:i386
libgl1:i386 libglu1-mesa:i386 libglvnd0:i386 libglx-mesa0 libglx-mesa0:i386
libgnutls28-dev libgstreamer-plugins-bad1.0-dev
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev libjson-glib-dev
libnet1-dev libnftables-dev libnl-3-dev libopengl0:i386 libosmesa6
libprotobuf-c-dev libprotobuf-dev libsoup-3.0-dev libvulkan1:i386 libwrap0
libx11-dev libxfixes-dev libxtst-dev libzstd-dev mesa-libgallium:i386
mesa-utils net-tools nginx pkg-config protobuf-c-compiler protobuf-compiler
pulseaudio pulseaudio-utils python3-venv python3-websockets python3-zstandard
qemu-guest-agent rclone rsync smpq unattended-upgrades unzip uuid-dev
x11-utils x11-xserver-utils xdotool xvfb zlib1g-dev zstd"
WINE_VERSION=${W3CS_WINE_VERSION:-11.13~noble-1}

if want seed; then
  echo "=== seed: config files from $SOURCE -> $NEW:/root/w3cs-seed"
  "${SRC[@]}" "sudo tar -C / -cf - \
      etc/apt/keyrings/winehq-archive.key \
      etc/apt/sources.list.d/winehq-noble.sources \
      etc/ssh/sshd_config.d/00-war3-hardening.conf \
      etc/fail2ban/jail.local \
      etc/sysctl.d/99-war3.conf \
      etc/apt/apt.conf.d/20auto-upgrades \
      home/$ADMIN_USER/.ssh/authorized_keys" \
    | "${DST[@]}" "rm -rf /root/w3cs-seed && mkdir -p /root/w3cs-seed && tar -C /root/w3cs-seed -xf -"
  "${DST[@]}" "find /root/w3cs-seed -type f | sort"
fi

if want base; then
  echo "=== base: packages, users, swap, sysctl, fail2ban, ufw on $NEW"
  # ssh joins its arguments into one command string that the remote shell
  # re-parses: quote every argument and flatten the package list to one
  # line, or the remote side sees only the first word.
  # shellcheck disable=SC2086
  REMOTE_ARGS=$(printf '%q ' "$ADMIN_USER" "$SERVICE_USER" "$WINE_VERSION" "$(echo $PACKAGES)")
  "${DST[@]}" "bash -s $REMOTE_ARGS" <<'REMOTE'
set -euo pipefail
ADMIN_USER=$1; SERVICE_USER=$2; WINE_VERSION=$3; PACKAGES=$4
SEED=/root/w3cs-seed
export DEBIAN_FRONTEND=noninteractive
timedatectl set-timezone Europe/London

# --- users first: uids must match the source box (1000 admin, 1001
# service) because the rsynced runtime and lab trees carry numeric ids.
id "$ADMIN_USER" >/dev/null 2>&1 || \
  useradd -m -u 1000 -s /bin/bash -G users "$ADMIN_USER"
id "$SERVICE_USER" >/dev/null 2>&1 || \
  useradd -m -u 1001 -s /bin/bash -G users -c "war3 service" "$SERVICE_USER"
[ "$(id -u "$ADMIN_USER")" = 1000 ] || { echo "admin uid != 1000" >&2; exit 1; }
[ "$(id -u "$SERVICE_USER")" = 1001 ] || { echo "service uid != 1001" >&2; exit 1; }
printf '%s ALL=(ALL) NOPASSWD:ALL\n' "$ADMIN_USER" > /etc/sudoers.d/"$ADMIN_USER"
chmod 0440 /etc/sudoers.d/"$ADMIN_USER"
visudo -c -f /etc/sudoers.d/"$ADMIN_USER" >/dev/null
install -d -m 0700 -o "$ADMIN_USER" -g "$ADMIN_USER" /home/"$ADMIN_USER"/.ssh
install -m 0600 -o "$ADMIN_USER" -g "$ADMIN_USER" \
  "$SEED/home/$ADMIN_USER/.ssh/authorized_keys" \
  /home/"$ADMIN_USER"/.ssh/authorized_keys
loginctl enable-linger "$SERVICE_USER"

# --- apt: WineHQ repo (key + sources copied from the source box), i386.
dpkg --add-architecture i386
install -d -m 0755 /etc/apt/keyrings
install -m 0644 "$SEED/etc/apt/keyrings/winehq-archive.key" /etc/apt/keyrings/
install -m 0644 "$SEED/etc/apt/sources.list.d/winehq-noble.sources" \
  /etc/apt/sources.list.d/
apt-get update -qq
apt-get -yq -o Dpkg::Options::=--force-confold full-upgrade >/dev/null
# shellcheck disable=SC2086
apt-get -yq -o Dpkg::Options::=--force-confold install $PACKAGES >/dev/null
apt-get -yq install --allow-downgrades \
  "winehq-staging=$WINE_VERSION" "wine-staging=$WINE_VERSION" \
  "wine-staging-amd64=$WINE_VERSION" "wine-staging-i386:i386=$WINE_VERSION" \
  >/dev/null
apt-mark hold wine-staging wine-staging-amd64 wine-staging-i386:i386 \
  winehq-staging >/dev/null
apt-get -yq autoremove >/dev/null

# --- swap (4 GiB file beside the image's own swap.img, as on the source).
if [ ! -f /swapfile ]; then
  fallocate -l 4G /swapfile
  chmod 600 /swapfile
  mkswap /swapfile >/dev/null
  swapon /swapfile
  grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi

# --- kernel/network tuning, fail2ban, unattended-upgrades.
install -m 0644 "$SEED/etc/sysctl.d/99-war3.conf" /etc/sysctl.d/99-war3.conf
sysctl --system >/dev/null
install -m 0644 "$SEED/etc/fail2ban/jail.local" /etc/fail2ban/jail.local
systemctl enable --now fail2ban >/dev/null 2>&1
systemctl restart fail2ban
install -m 0644 "$SEED/etc/apt/apt.conf.d/20auto-upgrades" \
  /etc/apt/apt.conf.d/20auto-upgrades
systemctl enable --now unattended-upgrades >/dev/null 2>&1

# --- firewall: the source box's rule set (the deploy adds its UDP ranges
# again; ufw de-duplicates).
ufw --force default deny incoming >/dev/null
ufw --force default allow outgoing >/dev/null
for rule in 22/tcp 80/tcp 443/tcp 8145/tcp 8150/tcp 40000:40199/udp \
    40000:40599/udp 4443/udp; do
  ufw allow "$rule" >/dev/null
done
ufw --force enable >/dev/null

echo "base complete: $(wine --version 2>/dev/null || echo 'wine missing'), " \
  "swap $(swapon --show --noheadings | wc -l) devices, ufw $(ufw status | head -1)"
REMOTE
  echo "=== verify admin login + sudo before anything closes root"
  "${ADM[@]}" 'sudo -n true && echo "admin login + passwordless sudo OK on $(hostname)"'
fi

if want sync; then
  echo "=== sync: runtime + lab from $SOURCE -> $NEW (temporary root key)"
  TMPKEY=/root/.ssh/w3cs-clone-tmp
  PUB=$("${SRC[@]}" "sudo sh -c 'test -f $TMPKEY || ssh-keygen -q -t ed25519 -N \"\" -C w3cs-clone-tmp -f $TMPKEY; cat $TMPKEY.pub'")
  "${DST[@]}" "grep -qF '$PUB' /root/.ssh/authorized_keys || echo '$PUB' >> /root/.ssh/authorized_keys"
  # Long transfer: run detached on the source box and poll its log, so a
  # dropped developer session never leaves a half-copied tree unnoticed.
  "${SRC[@]}" "sudo bash -c 'cat > /root/w3cs-clone.sh' " <<REMOTE
#!/bin/bash
set -euo pipefail
RS=(rsync -aHAX --numeric-ids --delete -e "ssh -i $TMPKEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new")
ssh -i $TMPKEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new root@$NEW_IP \
  mkdir -p /opt/war3-runtime /home/$SERVICE_USER /usr/local/go /usr/local/libexec/war3 /etc/w3cs
"\${RS[@]}" --exclude /classic/w3cs-criu/images/ /opt/war3-runtime/ root@$NEW_IP:/opt/war3-runtime/
"\${RS[@]}" /home/$SERVICE_USER/ root@$NEW_IP:/home/$SERVICE_USER/
"\${RS[@]}" /usr/local/go/ root@$NEW_IP:/usr/local/go/
"\${RS[@]}" /usr/local/libexec/war3/ root@$NEW_IP:/usr/local/libexec/war3/
"\${RS[@]}" /etc/w3cs/ root@$NEW_IP:/etc/w3cs/
# The replay-open gate libraries, the CRIU python helpers and the shims
# live loose in /usr/local/lib (the claim image builder fails with
# "Classic replay gate library is missing" without them). No --delete:
# the directory is shared with python's own site packages.
rsync -aHAX --numeric-ids -e "ssh -i $TMPKEY -o BatchMode=yes -o StrictHostKeyChecking=accept-new" \
  --include='war3-*' --include='war3-*/**' --exclude='*' \
  /usr/local/lib/ root@$NEW_IP:/usr/local/lib/
echo CLONE-DONE
REMOTE
  "${SRC[@]}" "sudo sh -c 'chmod 700 /root/w3cs-clone.sh; rm -f /root/w3cs-clone.log; nohup /root/w3cs-clone.sh > /root/w3cs-clone.log 2>&1 < /dev/null & echo started pid \$!'"
  echo "poll: ssh $SOURCE 'sudo tail -3 /root/w3cs-clone.log'   (ends with CLONE-DONE)"
  echo "then: deploy/provision-cpu-server.sh $NEW sync-finish"
fi

if want sync-finish; then
  echo "=== sync-finish: verify clone, remove the temporary key"
  "${SRC[@]}" "sudo grep -q CLONE-DONE /root/w3cs-clone.log" || {
    echo "clone not finished:"; "${SRC[@]}" "sudo tail -5 /root/w3cs-clone.log"; exit 1; }
  "${SRC[@]}" "sudo rm -f /root/.ssh/w3cs-clone-tmp /root/.ssh/w3cs-clone-tmp.pub /root/w3cs-clone.sh"
  "${ADM[@]}" "sudo sed -i '/ w3cs-clone-tmp\$/d' /root/.ssh/authorized_keys; sudo du -sh /opt/war3-runtime /home/$SERVICE_USER /usr/local/go /usr/local/libexec/war3 /etc/w3cs; sudo ls -ld /home/$SERVICE_USER/w3cs-lab /opt/war3-runtime/classic/prefix-seat1"
fi

if want harden; then
  echo "=== harden: sshd drop-in (PermitRootLogin no) via $ADMIN_USER"
  "${ADM[@]}" 'sudo -n true' || { echo "admin login/sudo must work before hardening"; exit 1; }
  "${ADM[@]}" "sudo install -m 0644 /root/w3cs-seed/etc/ssh/sshd_config.d/00-war3-hardening.conf /etc/ssh/sshd_config.d/00-war3-hardening.conf && sudo sshd -t && sudo systemctl restart ssh && sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries|x11forwarding)'"
  if ssh -o BatchMode=yes -o ConnectTimeout=8 "root@$NEW_IP" true 2>/dev/null; then
    echo "ERROR: root login still accepted" >&2; exit 1
  fi
  echo "root login refused; admin login:"
  "${ADM[@]}" 'echo "  $(id -un)@$(hostname) sudo=$(sudo -n id -un)"'
  "${ADM[@]}" 'sudo rm -rf /root/w3cs-seed'
fi
