#!/usr/bin/env bash
# scripts/site-baseleg.sh {render|apply|check|install|remove|list} <site>
#
# Durable BASE L3 leg from the vcloud jumphost into a host-local, ISOLATED
# libvirt site network (D-126 amendment, 2026-07-17). office1-local is an
# isolated libvirt net (no <ip>, no <forward>), so the vcloud host has NO L3
# presence on 10.10.0.0/24 by default -- it must add its own address on the
# bridge (10.10.0.10/24, as-built) plus a route to the compose net
# (10.10.1.0/24 via voffice1 10.10.0.20). Both are lost on every reboot, which
# is exactly what stranded `ssh voffice1` after the vcloud patch+reboot.
#
# WHY a root system unit here (and NOT the D-126 rootless user forward): the
# forward in site-forward.sh PRESUPPOSES this leg -- it SSHes to 10.10.0.20,
# which is unreachable until the leg + route exist. L3 on a bridge needs root;
# there is no rootless equivalent. D-126 originally rejected "persist the L3
# route" over a netplan-vs-libvirt race, but a `Type=oneshot` unit ordered
# `After=libvirtd.service` runs AFTER libvirt has created the autostart bridge,
# so the race does not exist. This is that narrow reopening -- base leg ONLY.
#
# NO DRIFTING IDs (lib-net.sh PATTERN-1): the bridge name (virbr2 today) is
# assigned by libvirt and drifts on redefine. The STABLE key is the network
# NAME (office1-local); the bridge is DISCOVERED via `virsh net-info` at run
# time, never baked.
#
# NO SECRETS: this tool touches no key or credential material. `render`,
# `check`, `list` are READ-ONLY. `apply` mutates host L3 (idempotent) and needs
# root -- the boot unit runs it. `install`/`remove` manage the unit and are
# OPERATOR-RUN (root). ASCII + LF.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"   # self-locate
SELF="$HERE/site-baseleg.sh"

# --- leg table: MEASURED / as-built values ONLY (hard rule 2). Format:
#     name = "NET_NAME|HOST_CIDR|ROUTE_CIDR|ROUTE_GW"
#   NET_NAME   libvirt network NAME (stable key; the bridge is discovered from it)
#   HOST_CIDR  address the vcloud host adds on that bridge
#   ROUTE_CIDR service/compose net reached THROUGH the site
#   ROUTE_GW   next hop for ROUTE_CIDR (the containment VM on NET_NAME)
#
# office1: net office1-local (as-built vr1-office1-as-built.md:41 -> vcloud
# 10.10.0.10 on virbr2); compose 10.10.1.0/24 via voffice1 10.10.0.20 (:88).
# DC rows are DEFERRED: the DCs nest inside vvr1-dc0 and are reached by qemu+ssh
# (D-123 / R-5), NOT necessarily an L3 leg -- add a row ONLY if/when a DC needs
# one AND its values are measured (never an inferred IP).
declare -A LEGS=(
  [office1]="office1-local|10.10.0.10/24|10.10.1.0/24|10.10.0.20"
  # [vr1-dc0]="<net>|<host-cidr>|<route-cidr>|<route-gw>"   # MEASURE first (qemu+ssh may make this moot)
)

UNIT_PREFIX="site-baseleg-"
# The poll below is the ONLY backstop: Type=oneshot does NOT support Restart=, so
# if the bridge is not up within BRIDGE_WAIT at a contended cold boot, the leg
# never comes up and there is NO systemd-level retry. Widen this (not lower it)
# if a slow cold boot is observed to miss the window.
BRIDGE_WAIT=60   # seconds to wait for the autostart bridge post-libvirtd

die() { echo "site-baseleg: $*" >&2; exit 1; }

usage() {
  cat <<EOF
usage: bash scripts/site-baseleg.sh <cmd> <site>
  render  <site>   print the systemd system unit (read-only)
  apply   <site>   add the host leg + compose route, idempotent (root; the unit runs this)
  check   <site>   verify the leg + route are present (read-only; exit 0=ok, 1=missing)
  install <site>   write + enable --now the system unit (OPERATOR-RUN, root)
  remove  <site>   disable + delete the system unit (OPERATOR-RUN, root)
  list             show configured sites
sites: ${!LEGS[*]}
EOF
}

lookup() {
  local site="$1"
  [ -n "${LEGS[$site]+x}" ] || die "unknown site '$site' (have: ${!LEGS[*]})"
  IFS='|' read -r NET_NAME HOST_CIDR ROUTE_CIDR ROUTE_GW <<<"${LEGS[$site]}"
  [ -n "$NET_NAME" ] && [ -n "$HOST_CIDR" ] && [ -n "$ROUTE_CIDR" ] && [ -n "$ROUTE_GW" ] \
    || die "site '$site' row is malformed"
}

# Resolve the bridge from the STABLE network name (never a baked virbrN).
discover_bridge() {
  command -v virsh >/dev/null 2>&1 || die "no virsh on PATH (are we on the vcloud host?)"
  virsh net-info "$NET_NAME" 2>/dev/null | awk '/^Bridge:/{print $2}'
}

