#!/usr/bin/env bash
# scripts/carve-host-interfaces.sh <hostname> [--apply]
#
# Pattern-A interface carve for ONE freshly-commissioned host. Reconstructs the
# host network tree that was lost when the machine was decomposed. Default is
# DRY-RUN (resolves every id live and prints each mutation it WOULD run, changes
# nothing). Pass --apply to execute.
#
# Target tree (octet N = .40-.43 by host index; see lib-hosts.sh HOST_OCTET):
#   enp1s0 --> br-ex (OVS bridge) + STATIC 10.12.4.N (provider-public; Pattern A)
#                                        MAAS builds the OVS br-ex on enp1s0 and the host
#                                        static lands on br-ex. enp1s0 becomes an L2-only
#                                        OVS member; ovn-chassis consumes br-ex via
#                                        bridge-interface-mappings (MAC) + ovn-bridge-
#                                        mappings physnet1:br-ex -- it does NOT build its
#                                        own bridge, and the API LXD containers' 'public'
#                                        endpoint attaches to this same OVS br-ex.
#   enp7s0 --> br-metal (standard bridge) + STATIC 10.12.8.N  (metal-admin)
#              br-metal.103 (vlan, VID 103)
#                --> br-internal (standard bridge) + STATIC 10.12.12.N (metal-internal)
#   enp8s0   raw  + STATIC 10.12.16.N   (data-tenant)
#   enp9s0   raw  + STATIC 10.12.32.N   (storage;     Juju auto-bridges at deploy)
#   enp10s0  raw  + STATIC 10.12.36.N   (replication; Juju auto-bridges at deploy)
#   enp11s0  idle (ex-lbaas; no link)
#
# All ids resolved live: system_id by hostname, interface id by name, subnet id and
# VLAN object id by CIDR. Idempotent: skips a bridge/vlan/link that already exists.
# Requires the host to be Ready (link-subnet/update are rejected on Deployed).
#
# DC selector (opt-in, DOCFIX-166): set DC=vr0-dc0|vr1-dc0|vr1-dc1 to call
# lib_net_select_dc/lib_hosts_select_dc explicitly. Unset (default) == DC0's
# real plane scheme + enrolled hosts, unchanged. vr1-dc0 no-ops at the
# network layer; vr1-dc1 selects its ratified planes (D-124 amendment);
# BOTH then FAIL at the host layer (no per-DC HOST_OCTET/HOST_BOOT_MAC
# yet) -- see scripts/lib-net.sh / scripts/lib-hosts.sh.
#
# Exit: 0 ok | 1 fatal | 2 warning

set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib-net.sh
. "$SCRIPT_DIR/lib-net.sh"
# shellcheck source=scripts/lib-hosts.sh
. "$SCRIPT_DIR/lib-hosts.sh"

# --- $DC selector (opt-in; tooling gap register #15 sub-item 2, DOCFIX-166) ---
# Same convention as lib-net.sh/lib-hosts.sh's own selectors (DOCFIX-151) and the
# dc-dc-phase3 runbook's Step 1: an explicit $DC env var, never an inferred
# default. Unset/empty $DC changes NOTHING -- this script runs exactly as it
# always has, implicitly against DC0/VR0's real plane scheme and enrolled
# hosts (backward compatible by construction). Set DC=vr0-dc0|vr1-dc0|vr1-dc1 to select (D-119)
# explicitly; the selectors' own fail-loud behavior becomes this script's own
# exit code via set -e. VR1 DCs are REFUSED even though lib-hosts' VR1 arms
# are populated (2026-07-23): this script builds the VR0 Pattern-A tree
# (br-metal + VLAN-103 br-internal stack), which D-133 SUPERSEDES for VR1 --
# VR1 DC nodes carve every plane FLAT on its dedicated NIC (executed via the
# MAAS API at Stage 4; capture docs/audit/stage4-carve-verify-20260723.txt).
DC="${DC:-}"
if [ -n "$DC" ]; then
  case "$DC" in vr1-*)
    echo "FAIL: $DC refused -- this script carves the VR0 Pattern-A VLAN-103 stack, superseded for VR1 by D-133 (flat per-NIC carve, applied at Stage 4). There is nothing for it to do on a VR1 fleet." >&2
    exit 1 ;; esac
  lib_net_select_dc "$DC"
  lib_hosts_select_dc "$DC"
fi

