#!/usr/bin/env bash
# build-stack.sh — orchestrate the Ulicraft stack: offline prep then bring-up.
#
# Phases:
#   preflight   always — verify .env + required tools
#   --prep      ONLINE  — render configs, build landing, mirror mods + air-gap,
#                         fetch launcher, pre-bake the server volume
#   --up        OFFLINE — docker compose up -d the full stack
#   (no flag)   preflight + print what each phase would do, then exit
#
# Cautious by design: halts on the first missing var, missing tool, or failed
# step. Each sub-script is itself responsible for its own integrity checks.
set -euo pipefail

cd "$(dirname "$0")/.."   # repo root
COMPOSE=(docker compose)

# ---- helpers ---------------------------------------------------------------
log()  { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }

need_tool() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; }
have_script() { [ -x "tooling/$1" ] || die "missing/not-executable: tooling/$1"; }

# ---- preflight -------------------------------------------------------------
preflight() {
  log "preflight"
  [ -f .env ] || die ".env not found (copy .env.example and fill it)"
  # shellcheck disable=SC1091
  set -a; . ./.env; set +a
  : "${BASE_DOMAIN:?BASE_DOMAIN unset in .env}"
  : "${HOST_LAN_IP:?HOST_LAN_IP unset in .env}"
  : "${RCON_PASSWORD:?RCON_PASSWORD unset in .env}"

  need_tool docker
  ${COMPOSE[@]} version >/dev/null 2>&1 || die "docker compose plugin not available"
  need_tool envsubst
  need_tool jq
  need_tool curl
  need_tool sha1sum
  command -v pnpm >/dev/null 2>&1 || warn "pnpm not found — landing build will be skipped if absent"

  echo "  BASE_DOMAIN=${BASE_DOMAIN}  HOST_LAN_IP=${HOST_LAN_IP}"
  echo "  preflight ok"
}

# ---- online prep -----------------------------------------------------------
prep() {
  log "PHASE prep (requires internet)"

  log "render configs"
  have_script render-config.sh
  tooling/render-config.sh

  log "build landing page → www/"
  if command -v pnpm >/dev/null 2>&1; then
    ( cd landing && pnpm install --frozen-lockfile && BASE_DOMAIN="${BASE_DOMAIN}" pnpm run build )
  else
    warn "pnpm absent — skipping landing build"
  fi

  log "mirror mod jars (offline pack)"
  have_script mirror-mods.sh
  tooling/mirror-mods.sh

  log "mirror full air-gap upstream (meta/libraries/assets/client.jar)"
  have_script mirror-airgap.sh
  tooling/mirror-airgap.sh

  log "fetch launcher releases"
  have_script fetch-launcher.sh
  tooling/fetch-launcher.sh

  log "fetch authlib-injector.jar (server JVM agent → drasl)"
  if [ -f runtime/authlib-injector.jar ]; then
    echo "  runtime/authlib-injector.jar present — skipping"
  else
    ai_url="$(curl -fsSL https://api.github.com/repos/yushijinhun/authlib-injector/releases/latest \
      | jq -r '.assets[] | select(.name|test("authlib-injector-[0-9].*\\.jar$")) | .browser_download_url' | head -1)"
    [ -n "$ai_url" ] && [ "$ai_url" != "null" ] || die "could not resolve authlib-injector release asset"
    curl -fSL --retry 3 -o runtime/authlib-injector.jar "$ai_url"
    echo "  downloaded $(basename "$ai_url")"
  fi

  if [ "${ENABLE_MDNS:-false}" = true ]; then
    log "build avahi mDNS image (cache it for offline use)"
    "${COMPOSE[@]}" -f docker-compose.yml -f docker-compose.avahi.yml build avahi
  fi

  log "pre-bake server volume (TYPE=NEOFORGE install, one-shot, online)"
  # Bring minecraft up once so itzg installs NeoForge + mods into the volume,
  # then stop it. The party then runs offline against the baked volume.
  # caddy.yml so the pack (pack.${BASE_DOMAIN}) resolves + is served during bake.
  "${COMPOSE[@]}" -f docker-compose.yml -f docker-compose.caddy.yml up -d minecraft
  warn "watch logs until 'Done' then re-run with --up; or automate a readiness probe"
}

