#!/bin/bash
# panel-native Windrose entrypoint.
#
# Contract:
#   /game       — SteamCMD-installed Windows build of Windrose (volume,
#                 populated by the panel's SteamCMD updater sidecar).
#                 If missing, exit with EX_CONFIG and a helpful message.
#   /game-saves — ServerDescription.json + R5/Saved/ tree (volume).
#
# Runs as root for its opening stage (fix ownership after the SteamCMD
# sidecar wrote as root, nuke stale X lock, symlink /Logs + R5/Saved into
# the save volume), then drops to the `panel` user for Wine + Xvfb + the
# game. Wine refuses to run as root, so the drop is load-bearing.

set -euo pipefail

log() { printf '[panel-windrose] %s\n' "$*"; }

# ----- stage 1: root-only bootstrap -----
if [[ "$(id -u)" = "0" ]]; then
  chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true

  # Remove Debian/Ubuntu's "127.0.1.1 <hostname>" mapping from the
  # container's /etc/hosts (docker manages a per-container copy at
  # /var/lib/docker/containers/<id>/hosts, so this does NOT touch the
  # host file).
  #
  # Why: Windrose's UE5 / Wine ICE stack does gethostname() +
  # gethostbyname(), gets 127.0.1.1, then enumerates it as a host
  # ICE candidate. When a player joins via cloud P2P the server
  # tries to send STUN from 127.0.1.1 to the peer's relay, the
  # Linux kernel rejects with EACCES (loopback can't route
  # externally), Windrose retries a few times then crashes the
  # *server* process. Removing the mapping makes gethostbyname
  # fall through, ICE skips the loopback candidate, and joins work.
  # Note: /etc/hosts is a docker bind mount of the per-container
  # /var/lib/docker/containers/<id>/hosts; `sed -i` fails with
  # "Device or resource busy" on rename, so we filter then truncate-
  # write in place via grep|tee.
  #
  # Two-stage fix:
  #   1) Strip any "127.0.1.1 <hostname>" line.
  #   2) Pin "127.0.0.1 <hostname>" so NSS-files wins before DNS —
  #      otherwise systemd-resolved on the host (which the container's
  #      resolv.conf points at via 127.0.0.53 under network_mode=host)
  #      synthesizes <hostname>=>127.0.1.1 from thin air, undoing
  #      step 1. With <hostname>=>127.0.0.1 in /etc/hosts, ICE's
  #      loopback filter actually catches it and the candidate is
  #      dropped instead of becoming an EACCES STUN.
  if [[ -w /etc/hosts ]]; then
    HOSTNAME_VAL="$(hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo localhost)"
    grep -vE '^127\.0\.1\.[0-9]+[[:space:]]' /etc/hosts > /tmp/hosts.new || true
    if [[ -s /tmp/hosts.new ]]; then
      cat /tmp/hosts.new > /etc/hosts
    fi
    if ! grep -qE "^127\\.0\\.0\\.1[[:space:]].*\\b${HOSTNAME_VAL}\\b" /etc/hosts; then
      printf '127.0.0.1 %s\n' "${HOSTNAME_VAL}" >> /etc/hosts
    fi
    rm -f /tmp/hosts.new
  fi

  # UE5 under Wine writes to Z:\Logs (= /Logs) and Z:\<Project>\Saved etc.
  # Pre-create /Logs as a symlink into the save volume so logs survive
  # container recreate and show up in the Files tab.
  mkdir -p /game-saves/WindroseLogs
  chown -R panel:panel /game-saves/WindroseLogs
  if [[ ! -e /Logs || -L /Logs ]]; then
    ln -sfn /game-saves/WindroseLogs /Logs
  fi

  log "dropping privileges to panel (UID 1000)"
  exec setpriv --reuid=panel --regid=panel --clear-groups \
       --inh-caps=-all --bounding-set=-all \
       env \
         HOME=/home/panel \
         WINEPREFIX=/home/panel/.wine \
         WINEDEBUG=-all \
         SERVER_NAME="${SERVER_NAME:-panel Windrose}" \
         DIRECT_PORT="${DIRECT_PORT:-7777}" \
         USE_DIRECT_CONNECTION="${USE_DIRECT_CONNECTION:-true}" \
         PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
       /entrypoint.sh "$@"
