#!/usr/bin/env bash
# Pre-seed WireGuard identity for the wlan-star underlay (keys only — tip via update.sh).
#
# Why this exists: the enrollment listener runs inside the bridge, but the hub builds
# ETH_PORTS from peers.json. A hub with zero peers has no gretap to bind, so the very
# first star cannot bootstrap from live enrollment alone. This seeds hub + spokes so
# tunnel_up.sh has eg-sNN legs on first start; live enrollment then handles re-enroll
# and future virgin seats.
#
#   sudo bash provision_hub_spoke.sh hub
#   sudo bash provision_hub_spoke.sh hub-add-peer --spoke-id 11 --pub <spokepub>
#   sudo bash provision_hub_spoke.sh spoke --spoke-id 11 --hub-pub <hubpub> \
#        --hub-endpoint 10.1.10.232:51820
#   sudo bash provision_hub_spoke.sh show
#
# Machine-readable lines for the PC orchestrator: KEY=value on stdout.

set -euo pipefail

if [[ "${EUID}" -ne 0 ]]; then
  echo "Run with sudo" >&2
  exit 1
fi

EDGE_ROOT="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "${EDGE_ROOT}/pog_edge_env.sh"

WG_DIR="${POG_EDGE_WG_DIR:-/etc/wireguard}"
WG_CONF="${WG_DIR}/wg0.conf"
WG_PORT="${POG_EDGE_WG_PORT:-51820}"
BANK="${EDGE_BANK:-0}"
HUB_STATE_DIR="${EDGE_HUB_STATE_DIR:-/etc/ethergold/hub}"
SPOKE_KEYS_DIR="${EDGE_SPOKE_KEYS_DIR:-/etc/ethergold/spoke}"

CMD="${1:-}"
shift || true

SPOKE_ID=""
HUB_PUB=""
HUB_ENDPOINT=""
SPOKE_PUB=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --spoke-id) SPOKE_ID="$2"; shift 2 ;;
    --hub-pub) HUB_PUB="$2"; shift 2 ;;
    --hub-endpoint) HUB_ENDPOINT="$2"; shift 2 ;;
    --pub) SPOKE_PUB="$2"; shift 2 ;;
    *) echo "FATAL: unknown arg $1" >&2; exit 1 ;;
  esac
done

if ! command -v wg >/dev/null 2>&1; then
  echo "FATAL: install wireguard — sudo apt install wireguard" >&2
  exit 1
fi

overlay_for() {
  printf '10.99.%s.%s' "${BANK}" "$1"
}

gretap_for() {
  printf 'eg-s%02d' "$1"
}

# Keypair per device; private key never leaves the box.
ensure_keypair() {
  local dir="$1"
  install -d -m 700 "${dir}"
  # umask before redirect — wg warns "world accessible" otherwise and some
  # orchestrators treat that stderr as fatal.
  (
    umask 077
    if [[ ! -s "${dir}/wg_private.key" ]]; then
      wg genkey > "${dir}/wg_private.key"
    fi
    if [[ ! -s "${dir}/wg_public.key" ]]; then
      wg pubkey < "${dir}/wg_private.key" > "${dir}/wg_public.key"
    fi
  )
  chmod 600 "${dir}/wg_private.key"
  chmod 644 "${dir}/wg_public.key"
}

backup_conf() {
  if [[ -f "${WG_CONF}" ]]; then
    cp -a "${WG_CONF}" "${WG_CONF}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
  fi
}

case "${CMD}" in
  hub)
    ensure_keypair "${HUB_STATE_DIR}"
    priv="$(cat "${HUB_STATE_DIR}/wg_private.key")"
    pub="$(cat "${HUB_STATE_DIR}/wg_public.key")"
    install -d -m 700 "${WG_DIR}"
    # Peers are added live by enrollment / restore — Interface only here.
    # Table=off: gretap rides the overlay /32, no wg-quick route takeover.
    if [[ -f "${WG_CONF}" ]] && grep -q "PrivateKey = ${priv}" "${WG_CONF}"; then
      echo "HUB_CONF=unchanged"
    else
      backup_conf
      umask 077
      cat > "${WG_CONF}" <<EOF
# EtherGold wlan-star HUB — bank ${BANK} overlay $(overlay_for 1)
# Spoke [Peer] blocks are added live (wg set) by enrollment and restored from
# ${HUB_STATE_DIR}/peers.json — do not hand-add peers here.
[Interface]
PrivateKey = ${priv}
ListenPort = ${WG_PORT}
Table = off
EOF
      chmod 600 "${WG_CONF}"
      echo "HUB_CONF=written"
    fi
    install -d -m 750 "${HUB_STATE_DIR}"
    [[ -f "${HUB_STATE_DIR}/peers.json" ]] || echo '{}' > "${HUB_STATE_DIR}/peers.json"
    touch "${HUB_STATE_DIR}/allowlist.txt"
    echo "HUB_PUB=${pub}"
    echo "HUB_OVERLAY=$(overlay_for 1)"
    echo "WG_PORT=${WG_PORT}"
    ;;

  hub-add-peer)
    if [[ -z "${SPOKE_ID}" || -z "${SPOKE_PUB}" ]]; then
      echo "FATAL: hub-add-peer needs --spoke-id N --pub <spokepub>" >&2
      exit 1
    fi
    spoke_pub="${SPOKE_PUB}"
    install -d -m 750 "${HUB_STATE_DIR}"
    overlay="$(overlay_for "${SPOKE_ID}")"
    gretap="$(gretap_for "${SPOKE_ID}")"
    python3 - "${HUB_STATE_DIR}/peers.json" "${spoke_pub}" "${SPOKE_ID}" "${overlay}" "${gretap}" <<'PY'