MAAS_PROFILE="${MAAS_PROFILE:-admin}"
FATAL=0
fail() { echo "FAIL: $*" >&2; FATAL=$((FATAL+1)); }
note() { echo "NOTE: $*"; }
hdr()  { echo; echo "=== $* ==="; }

usage() { echo "usage: $0 <hostname> [--apply]" >&2; exit 1; }

HN="${1:-}"; [ -n "$HN" ] || usage
MODE="dryrun"; [ "${2:-}" = "--apply" ] && MODE="apply"

# validate hostname is one of ours
ok=0; for h in "${HOSTS[@]}"; do [ "$h" = "$HN" ] && ok=1; done
[ "$ok" = 1 ] || { echo "ERROR: '$HN' is not one of: ${HOSTS[*]}" >&2; exit 1; }

need_jq || exit 1
OCTET="${HOST_OCTET[$HN]}"

# ---- live resolvers (read-only; safe in both modes) -----------------------
maas_q() { maas "$MAAS_PROFILE" "$@"; }

SID="$(host_sysid "$HN" || true)"
[ -n "$SID" ] || { fail "$HN is not enrolled in MAAS"; exit 1; }

STATUS="$(maas_q machine read "$SID" 2>/dev/null | jq -r '.status_name // "?"')"
[ "$STATUS" = "Ready" ] || { fail "$HN ($SID) is '$STATUS', not Ready -- interface edits are rejected unless Ready/Broken"; exit 1; }

# subnet id + vlan object id, resolved BY CIDR (drift-proof)
SUBNETS_JSON="$(maas_q subnets read)"
subid_of()  { printf '%s' "$SUBNETS_JSON" | jq -r --arg c "$1" '.[]|select(.cidr==$c)|.id' | head -1; }
vlanid_of() { printf '%s' "$SUBNETS_JSON" | jq -r --arg c "$1" '.[]|select(.cidr==$c)|(.vlan.id // .vlan)' | head -1; }
vlanvid_of(){ printf '%s' "$SUBNETS_JSON" | jq -r --arg c "$1" '.[]|select(.cidr==$c)|(.vlan.vid // empty)' | head -1; }

# plane CIDRs (verified set; sourced order from lib-net PLANE_CIDRS)
C_PROV="10.12.4.0/22"; C_METAL="10.12.8.0/22"; C_INT="$METAL_INTERNAL_CIDR"  # 10.12.12.0/22
C_DATA="10.12.16.0/22"; C_STOR="10.12.32.0/22"; C_REPL="10.12.36.0/22"

# assert all planes resolve, and the tagged plane is really VID 103
for c in "$C_PROV" "$C_METAL" "$C_INT" "$C_DATA" "$C_STOR" "$C_REPL"; do
  [ -n "$(subid_of "$c")" ] || { fail "no MAAS subnet for $c"; }
  [ -n "$(vlanid_of "$c")" ] || { fail "no VLAN for $c"; }
done
[ "$FATAL" = 0 ] || exit 1
gotvid="$(vlanvid_of "$C_INT")"
[ "$gotvid" = "$METAL_INTERNAL_VID" ] || { fail "metal-internal $C_INT is VID '$gotvid', expected $METAL_INTERNAL_VID"; exit 1; }

# interface id by name (live)
ifid_of() { maas_q interfaces read "$SID" | jq -r --arg n "$1" '.[]|select(.name==$n)|.id' | head -1; }
# is interface (by name) already linked to a given cidr?
linked_to() {
  maas_q interfaces read "$SID" \
    | jq -e --arg n "$1" --arg c "$2" '.[]|select(.name==$n)|.links[]?|select(.subnet.cidr==$c)' >/dev/null 2>&1
}

# ---- mutation emitter ------------------------------------------------------
# emit "<human desc>" maas <args...>   (runs in apply; prints WOULD in dryrun)
emit() {
  local desc="$1"; shift
  if [ "$MODE" = "apply" ]; then
    echo "  DO: $desc"
    local out
    if ! out="$(maas "$MAAS_PROFILE" "$@" 2>&1)"; then
      fail "$desc"
      echo "       MAAS said: $(printf '%s' "$out" | grep -viE '^(Success|Machine-readable)' | head -3 | tr '\n' ' ')" >&2
      # FIXED 2026-07-10 (full-project sweep): this used to `return 1` here.
      # Every call site below invokes emit() as a bare, unguarded statement,
      # and this file runs under `set -euo pipefail` -- a bare function call
      # returning non-zero terminates the WHOLE script immediately, before
      # the FATAL-accumulator's own "Summary: N fatal" line (and the exit-1
      # gate that follows it) ever runs. `fail()` two lines up already does
      # the accounting this function's caller needs; nothing downstream
      # checks emit()'s own return value, so returning non-zero here bought
      # nothing except silently truncating apply-mode runs on the FIRST
      # MAAS error instead of reporting the full carve-attempt picture.
      # Do not reintroduce `return 1` here without also either wrapping
      # every call site in `|| true` or removing `set -e` for this function.
    fi
  else
    echo "  WOULD: $desc"
    echo "         maas $MAAS_PROFILE $*"
  fi
}