fi

GAME_DIR=/game
SAVE_DIR=/game-saves

# --- locate the server binary ---
# Windrose's StartServerForeground.bat launches the shipping binary directly:
#   start /abovenormal R5\Binaries\Win64\WindroseServer-Win64-Shipping.exe -log
# The WindroseServer.exe at the game root is a ~260KB launcher stub, not the
# real server. Prefer the shipping binary; fall back to any match if Windrose
# changes their layout.
CANDIDATES=(
  "${GAME_DIR}/R5/Binaries/Win64/WindroseServer-Win64-Shipping.exe"
  "${GAME_DIR}/R5/Binaries/WinGDK/WindroseServer-WinGDK-Shipping.exe"
  "${GAME_DIR}/R5/Binaries/Win64/WindroseServer.exe"
)
DEDI_EXE=""
for c in "${CANDIDATES[@]}"; do
  if [[ -f "$c" ]]; then
    DEDI_EXE="$c"
    break
  fi
done
if [[ -z "${DEDI_EXE}" ]]; then
  # Last-ditch: find any *-Shipping.exe (or WindroseServer.exe in a Binaries dir).
  found=$(find "${GAME_DIR}" -maxdepth 6 -iname "WindroseServer*.exe" -type f 2>/dev/null | grep -i "Shipping\|Binaries" | head -1 || true)
  if [[ -n "${found}" ]]; then
    DEDI_EXE="${found}"
  fi
fi
if [[ -z "${DEDI_EXE}" ]]; then
  log "ERROR: WindroseServer-Win64-Shipping.exe not found under ${GAME_DIR}/R5/Binaries/."
  log "       Run 'Update' on this server in the panel — that launches a"
  log "       SteamCMD sidecar which installs Windrose (Windows build) into"
  log "       this volume via +@sSteamCmdForcePlatformType windows."
  exit 78   # EX_CONFIG
fi
log "found server binary: ${DEDI_EXE}"

# --- persist Saves into /game-saves so SteamCMD validate can't clobber ---
# UE5 writes to <GameRoot>/R5/Saved/. Move the directory on first boot then
# symlink. Rebooting finds the symlink already in place; idempotent.
mkdir -p "${SAVE_DIR}/R5"
if [[ -d "${GAME_DIR}/R5/Saved" && ! -L "${GAME_DIR}/R5/Saved" ]]; then
  if [[ ! -d "${SAVE_DIR}/R5/Saved" ]]; then
    mv "${GAME_DIR}/R5/Saved" "${SAVE_DIR}/R5/Saved"
  else
    rm -rf "${GAME_DIR}/R5/Saved"
  fi
fi
mkdir -p "${SAVE_DIR}/R5/Saved"
mkdir -p "${GAME_DIR}/R5"
ln -sfn "${SAVE_DIR}/R5/Saved" "${GAME_DIR}/R5/Saved"

# --- ServerDescription.json: seed → migrate → symlink ---
# Windrose 0.10.x generates ServerDescription.json at /game/R5/ on first
# boot, which races our "migrate to save volume" step and leaves the file
# unmigrated until the next restart (panel's Config tab reads from
# /game-saves/R5/). Fix: seed a minimal stub in the save volume BEFORE
# the game starts. The game opens the existing file, fills in empty
# InviteCode / PersistentServerId / WorldIslandId on first save, and panel
# reads a populated config immediately — no first-boot restart dance.
# Template mirrors cytech-services AMP (windroseserverdescription.json).
mkdir -p "${GAME_DIR}/R5" "${SAVE_DIR}/R5"
DESC_GAME="${GAME_DIR}/R5/ServerDescription.json"
DESC_SAVE="${SAVE_DIR}/R5/ServerDescription.json"