import json, os, sys, time
path, pub, sid, overlay, gretap = sys.argv[1:6]
try:
    peers = json.loads(open(path, encoding="utf-8").read())
    if not isinstance(peers, dict):
        peers = {}
except Exception:
    peers = {}
peers[pub] = {
    "spoke_id": int(sid),
    "overlay": overlay,
    "gretap": gretap,
    "enrolled_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    "source": "provision_hub_spoke",
}
tmp = path + ".tmp"
open(tmp, "w", encoding="utf-8").write(json.dumps(peers, indent=2, sort_keys=True) + "\n")
os.replace(tmp, path)
print(f"PEER_SEEDED={sid} overlay={overlay} gretap={gretap}")
PY
    chmod 640 "${HUB_STATE_DIR}/peers.json"
    if ! grep -qF "${spoke_pub}" "${HUB_STATE_DIR}/allowlist.txt" 2>/dev/null; then
      printf '%s spoke%s\n' "${spoke_pub}" "${SPOKE_ID}" >> "${HUB_STATE_DIR}/allowlist.txt"
    fi
    echo "ALLOWLIST_COUNT=$(grep -cve '^\s*$' "${HUB_STATE_DIR}/allowlist.txt" || true)"
    ;;

  spoke)
    if [[ -z "${SPOKE_ID}" || -z "${HUB_PUB}" || -z "${HUB_ENDPOINT}" ]]; then
      echo "FATAL: spoke needs --spoke-id N --hub-pub K --hub-endpoint IP:PORT" >&2
      exit 1
    fi
    ensure_keypair "${SPOKE_KEYS_DIR}"
    priv="$(cat "${SPOKE_KEYS_DIR}/wg_private.key")"
    pub="$(cat "${SPOKE_KEYS_DIR}/wg_public.key")"
    overlay="$(overlay_for "${SPOKE_ID}")"
    hub_overlay="$(overlay_for 1)"
    install -d -m 700 "${WG_DIR}"
    backup_conf
    umask 077
    cat > "${WG_CONF}" <<EOF
# EtherGold wlan-star SPOKE ${SPOKE_ID} — bank ${BANK} overlay ${overlay}
# gretap $(gretap_for "${SPOKE_ID}") rides this tunnel toward hub ${hub_overlay}.
[Interface]
PrivateKey = ${priv}
ListenPort = ${WG_PORT}
Table = off

[Peer]
PublicKey = ${HUB_PUB}
AllowedIPs = ${hub_overlay}/32
Endpoint = ${HUB_ENDPOINT}
PersistentKeepalive = 25
EOF
    chmod 600 "${WG_CONF}"
    echo "SPOKE_CONF=written"
    echo "SPOKE_PUB=${pub}"
    echo "SPOKE_ID=${SPOKE_ID}"
    echo "SPOKE_OVERLAY=${overlay}"
    echo "SPOKE_GRETAP=$(gretap_for "${SPOKE_ID}")"
    ;;

  show)
    echo "ROLE=${POG_EDGE_ROLE:-unset} TOPOLOGY=${EDGE_TOPOLOGY:-unset} BANK=${BANK}"
    echo "UPLINK=${EDGE_UPLINK:-unset} GAME_IF=${POG_EDGE_GAME_IF:-none}"
    if [[ -f "${WG_CONF}" ]]; then
      echo "WG_CONF=present"
      grep -E '^(ListenPort|AllowedIPs|Endpoint|Table) ' "${WG_CONF}" || true
      grep -c '^\[Peer\]' "${WG_CONF}" | sed 's/^/WG_CONF_PEERS=/'
    else
      echo "WG_CONF=MISSING"
    fi
    [[ -s "${HUB_STATE_DIR}/wg_public.key" ]] && echo "HUB_PUB=$(cat "${HUB_STATE_DIR}/wg_public.key")"
    [[ -s "${SPOKE_KEYS_DIR}/wg_public.key" ]] && echo "SPOKE_PUB=$(cat "${SPOKE_KEYS_DIR}/wg_public.key")"
    if [[ -f "${HUB_STATE_DIR}/peers.json" ]]; then
      python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('PEERS='+','.join(str(v.get('spoke_id')) for v in d.values()))" "${HUB_STATE_DIR}/peers.json" || true
    fi
    ;;

  *)
    echo "FATAL: usage: provision_hub_spoke.sh hub|hub-add-peer|spoke|show" >&2
    exit 1
    ;;
esac