# ---- bring-up --------------------------------------------------------------
# Modes (DNS is YOUR party server — see tooling/dns-records.sh for the records):
#   online  base + landing/launcher; internet present, no mirror
#   airgap  online + full upstream mirror (tls internal :443) for no-internet
#   core    base only (no landing, no mirror) — debugging
up() {
  local mode="${1:-airgap}"
  # build-stack is the caddy (LAN/air-gap) deployer; caddy lives in its own file
  # now (the base stack can also run behind the host's nginx — see
  # docker-compose.nginx.yml). Always include caddy here.
  local -a files=(-f docker-compose.yml -f docker-compose.caddy.yml)
  case "$mode" in
    core)   ;;
    online) files+=(-f docker-compose.static.yml) ;;
    airgap) files+=(-f docker-compose.static.yml -f docker-compose.mirror.yml) ;;
    *) die "unknown up mode: '$mode' (use online|airgap|core)" ;;
  esac
  # Optional mDNS (.local) responder — see ENABLE_MDNS in .env.
  if [ "${ENABLE_MDNS:-false}" = true ]; then
    files+=(-f docker-compose.avahi.yml)
    case "$BASE_DOMAIN" in
      *.local) ;;
      *) warn "ENABLE_MDNS=true but BASE_DOMAIN=$BASE_DOMAIN is not *.local — mDNS won't resolve it" ;;
    esac
    pgrep -x avahi-daemon >/dev/null 2>&1 \
      || warn "host avahi-daemon not detected — the avahi container publishes through it; start it (e.g. 'systemctl start avahi-daemon')"
    [ -S /run/dbus/system_bus_socket ] \
      || warn "host D-Bus socket /run/dbus/system_bus_socket missing — avahi publish will fail"
    local ifc; ifc=$(ip -o link show 2>/dev/null | wc -l)
    [ "${ifc:-0}" -gt 6 ] \
      && warn "host has ${ifc} network interfaces — set 'allow-interfaces=<lan-nic>' in /etc/avahi/avahi-daemon.conf, else avahi self-collides (Local name collision)"
  fi
  log "PHASE up (mode=$mode${ENABLE_MDNS:+, mdns=$ENABLE_MDNS})"
  have_script render-config.sh
  tooling/render-config.sh          # cheap; ensures configs match current .env

  # Static mode mounts ./caddy/caddy-root-ca.crt as a file at /srv/ca-pub/ca.crt.
  # Pre-create it (empty) so Docker doesn't make a directory at that path; the
  # real cert is exported below once Caddy's local CA exists.
  case "$mode" in
    online|airgap) [ -e caddy/caddy-root-ca.crt ] || : > caddy/caddy-root-ca.crt ;;
  esac

  "${COMPOSE[@]}" "${files[@]}" up -d

  # Mirror/full provision Caddy's local CA — export it so guests can trust the
  # HTTPS asset mirror (served at http://${BASE_DOMAIN}/ca.crt in static/full).
  case "$mode" in
    airgap)
      local i
      for i in 1 2 3 4 5; do
        docker exec caddy cat /data/caddy/pki/authorities/local/root.crt \
          > caddy/caddy-root-ca.crt 2>/dev/null && break
        sleep 1
      done
      [ -s caddy/caddy-root-ca.crt ] \
        && echo "  exported Caddy CA → caddy/caddy-root-ca.crt (served at /ca.crt)" \
        || warn "Caddy CA not ready; export later: docker exec caddy cat /data/caddy/pki/authorities/local/root.crt > caddy/caddy-root-ca.crt"
      ;;
  esac

  log "stack up (mode=$mode). apex: http://${BASE_DOMAIN}  auth: http://auth.${BASE_DOMAIN}"
  echo
  log "Add these to your party DNS (mode=$mode):"
  [ -x tooling/dns-records.sh ] && tooling/dns-records.sh "$mode" | sed -n '/## Plain/,/^$/p'
}

# ---- dispatch --------------------------------------------------------------
preflight
case "${1:-}" in
  --prep) prep ;;
  --up)   up "${2:-airgap}" ;;
  "")     cat <<EOF

Nothing run. Choose a phase:
  $0 --prep            # ONLINE, once: render, build landing, mirror, fetch, pre-bake
  $0 --up [mode]       # bring the stack up; mode = online|airgap|core (default airgap)
                       #   online = landing + launcher; internet present, no mirror
                       #   airgap = online + full upstream mirror (:443), no internet
                       #   core   = base only (debugging)

DNS: configure your party DNS with the records from  tooling/dns-records.sh [mode].
mDNS (.local) alternative: set ENABLE_MDNS=true + BASE_DOMAIN=*.local in .env.
EOF
    ;;
  *) die "unknown arg: $1 (use --prep or --up [online|airgap|core])" ;;
esac