# release_self_indexed <ip> <subid>: release a DISCOVERED (alloc_type 6) record on
# <ip> so a STATIC can take it. <ip> is THIS host's architecturally-indexed metal
# address (10.12.12.<octet> from HOST_OCTET), so a discovered observation on it is this
# host's own commissioning ghost. SAFETY: refuse if ANY source positively identifies a
# DIFFERENT owner -- the StaticIPAddress node_summary.system_id (when present) must equal
# this host's SID, and the discoveries-table MAC (when present) must equal this host's
# boot MAC. Absent positive foreign identification, releasing a DISCOVERED (advisory)
# observation on this host's own indexed IP is safe.
# (node_summary.system_id is often EMPTY on a fresh discovered record -- that is why the
# earlier system_id-only gate silently no-op'd and the metal static had to be released by
# hand. See troubleshooting appendix / changelog.)
release_self_indexed() {
  local ip="$1" subid="$2" rec sid_owner disc_mac basis
  rec="$(maas_q subnet ip-addresses "$subid" 2>/dev/null \
          | jq -c --arg ip "$ip" '.[]|select(.ip==$ip and .alloc_type==6)' | head -1)"
  [ -z "$rec" ] && return 0          # no discovered record -> nothing to do
  sid_owner="$(printf '%s' "$rec" | jq -r '.node_summary.system_id // empty')"
  if [ -n "$sid_owner" ] && [ "$sid_owner" != "$SID" ]; then
    fail "$ip is DISCOVERED by a different node ($sid_owner), not $HN -- refusing (real conflict)"; return 1
  fi
  disc_mac="$(maas_q discoveries read 2>/dev/null | jq -r --arg ip "$ip" '.[]?|select(.ip==$ip)|.mac_address' | head -1)"
  if [ -n "$disc_mac" ] && [ "$disc_mac" != "${HOST_BOOT_MAC[$HN]}" ]; then
    fail "$ip observed from MAC $disc_mac, not $HN boot MAC ${HOST_BOOT_MAC[$HN]} -- refusing (real conflict)"; return 1
  fi
  basis="this host's indexed metal IP"
  [ "$sid_owner" = "$SID" ] && basis="system_id match ($SID)"
  [ -n "$disc_mac" ] && [ "$disc_mac" = "${HOST_BOOT_MAC[$HN]}" ] && basis="boot-MAC match ($disc_mac)"
  note "releasing DISCOVERED $ip (basis: $basis)"
  emit "release DISCOVERED $ip" ipaddresses release ip="$ip" force=true discovered=true
}

hdr "$HN ($SID)  octet=.$OCTET  mode=$MODE"
echo "resolved subnet/vlan ids (by CIDR):"
printf "  provider  %s sub=%s vlan=%s\n" "$C_PROV"  "$(subid_of "$C_PROV")"  "$(vlanid_of "$C_PROV")"
printf "  metal     %s sub=%s vlan=%s\n" "$C_METAL" "$(subid_of "$C_METAL")" "$(vlanid_of "$C_METAL")"
printf "  internal  %s sub=%s vlan=%s (vid %s)\n" "$C_INT" "$(subid_of "$C_INT")" "$(vlanid_of "$C_INT")" "$gotvid"
printf "  data      %s sub=%s vlan=%s\n" "$C_DATA" "$(subid_of "$C_DATA")" "$(vlanid_of "$C_DATA")"
printf "  storage   %s sub=%s vlan=%s\n" "$C_STOR" "$(subid_of "$C_STOR")" "$(vlanid_of "$C_STOR")"
printf "  replicat  %s sub=%s vlan=%s\n" "$C_REPL" "$(subid_of "$C_REPL")" "$(vlanid_of "$C_REPL")"