render_unit() {
  local site="$1"
  lookup "$site"
  # No baked bridge, no baked IP command: the unit invokes THIS script's `apply`,
  # which discovers the bridge and does the idempotent add at run time (hard rule
  # 2 + PATTERN-1). Ordered After=libvirtd so the autostart bridge already exists.
  # Ordering: MEASURED on the vcloud host -- monolithic libvirtd.service is active
  # and owns network creation here (virtnetworkd/virtqemud inactive). We ALSO order
  # after virtnetworkd.service so the unit stays correct on a modular libvirt host
  # (Ubuntu 24.04 / libvirt 10.x can split the daemons); After= on an inactive/absent
  # unit is a harmless no-op. The bridge poll in `apply` is the real guarantee.
  # NOTE: ExecStart is this script's ABSOLUTE path; a repo move/rename (cf. D-110)
  # breaks the boot unit -- re-run `install` after any relocation.
  cat <<EOF
[Unit]
Description=D-126 base L3 leg for ${site} (${HOST_CIDR} on ${NET_NAME} + route ${ROUTE_CIDR})
After=libvirtd.service virtnetworkd.service
Wants=libvirtd.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${SELF} apply ${site}

[Install]
WantedBy=multi-user.target
EOF
}

apply_leg() {
  local site="$1"
  lookup "$site"
  [ "$(id -u)" -eq 0 ] || die "apply needs root (adds an address + route)"
  local br="" i
  for ((i=0; i<BRIDGE_WAIT; i++)); do
    br="$(discover_bridge)"; [ -n "$br" ] && break
    sleep 1
  done
  [ -n "$br" ] || die "bridge for net '$NET_NAME' not present after ${BRIDGE_WAIT}s (libvirtd up?)"
  # Idempotent: only add the address if the bridge does not already carry it.
  if ip -br addr show dev "$br" 2>/dev/null | grep -qw "$HOST_CIDR"; then
    echo "leg present: $HOST_CIDR on $br"
  else
    ip addr add "$HOST_CIDR" dev "$br" || die "failed to add $HOST_CIDR on $br"
    echo "added leg: $HOST_CIDR on $br"
  fi
  # `ip route replace` is idempotent by construction.
  ip route replace "$ROUTE_CIDR" via "$ROUTE_GW" dev "$br" || die "failed to set route $ROUTE_CIDR"
  echo "route set: $ROUTE_CIDR via $ROUTE_GW dev $br"
}

check_leg() {
  local site="$1"
  lookup "$site"
  local br; br="$(discover_bridge)"
  [ -n "$br" ] || { echo "check: net '$NET_NAME' has no bridge"; return 1; }
  local rc=0
  if ip -br addr show dev "$br" 2>/dev/null | grep -qw "$HOST_CIDR"; then
    echo "ok   leg   $HOST_CIDR on $br"
  else
    echo "MISS leg   $HOST_CIDR on $br"; rc=1
  fi
  if ip route show "$ROUTE_CIDR" 2>/dev/null | grep -q "via $ROUTE_GW"; then
    echo "ok   route $ROUTE_CIDR via $ROUTE_GW"
  else
    echo "MISS route $ROUTE_CIDR via $ROUTE_GW"; rc=1
  fi
  return $rc
}

cmd="${1:-}"; site="${2:-}"
case "$cmd" in
  -h|--help|help|"") usage; [ -z "$cmd" ] && exit 1 || exit 0 ;;
  list)    echo "configured sites:"; for s in "${!LEGS[@]}"; do echo "  $s -> ${LEGS[$s]}"; done ;;
  render)  [ -n "$site" ] || die "render needs a <site>";  render_unit "$site" ;;
  apply)   [ -n "$site" ] || die "apply needs a <site>";   apply_leg "$site" ;;
  check)   [ -n "$site" ] || die "check needs a <site>";   check_leg "$site" ;;
  install)
    [ -n "$site" ] || die "install needs a <site>"
    [ "$(id -u)" -eq 0 ] || die "install needs root (writes /etc/systemd/system, enables the unit)"
    lookup "$site"
    unit="${UNIT_PREFIX}${site}.service"
    render_unit "$site" > "/etc/systemd/system/$unit"
    echo "wrote /etc/systemd/system/$unit"
    systemctl daemon-reload
    systemctl enable --now "$unit"
    systemctl --no-pager status "$unit" | head -8 || true
    ;;
  remove)
    [ -n "$site" ] || die "remove needs a <site>"
    [ "$(id -u)" -eq 0 ] || die "remove needs root"
    lookup "$site"
    unit="${UNIT_PREFIX}${site}.service"
    systemctl disable --now "$unit" 2>/dev/null || true
    rm -f "/etc/systemd/system/$unit"
    systemctl daemon-reload
    echo "removed $unit"
    ;;
  *) die "unknown command '$cmd' (see --help)" ;;
esac
