#!/usr/bin/env bash
# scripts/dc-plane-ipam.sh <check> <site>
#
# Site-keyed per-DC plane IPAM state for VR1. Ruled by the D-134 AMENDMENT of
# 2026-07-27 (R4): "a site-keyed reservation tool on the dc-mirror.sh /
# dc-rack-net.sh pattern (which also gives D-134 an EXECUTABLE gate instead of
# prose)". THIS FILE IS THAT GATE. Until it existed, D-134's band table lived
# only as decision prose and MAAS held ZERO reserved ranges -- a ruled decision
# with no artifact, which is the class the 2026-07-27 audit found twice.
#
# ACTIONS
#   check <site>      READ-ONLY. Compare live MAAS against the ruled expected
#                     state. Exit 0 only if every assertion passes.
#   carve-v6 <site>   Create the six v6 plane subnets, each on the SAME MAAS vlan
#                     as its v4 twin (R2 dual-stack). DRY unless --commit.
#   reserve <site>    Create the D-134 reserved bands + the FIP pool (R4).
#                     DRY unless --commit.
#
# The read-only gate was shipped and RUN FIRST, deliberately (hard rule 3), so the
# mutations are planned against a MEASURED baseline rather than an assumed one.
# Both mutating actions are DRY BY DEFAULT, IDEMPOTENT, and READ BACK every write
# -- this repo has been bitten by a script whose `apply` always silently dry-ran.
#
# EXPECTED STATE IS DERIVED, NEVER HARDCODED (hard rule 3):
#   v4 plane CIDRs  <- scripts/lib-net.sh via lib_net_select_dc  (site-keyed)
#   v6 plane /64s   <- the NetBox APEX record (office1-netbox is the VR1 working
#                      apex, DOCFIX-195). D-136 option (D), ADOPTED 2026-07-27,
#                      makes the apex the source these values come from; reading
#                      it here rather than carrying a second hand-maintained
#                      table is that adoption applied.
#   band table      <- the D-134 2026-07-23 AMENDMENT (the amendment is
#                      authoritative; the original table above it is SUPERSEDED).
#
# Exit: 0 all assertions pass | 1 assertion(s) failed | 2 could not evaluate.
# "Could not look" is NEVER "nothing there" -- an unreachable MAAS or an
# unreadable apex record REFUSES (exit 2) instead of reporting a clean tree.
# ASCII + LF.
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$HERE/.." && pwd)"
ACTION="${1:-}"; SITE="${2:-}"
# Shift ONLY what actually exists: `shift 2` with a single argument fails, leaving
# "$@" holding the action itself, which the option loop below then rejected as an
# unknown option instead of printing usage. Found by the harness, not by review.
if [ "$#" -gt 2 ]; then shift 2; else set --; fi
COMMIT=0
for a in "$@"; do case "$a" in --commit) COMMIT=1 ;; *) echo "FAIL: unknown option '$a'" >&2; exit 2 ;; esac; done
MAAS_PROFILE="${MAAS_PROFILE:-admin}"

usage() {
  cat >&2 <<EOF
usage: $(basename "$0") <check|carve-v6|reserve|retire-v6-ula> <site> [--commit]

  check      READ-ONLY. Compare live MAAS against the ruled expected state.
  carve-v6   Create the six v6 plane subnets on their v4 twin's fabric (R2).
  reserve    Create the D-134 reserved bands, + the FIP pool (R4).
  retire-v6-ula
             D-139 step 6, MAAS half: DELETE this site's retired ULA v6 plane
             subnets. REFUSES on any subnet that still holds an allocation or a
             machine-interface link, NAMING what holds it. Deletes one at a
             time, never in a loop, and reads back that each is gone.

  DRY BY DEFAULT. Without --commit nothing is written: the planned calls are
  printed and nothing else. With --commit each write is READ BACK and verified,
  per the maas-node-power.sh precedent. Both mutating actions are IDEMPOTENT --
  an item already in the ruled state is SKIPPED, not rewritten.
  site: vr1-dc0 | vr1-dc1   (region-qualified per D-119; bare dc0/dc1 are RETIRED)
env:  MAAS_PROFILE (default: admin)
      APEX_RECORD  (default: newest netbox/draft/vr1-office1-current-*.json)
EOF
  exit 2
}