# helper: link a RAW physical NIC -> move to plane VLAN, then STATIC link
carve_raw() {
  local nic="$1" cidr="$2" ip="$3"
  local id vlan sub
  id="$(ifid_of "$nic")"
  [ -n "$id" ] || { fail "$nic not found on $HN"; return 1; }
  if linked_to "$nic" "$cidr"; then note "$nic already STATIC on $cidr -- SKIP"; return 0; fi
  vlan="$(vlanid_of "$cidr")"; sub="$(subid_of "$cidr")"
  emit "$nic(id=$id) -> VLAN $vlan ($cidr)" interface update "$SID" "$id" vlan="$vlan"
  emit "$nic(id=$id) -> STATIC $ip on subnet $sub" interface link-subnet "$SID" "$id" mode=STATIC subnet="$sub" ip_address="$ip"
}

# Pattern A helper: build the MAAS-managed OVS bridge br-ex on the provider uplink and put
# the host static on br-ex. enp1s0 becomes an L2-only OVS member; ovn-chassis then consumes
# br-ex (bridge-interface-mappings MAC + ovn-bridge-mappings physnet1:br-ex) -- it does NOT
# build its own bridge, and the API LXD containers' 'public' endpoint shares this same OVS
# br-ex. (Reverts the D-057 raw-uplink Pattern B, which let Juju build a competing Linux
# bridge br-enp1s0 that captured enp1s0 and starved OVS br-ex, darkening the floating-IP
# plane.) Idempotent.
build_ovs_brex() {
  local nic="$1" cidr="$2" ip="$3" id lid brid pvlan
  id="$(ifid_of "$nic")"; [ -n "$id" ] || { fail "$nic not found on $HN"; return 1; }
  pvlan="$(vlanid_of "$cidr")"; [ -n "$pvlan" ] || { fail "no VLAN for provider $cidr"; return 1; }
  # 1) clear the commissioning link(s) so the host L3 lands on br-ex, not the member NIC
  for lid in $(maas_q interfaces read "$SID" | jq -r --arg n "$nic" '.[]|select(.name==$n)|.links[]?|select(.subnet!=null)|.id'); do
    emit "unlink $nic(id=$id) commissioning link id=$lid (L3 moves to br-ex)" interface unlink-subnet "$SID" "$id" id="$lid"
  done
  # 2) create the OVS bridge br-ex on the provider uplink (skip if it already exists).
  #    Commissioning leaves the uplink on an auto-fabric, NOT the provider VLAN; a MAAS
  #    bridge inherits its parent's VLAN at create, so move the uplink onto the provider
  #    VLAN first or the br-ex static below lands on the wrong plane. Mirrors carve_raw's
  #    vlan-first placement (restores the move the pre-D-060 Pattern B helper performed).
  #    Done inside this guard so a re-run (br-ex already a bridge member) never attempts
  #    a VLAN update on an enslaved NIC.
  if [ -z "$(ifid_of br-ex)" ]; then
    emit "$nic(id=$id) -> VLAN $pvlan ($cidr)" interface update "$SID" "$id" vlan="$pvlan"
    emit "create br-ex (OVS) parent=$nic(id=$id)" interfaces create-bridge "$SID" name=br-ex bridge_type=ovs parent="$id"
  else note "br-ex exists -- SKIP create"; fi
  [ "$MODE" = apply ] && brid="$(ifid_of br-ex)" || brid="<br-ex-id>"
  # 3) host static on br-ex (provider-public presence)
  if ! linked_to br-ex "$cidr"; then
    emit "br-ex(id=$brid) -> STATIC $ip on subnet $(subid_of "$cidr")" \
      interface link-subnet "$SID" "$brid" mode=STATIC subnet="$(subid_of "$cidr")" ip_address="$ip"
  else note "br-ex already on $cidr -- SKIP"; fi
}

hdr "provider plane (enp1s0 -> OVS br-ex + STATIC; Pattern A)"
# Pattern A: MAAS builds the OVS bridge br-ex on enp1s0; the host static lands on br-ex and
# enp1s0 becomes an L2-only OVS member. ovn-chassis consumes br-ex at deploy via
# bridge-interface-mappings (MAC) + ovn-bridge-mappings physnet1:br-ex -- it does NOT build
# its own bridge, and the API LXD containers' 'public' endpoint attaches to this same OVS
# br-ex. (Reverts D-057's raw-uplink Pattern B, which let Juju build a competing Linux bridge
# br-enp1s0 that captured enp1s0 and starved OVS br-ex, darkening the floating-IP plane.)
build_ovs_brex enp1s0 "$C_PROV" "10.12.4.$OCTET"