if [[ -f "${DESC_GAME}" && ! -L "${DESC_GAME}" ]]; then
  # Legacy path: a prior boot wrote the file at /game/R5/ without migration.
  if [[ ! -s "${DESC_SAVE}" ]]; then
    log "migrating existing ServerDescription.json into save volume"
    mv "${DESC_GAME}" "${DESC_SAVE}"
  else
    rm -f "${DESC_GAME}"
  fi
fi

if [[ ! -s "${DESC_SAVE}" ]]; then
  log "seeding blank ServerDescription.json (game fills in InviteCode, WorldIslandId, PersistentServerId on first save)"
  cat > "${DESC_SAVE}" <<'JSON'
{
	"Version": 1,
	"ServerDescription_Persistent":
	{
		"PersistentServerId": "",
		"InviteCode": "",
		"IsPasswordProtected": false,
		"Password": "",
		"ServerName": "panel Windrose",
		"Note": "",
		"WorldIslandId": "",
		"MaxPlayerCount": 8,
		"P2pProxyAddress": "127.0.0.1"
	}
}
JSON
fi

# Symlink /game/R5/ServerDescription.json → save-volume copy so the game
# reads + writes the persistent file regardless of which path it opens.
ln -sfn "${DESC_SAVE}" "${DESC_GAME}"

# --- patch DirectConnectionServerPort + UseDirectConnection from env ---
# Windrose reads its listening port from this JSON (no CLI flag honored).
# We patch on every boot so a panel-allocated DIRECT_PORT flows through.
# UseDirectConnection: true = listen on a public IP, players use IP+port.
#                       false = use windrose.support cloud P2P (NAT-traversal
#                       via TURN). Required when the host is on a LAN with
#                       no port-forward, or for invite-code-only flows.
# Default: true (matches the panel's typical "expose host networking + open
# the port" deployment). Set USE_DIRECT_CONNECTION=false on a per-instance
# basis (panel config_values) to switch to cloud P2P.
USE_DIRECT_CONN_LOWER="$(printf '%s' "${USE_DIRECT_CONNECTION:-true}" | tr '[:upper:]' '[:lower:]')"
case "${USE_DIRECT_CONN_LOWER}" in
  false|0|no|off) UDC_VAL="false" ;;
  *)              UDC_VAL="true"  ;;
esac
if [[ -s "${DESC_SAVE}" ]]; then
  log "patching ServerDescription.json (DirectConnectionServerPort=${DIRECT_PORT}, UseDirectConnection=${UDC_VAL})"
  if grep -q '"DirectConnectionServerPort"' "${DESC_SAVE}"; then
    sed -i -E "s|(\"DirectConnectionServerPort\"[[:space:]]*:[[:space:]]*)[0-9]+|\1${DIRECT_PORT}|" "${DESC_SAVE}"
  else
    # Insert before the closing brace of ServerDescription_Persistent.
    sed -i -E "s|(\"P2pProxyAddress\"[[:space:]]*:[[:space:]]*\"[^\"]*\")|\1,\n\t\t\"UseDirectConnection\": ${UDC_VAL},\n\t\t\"DirectConnectionServerPort\": ${DIRECT_PORT}|" "${DESC_SAVE}"
  fi
  if grep -q '"UseDirectConnection"' "${DESC_SAVE}"; then
    sed -i -E "s|(\"UseDirectConnection\"[[:space:]]*:[[:space:]]*)(true\|false)|\1${UDC_VAL}|" "${DESC_SAVE}"
  fi
fi