case "$ACTION" in check|carve-v6|reserve|retire-v6-ula) ;; *) usage ;; esac
case "$SITE" in
  vr1-dc0|vr1-dc1) ;;
  dc0|dc1|dc2)
    echo "FAIL: bare '$SITE' is RETIRED (D-119) -- it was AMBIGUOUS ACROSS REGIONS." >&2
    echo "      Use the region-qualified selector: vr1-dc0 | vr1-dc1" >&2
    exit 2 ;;
  *) usage ;;
esac

PASS=0; FAIL=0
ok()  { PASS=$((PASS+1)); printf '  [ok]   %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf '  [FAIL] %s\n' "$1"; }
refuse() { echo; echo "REFUSE: $1" >&2; echo "  (could not evaluate -- this is NOT a pass)" >&2; exit 2; }

# ---- D-134 AMENDMENT 2026-07-23 band table (the authoritative one) ------------
# Host-part offsets within each plane. Row 0 of every VR1 DC plane, BOTH DCs.
#   .1-.3     infrastructure (.1 gw, .2 rack, .3 DNS forwarder on metal-admin)
#   .4-.49    RESERVED utility/infra nodes
#   .50-.99   VIP band   (widened from .50-.60 by the R11 D-020 amendment)
#   .100-.200 nodes      (control .100-.119 / compute .120-.149 / storage .150-.200)
#   .201-.254 dynamic    (MOVED here from .100-.200 by the amendment)
BAND_UTIL_LO=4;    BAND_UTIL_HI=49
BAND_VIP_LO=50;    BAND_VIP_HI=99
BAND_DYN_LO=201;   BAND_DYN_HI=254

# ---- v4 planes from lib-net (site-keyed) -------------------------------------
# shellcheck disable=SC1091
source "$REPO/scripts/lib-net.sh" >/dev/null 2>&1 || refuse "cannot source scripts/lib-net.sh"
lib_net_select_dc "$SITE" >/dev/null 2>&1 || refuse "lib_net_select_dc '$SITE' failed"
[ "${#PLANE_CIDRS[@]}" -gt 0 ] || refuse "lib-net produced no PLANE_CIDRS for $SITE"

# ---- v6 planes from the APEX record ------------------------------------------
APEX_RECORD="${APEX_RECORD:-$(ls -1t "$REPO"/netbox/draft/vr1-office1-current-*.json 2>/dev/null | head -1)}"
[ -n "$APEX_RECORD" ] && [ -r "$APEX_RECORD" ] \
  || refuse "no readable apex record (looked for netbox/draft/vr1-office1-current-*.json; set APEX_RECORD)"

V6_MAP="$(python3 - "$APEX_RECORD" "$SITE" <<'PY'
import json, sys
rec, site = sys.argv[1], sys.argv[2]
try:
    d = json.load(open(rec))
except Exception as e:
    sys.stderr.write("apex record unreadable: %s\n" % e); sys.exit(3)
out = []
for p in d.get('ipam/prefixes', []):
    pre = p.get('prefix', '')
    if ':' not in pre or not pre.endswith('/64'):
        continue                      # /60 and /48 are parent blocks, not planes
    sc = p.get('scope') or {}
    if (sc.get('name') or p.get('scope_site') or '') != site:
        continue
    role = p.get('role') or {}
    role = role.get('slug') if isinstance(role, dict) else role
    desc = (p.get('description') or '')
    # the provider VIP /64 is a SEPARATE block from the provider node /64; it is
    # not a node-addressing plane, so it is reported apart rather than as a plane.
    kind = 'vip' if 'VIP' in desc else 'plane'
    out.append("%s\t%s\t%s" % (role, kind, pre))
print("\n".join(sorted(out)))
PY
)" || refuse "could not parse the apex record"
[ -n "$V6_MAP" ] || refuse "apex record carries NO /64 prefixes scoped to $SITE"

# ---- live MAAS ---------------------------------------------------------------
command -v maas >/dev/null 2>&1 \
  || refuse "the 'maas' client is ABSENT on this host. NOTE: a missing binary is a
  MISSING TOOL, not an unreachable MAAS -- the 2026-07-27 audit found three separate
  preflight/deploy failures that were all 'the client is not installed' reported as
  something else. Run this from the D-128 Plane-2 host (voffice1)."