hdr "metal stack (enp7s0 -> br-metal -> br-metal.103 -> br-internal)"
EID="$(ifid_of enp7s0)"; [ -n "$EID" ] || fail "enp7s0 not found"
# 1) clear enp7s0's commissioning link(s) so the IP lands on the bridge, not the member
if maas_q interfaces read "$SID" | jq -e '.[]|select(.name=="enp7s0")|.links[]?|select(.subnet!=null)' >/dev/null 2>&1; then
  for lid in $(maas_q interfaces read "$SID" | jq -r '.[]|select(.name=="enp7s0")|.links[]?|select(.subnet!=null)|.id'); do
    emit "unlink enp7s0(id=$EID) commissioning link id=$lid" interface unlink-subnet "$SID" "$EID" id="$lid"
  done
fi
# 2) br-metal (standard) on enp7s0 -- inherits enp7s0's 2_metal untagged VLAN
if [ -z "$(ifid_of br-metal)" ]; then
  emit "create br-metal (standard) parent=enp7s0(id=$EID)" interfaces create-bridge "$SID" name=br-metal bridge_type=standard parent="$EID"
else note "br-metal exists -- SKIP create"; fi
[ "$MODE" = apply ] && BMID="$(ifid_of br-metal)" || BMID="<br-metal-id>"
if ! linked_to br-metal "$C_METAL"; then
  release_self_indexed "10.12.8.$OCTET" "$(subid_of "$C_METAL")" || true
  emit "br-metal(id=$BMID) -> STATIC 10.12.8.$OCTET on subnet $(subid_of "$C_METAL")" \
    interface link-subnet "$SID" "$BMID" mode=STATIC subnet="$(subid_of "$C_METAL")" ip_address="10.12.8.$OCTET"
else note "br-metal already on $C_METAL -- SKIP"; fi
# 3) br-metal.103 (VLAN, VID 103) on br-metal
if [ -z "$(ifid_of br-metal.103)" ]; then
  emit "create br-metal.103 (VID 103, vlan obj $(vlanid_of "$C_INT")) parent=br-metal(id=$BMID)" \
    interfaces create-vlan "$SID" vlan="$(vlanid_of "$C_INT")" parent="$BMID"
else note "br-metal.103 exists -- SKIP create"; fi
[ "$MODE" = apply ] && V103="$(ifid_of br-metal.103)" || V103="<br-metal.103-id>"
# 4) br-internal (standard) on br-metal.103
if [ -z "$(ifid_of br-internal)" ]; then
  emit "create br-internal (standard) parent=br-metal.103(id=$V103)" \
    interfaces create-bridge "$SID" name=br-internal bridge_type=standard parent="$V103"
else note "br-internal exists -- SKIP create"; fi
[ "$MODE" = apply ] && BIID="$(ifid_of br-internal)" || BIID="<br-internal-id>"
if ! linked_to br-internal "$C_INT"; then
  emit "br-internal(id=$BIID) -> STATIC 10.12.12.$OCTET on subnet $(subid_of "$C_INT")" \
    interface link-subnet "$SID" "$BIID" mode=STATIC subnet="$(subid_of "$C_INT")" ip_address="10.12.12.$OCTET"
else note "br-internal already on $C_INT -- SKIP"; fi

hdr "data / storage / replication (raw + static)"
carve_raw enp8s0  "$C_DATA" "10.12.16.$OCTET"
carve_raw enp9s0  "$C_STOR" "10.12.32.$OCTET"
carve_raw enp10s0 "$C_REPL" "10.12.36.$OCTET"

hdr "enp11s0 (ex-lbaas) -- left idle by design (no link)"
note "no action on enp11s0"

# ---- verify (read-only, both modes) ---------------------------------------
hdr "resulting interface tree (live)"
maas_q interfaces read "$SID" | jq -r '
  .[] | "  \(.name)\ttype=\(.type)\tvlan=\(.vlan.fabric):\(.vlan.vid)\tlinks=\([.links[]?|{(.subnet.cidr // "none"):(.ip_address // .mode)}])"' | sort

echo
if [ "$MODE" = dryrun ]; then
  note "DRY-RUN only -- nothing changed. Re-run with --apply to execute."
fi
echo "Summary: ${FATAL} fatal"
[ "$FATAL" -gt 0 ] && exit 1
exit 0