# Belt-and-suspenders watcher — catches the odd case where a future
# Windrose build writes to /game/R5/ through the symlink's parent dir
# instead of following the link. Exits once things are consistent;
# bounded to 30 min to avoid leaking on crash paths.
(
  for _ in $(seq 1 360); do
    sleep 5
    if [[ -L "${DESC_GAME}" ]] && [[ -s "${DESC_SAVE}" ]]; then exit 0; fi
    if [[ -f "${DESC_GAME}" ]] && [[ ! -L "${DESC_GAME}" ]]; then
      log "watcher: re-migrating ServerDescription.json (game wrote through the symlink parent?)"
      if [[ ! -s "${DESC_SAVE}" ]]; then
        mv "${DESC_GAME}" "${DESC_SAVE}"
      else
        rm -f "${DESC_GAME}"
      fi
      ln -sfn "${DESC_SAVE}" "${DESC_GAME}"
      exit 0
    fi
  done
) &
DESC_WATCHER_PID=$!

# --- Wine prefix bootstrap (first boot, or repair after a partial init). xvfb-run
#     handles X for the real run; for prefix init we still need a display so
#     wineboot's installer dialogs auto-dismiss. ---
NEED_INIT=0
if [[ ! -d "${WINEPREFIX}" || ! -f "${WINEPREFIX}/system.reg" ]]; then
  NEED_INIT=1
fi
# A previous boot can leave a *partially* initialized prefix (system.reg
# present but kernel32.dll missing) if wineboot was killed early — that
# state crashes every subsequent start with "could not load kernel32.dll,
# status c0000135" because our existing-prefix check passes. Detect and
# repair by nuking the broken prefix and re-initing from scratch.
if [[ "${NEED_INIT}" = 0 && ! -f "${WINEPREFIX}/drive_c/windows/system32/kernel32.dll" ]]; then
  log "wine prefix at ${WINEPREFIX} is missing kernel32.dll — wiping and reinitializing"
  rm -rf "${WINEPREFIX}"
  NEED_INIT=1
fi
if [[ "${NEED_INIT}" = 1 ]]; then
  log "initializing Wine prefix at ${WINEPREFIX}"
  xvfb-run -a wineboot --init 2>&1 | sed 's/^/  /' || true
  wineserver -w || true
  if [[ ! -f "${WINEPREFIX}/drive_c/windows/system32/kernel32.dll" ]]; then
    log "WARNING: wineboot finished but kernel32.dll still missing — game start will fail"
  fi
fi

# --- go ---
BIN_DIR="$(dirname "${DEDI_EXE}")"
cd "${BIN_DIR}"
log "starting Windrose dedicated server"
log "  game dir:  ${GAME_DIR}"
log "  save dir:  ${SAVE_DIR}"
log "  binary:    ${DEDI_EXE}"
log "  bin dir:   ${BIN_DIR}"
log "  port:      ${DIRECT_PORT}"

trap 'log "SIGTERM received; stopping server"; wineserver -k 2>/dev/null || true; kill "${DESC_WATCHER_PID:-}" 2>/dev/null || true; exit 0' TERM INT

# Launch flags — matched to cytech-services' AMP template (windrose.kvp):
#   R5                     project name (required positional for UE5 servers)
#   -stdout                mirror UE5 log categories to stdout, not just R5.log
#   -FullStdOutLogOutput   disable stdout filtering — the LogGlobalStatus ready
#                          line we grep for is category-filtered otherwise and
#                          only lands in R5.log. This flag is THE reason AMP's
#                          AppReadyRegex ever matches.
#   -log                   enable the UE5 log file (R5/Saved/Logs/R5.log) too
#   -UNATTENDED / -NullRHI standard UE5 headless flags; harmless no-ops on this
#                          build but documented in other UE5 dedicated patterns.
# Port + server identity come from ServerDescription.json (we patch the
# port above on every boot from $DIRECT_PORT). xvfb-run -a auto-allocates
# a free X display — see panel/memory/gotchas.md for why we can't use :99.
exec stdbuf -oL -eL xvfb-run -a --server-args="-screen 0 1024x768x24" \
    wine "${DEDI_EXE}" \
    R5 \
    -stdout \
    -FullStdOutLogOutput \
    -log \
    -UNATTENDED \
    -NullRHI