SUBJSON="$(maas "$MAAS_PROFILE" subnets read 2>/dev/null)" \
  || refuse "'maas $MAAS_PROFILE subnets read' failed -- MAAS unreachable or profile '$MAAS_PROFILE' not logged in"
RANGEJSON="$(maas "$MAAS_PROFILE" ipranges read 2>/dev/null)" \
  || refuse "'maas $MAAS_PROFILE ipranges read' failed"

# ============================ helpers ========================================
# vlan id of the fabric carrying a v4 plane -- a v6 subnet must land on the SAME
# vlan so the plane is genuinely dual-stack on one L2, not a parallel fabric.
vlan_of() { printf '%s' "$SUBJSON" | python3 -c "
import json,sys
c=sys.argv[1]
for x in json.load(sys.stdin):
    if x['cidr']==c: print((x.get('vlan') or {}).get('id','')); break
else: print('')
" "$1"; }
subnet_id_of() { printf '%s' "$SUBJSON" | python3 -c "
import json,sys
c=sys.argv[1]
for x in json.load(sys.stdin):
    if x['cidr']==c: print(x['id']); break
else: print('')
" "$1"; }
cidr_for_role() { local r="$1" c; for c in "${PLANE_CIDRS[@]}"; do
    [ "${PLANE_NAME[$c]:-}" = "$r" ] && { printf '%s' "$c"; return 0; }; done; return 1; }
range_type() { printf '%s' "$RANGEJSON" | python3 -c "
import json,sys
lo,hi=sys.argv[1],sys.argv[2]
for r in json.load(sys.stdin):
    if r.get('start_ip')==lo and r.get('end_ip')==hi: print(r.get('type','?')); break
else: print('')
" "$1" "$2"; }

PLANNED=0; APPLIED=0; SKIPPED=0; ERRORS=0
plan() { PLANNED=$((PLANNED+1)); printf '  [plan] %s\n' "$1"; }
skip() { SKIPPED=$((SKIPPED+1)); printf '  [skip] %s\n' "$1"; }
err()  { ERRORS=$((ERRORS+1));   printf '  [ERR]  %s\n' "$1"; }
did()  { APPLIED=$((APPLIED+1)); printf '  [done] %s\n' "$1"; }

if [ "$ACTION" = "check" ]; then
echo "== dc-plane-ipam check $SITE =="
echo "   apex record: ${APEX_RECORD#$REPO/}"
echo
echo "-- v4 planes (from lib-net) --"
for c in "${PLANE_CIDRS[@]}"; do
  name="${PLANE_NAME[$c]:-?}"
  fab="$(printf '%s' "$SUBJSON" | python3 -c "
import json,sys
c=sys.argv[1]
for x in json.load(sys.stdin):
    if x['cidr']==c: print((x.get('vlan') or {}).get('fabric','?')); break
else: print('')
" "$c")"
  if [ -n "$fab" ]; then ok "v4 $name $c present (fabric=$fab)"
  else bad "v4 $name $c ABSENT from MAAS"; fi
done

echo
echo "-- v6 planes (from the apex) --"
while IFS=$'\t' read -r role kind pre; do
  [ -z "$pre" ] && continue
  present="$(printf '%s' "$SUBJSON" | python3 -c "
import json,sys
p=sys.argv[1]
print('yes' if any(x['cidr']==p for x in json.load(sys.stdin)) else 'no')
" "$pre")"
  if [ "$kind" = "vip" ]; then
    # Not asserted as a MAAS subnet: the GUA VIP /64 holds hacluster-managed API
    # VIPs, not node addresses, so MAAS never allocates from it and cannot hand
    # one out. Reported for visibility; whether it should ALSO be a MAAS subnet
    # is an open design question, deliberately not decided by this gate.
    printf '  [note] %-16s %-30s provider VIP block, MAAS subnet=%s (not asserted)\n' "$role" "$pre" "$present"
    continue
  fi
  if [ "$present" = "yes" ]; then ok "v6 $role $pre present"
  else bad "v6 $role $pre ABSENT from MAAS (R2 dual-stack is RULED; the v6 carve has not run)"; fi
done <<< "$V6_MAP"

echo
echo "-- D-134 reserved bands (v4) --"
for c in "${PLANE_CIDRS[@]}"; do
  name="${PLANE_NAME[$c]:-?}"
  base="${c%.*}"; base="${base%.*}"          # 10.12.4.0/22 -> 10.12
  third="$(printf '%s' "$c" | cut -d. -f3)"  # third octet of the /22 base
  for band in "util:$BAND_UTIL_LO:$BAND_UTIL_HI" "vip:$BAND_VIP_LO:$BAND_VIP_HI"; do
    bn="${band%%:*}"; rest="${band#*:}"; lo="${rest%%:*}"; hi="${rest##*:}"
    want_lo="$base.$third.$lo"; want_hi="$base.$third.$hi"
    got="$(printf '%s' "$RANGEJSON" | python3 -c "
import json,sys
lo,hi=sys.argv[1],sys.argv[2]
for r in json.load(sys.stdin):
    if r.get('start_ip')==lo and r.get('end_ip')==hi:
        print(r.get('type','?')); break
else: print('')
" "$want_lo" "$want_hi")"
    case "$got" in
      reserved) ok "$name $bn band $want_lo-$want_hi reserved" ;;
      "")       bad "$name $bn band $want_lo-$want_hi NOT reserved (D-134 is RULED; MAAS may hand these out mid-deploy)" ;;
      *)        bad "$name $bn band $want_lo-$want_hi exists but type='$got' (want 'reserved') -- an unrecognised state REFUSES rather than passing" ;;
    esac
  done
done

echo
echo "RESULT: pass=$PASS fail=$FAIL"
if [ "$FAIL" -eq 0 ]; then
  echo "PASS: dc-plane-ipam check $SITE"
  exit 0
fi
echo "FAIL: dc-plane-ipam check $SITE -- $FAIL assertion(s) failed"
exit 1
fi

# ======================= carve-v6 (R2 dual-stack propagation) =================
# Creates the six v6 plane subnets, each on the SAME MAAS vlan as its v4 twin so
# the plane is genuinely dual-stack on one L2 rather than a parallel fabric.
# The provider GUA VIP /64 is NOT created: it holds hacluster-managed API VIPs,
# not node addresses, so MAAS never allocates from it (see the check action).
if [ "$ACTION" = "carve-v6" ]; then
  echo "== dc-plane-ipam carve-v6 $SITE $([ "$COMMIT" -eq 1 ] && echo '--commit' || echo '(DRY RUN)') =="
  echo "   apex record: ${APEX_RECORD#$REPO/}"
  echo
  while IFS=$'\t' read -r role kind pre; do
    [ -z "$pre" ] && continue
    [ "$kind" = "vip" ] && { skip "$role $pre -- provider VIP block, deliberately not a MAAS subnet"; continue; }
    if printf '%s' "$SUBJSON" | python3 -c "
import json,sys
sys.exit(0 if any(x['cidr']==sys.argv[1] for x in json.load(sys.stdin)) else 1)" "$pre"; then
      skip "$role $pre already present"; continue
    fi
    v4="$(cidr_for_role "$role")" || { err "$role: no v4 plane in lib-net for role '$role' -- refusing to guess a fabric"; continue; }
    vlan="$(vlan_of "$v4")"
    [ -n "$vlan" ] || { err "$role: v4 twin $v4 has no vlan id in MAAS -- refusing to create $pre on an unknown fabric"; continue; }
    plan "create $pre on vlan=$vlan (the $v4 fabric) -- role $role"
    if [ "$COMMIT" -eq 1 ]; then
      if maas "$MAAS_PROFILE" subnets create cidr="$pre" vlan="$vlan" \
           description="VR1 $SITE $role (v6, R2 dual-stack; D-101/D-111 apex value)" >/dev/null 2>&1; then
        # READ BACK -- a create that reports success but did not land is the class
        # this repo has been bitten by (opnsense-plugins.sh apply always dry-ran).
        got="$(maas "$MAAS_PROFILE" subnets read 2>/dev/null | python3 -c "
import json,sys
p=sys.argv[1]
for x in json.load(sys.stdin):
    if x['cidr']==p: print((x.get('vlan') or {}).get('id','')); break
else: print('')" "$pre")"
        if [ "$got" = "$vlan" ]; then did "$pre created and READ BACK on vlan=$got"
        else err "$pre create reported success but read-back gives vlan='$got' (want '$vlan')"; fi
      else
        err "$pre create FAILED"
      fi
    fi
  done <<< "$V6_MAP"
  echo
  echo "RESULT: planned=$PLANNED applied=$APPLIED skipped=$SKIPPED errors=$ERRORS"
  [ "$COMMIT" -eq 1 ] || echo "DRY RUN -- nothing was written. Re-run with --commit to apply."
  [ "$ERRORS" -eq 0 ] || exit 1
  exit 0
fi

# ======================= reserve (D-134 bands + FIP pool, R4) =================
if [ "$ACTION" = "reserve" ]; then
  echo "== dc-plane-ipam reserve $SITE $([ "$COMMIT" -eq 1 ] && echo '--commit' || echo '(DRY RUN)') =="
  echo

  # mk <subnet_cidr> <lo_ip> <hi_ip> <comment>
  mk() {
    local sub="$1" lo="$2" hi="$3" cmt="$4" sid t
    t="$(range_type "$lo" "$hi")"
    case "$t" in
      reserved) skip "$lo-$hi already reserved"; return 0 ;;
      "")       ;;
      *)        err "$lo-$hi exists with type='$t' (want 'reserved') -- REFUSING to rewrite an existing range; resolve by hand"; return 0 ;;
    esac
    sid="$(subnet_id_of "$sub")"
    [ -n "$sid" ] || { err "subnet $sub not in MAAS -- cannot reserve $lo-$hi on it"; return 0; }
    plan "reserve $lo-$hi on $sub (subnet id=$sid) -- $cmt"
    [ "$COMMIT" -eq 1 ] || return 0
    if maas "$MAAS_PROFILE" ipranges create type=reserved subnet="$sid" \
         start_ip="$lo" end_ip="$hi" comment="$cmt" >/dev/null 2>&1; then
      RANGEJSON="$(maas "$MAAS_PROFILE" ipranges read 2>/dev/null)"
      if [ "$(range_type "$lo" "$hi")" = "reserved" ]; then did "$lo-$hi reserved and READ BACK"
      else err "$lo-$hi create reported success but read-back does not show it reserved"; fi
    else
      err "$lo-$hi create FAILED"
    fi
  }

  echo "-- v4 D-134 bands --"
  for c in "${PLANE_CIDRS[@]}"; do
    name="${PLANE_NAME[$c]:-?}"
    b="${c%.*}"; b="${b%.*}"; third="$(printf '%s' "$c" | cut -d. -f3)"
    mk "$c" "$b.$third.$BAND_UTIL_LO"  "$b.$third.$BAND_UTIL_HI" "D-134 utility band ($name)"
    mk "$c" "$b.$third.$BAND_VIP_LO"   "$b.$third.$BAND_VIP_HI"  "D-134 VIP band ($name)"
  done

  echo
  echo "-- FIP pool (R4 ruled scope: 'plus the FIP pool') --"
  # NOT DERIVED BY PATTERN. lib-net carries FIP_POOL_START/END for vr1-dc0 and
  # deliberately UNSETS them for vr1-dc1, whose OpenStack-layer values are "NOT yet
  # ruled/measured ... UNSET so any use fails loud". Inventing dc1's pool by mirroring
  # dc0's shape would be an inferred value (hard rule 2), so this REFUSES instead.
  if [ -n "${FIP_POOL_START:-}" ] && [ -n "${FIP_POOL_END:-}" ]; then
    prov="$(cidr_for_role provider-public)" \
      && mk "$prov" "$FIP_POOL_START" "$FIP_POOL_END" "FIP pool for neutron (phase-04-network-verify asserts this)" \
      || err "no provider-public plane for $SITE"
  else
    err "FIP_POOL_START/END are UNSET for $SITE in lib-net.sh -- this is DELIBERATE
       (the dc1 arm unsets the OpenStack-layer values as not-yet-ruled). R4's ruled
       scope includes the FIP pool, so R4 CANNOT be fully executed for this site until
       the pool is ruled. Refusing to infer it from dc0's shape (hard rule 2)."
  fi

  echo
  echo "-- v6 bands (mirror the v4 octet; ruled 2026-07-27) --"
  # FORCED SEQUENCING, not a deferral: a range cannot be reserved on a subnet that
  # does not exist, so this is a no-op until carve-v6 has run.
  while IFS=$'\t' read -r role kind pre; do
    if [ -z "$pre" ] || [ "$kind" = "vip" ]; then continue; fi
    if ! printf '%s' "$SUBJSON" | python3 -c "
import json,sys
sys.exit(0 if any(x['cidr']==sys.argv[1] for x in json.load(sys.stdin)) else 1)" "$pre"; then
      skip "$role $pre not in MAAS yet -- run carve-v6 first (forced sequencing)"; continue
    fi
    base="${pre%%::/64}"
    # The ruled mirror is TEXTUAL, not numeric: keystone at v4 octet .50 is `::50`,
    # i.e. the same DIGITS in the v6 host part. Converting 50 to hex would give
    # `::32` -- a different, unruled band that would still look plausible.
    #
    # MEASURED 2026-07-27: an explicit reserved range CANNOT be created here, and
    # does not need to be. MAAS auto-reserves `::1`-`::ffff:ffff` on EVERY IPv6
    # subnet (purpose "reserved"), plus `::` itself under RFC 4291 s2.6.1. The ruled
    # bands sit entirely inside that, so `ipranges create` returns
    #   "Requested reserved range conflicts with an existing range."
    # and the addresses are ALREADY protected -- far more broadly than the band.
    # So this VERIFIES the property instead of attempting a write. R4's "v6 bands
    # follow as a second pass" is satisfied by MAAS's own default, not by our range.
    sid="$(subnet_id_of "$pre")"
    if [ -z "$sid" ]; then err "$role $pre has no subnet id"; continue; fi
    covered="$(maas "$MAAS_PROFILE" subnet reserved-ip-ranges "$sid" 2>/dev/null | python3 -c "
import json,sys,ipaddress
try: d=json.load(sys.stdin)
except Exception: print('ERR'); sys.exit()
lo=ipaddress.ip_address(sys.argv[1]); hi=ipaddress.ip_address(sys.argv[2])
for r in d:
    if ipaddress.ip_address(r['start'])<=lo and ipaddress.ip_address(r['end'])>=hi:
        print('yes'); break
else: print('no')
" "${base}::${BAND_UTIL_LO}" "${base}::${BAND_VIP_HI}")"
    case "$covered" in
      yes) skip "$role $pre bands ${BAND_UTIL_LO}-${BAND_VIP_HI} already protected by MAAS's default IPv6 reservation (::1-::ffff:ffff) -- no explicit range needed or possible" ;;
      no)  err "$role $pre ruled bands are NOT inside MAAS's reserved space -- investigate before deploy" ;;
      *)   err "$role $pre could not read reserved-ip-ranges (could not look is not nothing there)" ;;
    esac
  done <<< "$V6_MAP"

  echo
  echo "RESULT: planned=$PLANNED applied=$APPLIED skipped=$SKIPPED errors=$ERRORS"
  [ "$COMMIT" -eq 1 ] || echo "DRY RUN -- nothing was written. Re-run with --commit to apply."
  [ "$ERRORS" -eq 0 ] || exit 1
  exit 0
fi

# ============================================================================
# retire-v6-ula -- D-139 execution step 6, the MAAS half
# ============================================================================
# WHY THIS EXISTS. Step 6 says retire the ULA rows "in the apex AND in MAAS".
# The apex half has a tool (netbox/d139-step6-vip-rehome.py). The MAAS half was
# executed BY HAND for dc0 on 2026-08-02 -- four `maas subnet delete` calls --
# which is precisely the "no repo tool, mutations live only in a transcript"
# gap that D-132 q1 already forced this repo to close once for the region carve.
# Without this, the dc1 rebuild cannot reproduce the dc0 end state.
#
# THE GUARD IS THE POINT, and it is not theoretical -- it stopped a real
# deletion on 2026-08-02. `fd50:840e:74e2:220::/64` looked like the other four
# but held two STICKY allocations: `::5` user `juju-vr1-dc0` (the JUJU
# CONTROLLER, i.e. the deploy client) and `::6` user `MAAS` (the REGION VM the
# delete would be issued TO), neither with a GUA counterpart. Deleting it would
# have destroyed the only recorded v6 of both. So: an allocation or an interface
# link is a REFUSAL, and the refusal NAMES the holder rather than reporting a
# bare count -- "2 allocations" does not tell you it is the controller.
#
# BOTH CHECKS ARE REQUIRED. Zero allocated IPs does NOT mean nothing references
# a subnet; an interface can link a subnet without holding an address. The dc0
# run measured both, and the zero-link result is also what independently
# confirmed D-139 step 3 had migrated all nine role nodes to GUA.
if [ "$ACTION" = "retire-v6-ula" ]; then
  # Source of truth for the retired /48 is netbox/d139-gua-carve.py's
  # RETIRED_ULA_48. Retired FOR VR1; still a valid org aggregate elsewhere.
  RETIRED_ULA_48="fd50:840e:74e2::/48"
  echo "== dc-plane-ipam retire-v6-ula: $SITE (profile $MAAS_PROFILE, ULA $RETIRED_ULA_48) =="

  MACHJSON="$(maas "$MAAS_PROFILE" machines read 2>/dev/null)" \
    || refuse "'maas $MAAS_PROFILE machines read' failed -- cannot check interface links, and
  'could not look' is NEVER 'nothing there'"

  ULA_ROWS="$(printf '%s' "$SUBJSON" | python3 -c "
import ipaddress,json,sys
ula=ipaddress.ip_network(sys.argv[1])
for x in json.load(sys.stdin):
    c=x.get('cidr','')
    try: n=ipaddress.ip_network(c)
    except ValueError: continue
    if n.version==6 and n.subnet_of(ula): print('%s %s' % (x['id'], c))
" "$RETIRED_ULA_48")"

  if [ -z "$ULA_ROWS" ]; then
    echo "  [skip] no ULA v6 subnets remain in this region -- already retired"
    echo; echo "RESULT: planned=0 applied=0 skipped=1 errors=0"; exit 0
  fi

  while read -r sid scidr; do
    [ -n "$sid" ] || continue
    ips="$(maas "$MAAS_PROFILE" subnet ip-addresses "$sid" 2>/dev/null)" \
      || { err "$scidr could not read ip-addresses -- refusing to delete on an unread subnet"; continue; }
    holders="$(printf '%s' "$ips" | python3 -c "
import json,sys
try: d=json.load(sys.stdin)
except Exception: print('UNREADABLE'); sys.exit()
print('; '.join('%s (alloc_type=%s user=%s)' % (e.get('ip'), e.get('alloc_type'), e.get('user')) for e in d))
")"
    links="$(printf '%s' "$MACHJSON" | python3 -c "
import json,sys
sid=sys.argv[1]; out=[]
for m in json.load(sys.stdin):
    for i in m.get('interface_set',[]):
        for l in i.get('links',[]):
            if str(((l.get('subnet') or {}).get('id')))==sid:
                out.append('%s:%s' % (m.get('hostname'), i.get('name')))
print('; '.join(out))
" "$sid")"

    if [ "$holders" = "UNREADABLE" ]; then
      err "$scidr ip-addresses did not parse -- refusing"; continue
    fi
    if [ -n "$holders" ] || [ -n "$links" ]; then
      err "$scidr (id=$sid) is NOT empty -- REFUSING to delete.
        allocations: ${holders:-none}
        interface links: ${links:-none}
        Re-home these onto the GUA plane first. A sticky allocation here is a LIVE host:
        on dc0 this guard caught the juju controller (::5) and the MAAS region VM (::6)."
      continue
    fi

    if [ "$COMMIT" -eq 0 ]; then
      plan "$scidr (id=$sid) EMPTY -- would delete"
      continue
    fi
    if maas "$MAAS_PROFILE" subnet delete "$sid" >/dev/null 2>&1; then
      # READ BACK. A delete that "succeeded" is not evidence the row is gone.
      if maas "$MAAS_PROFILE" subnet read "$sid" >/dev/null 2>&1; then
        err "$scidr delete returned success but the subnet is STILL READABLE (id=$sid)"
      else
        APPLIED=$((APPLIED+1)); printf '  [apply] %s (id=%s) deleted and verified gone\n' "$scidr" "$sid"
      fi
    else
      err "$scidr delete FAILED (id=$sid)"
    fi
  done <<< "$ULA_ROWS"

  echo
  echo "RESULT: planned=$PLANNED applied=$APPLIED skipped=$SKIPPED errors=$ERRORS"
  [ "$COMMIT" -eq 1 ] || echo "DRY RUN -- nothing was written. Re-run with --commit to apply."
  [ "$ERRORS" -eq 0 ] || exit 1
  exit 0
fi
