# scripts/lib-net.sh
#
# Single source of truth for v1 (VR0 / Baldurkeep) pinned network values.
# SOURCED by discovery / verify scripts -- not executed directly. ASCII + LF.
# Authoritative per D-052 / D-053 (matches bundle.yaml at committed HEAD).
# Contains constants + tiny read-only helpers ONLY. No mutations, no state changes.
#
# CIDR is the stable key throughout: MAAS subnet IDs drift across cutovers
# (the D-052 cutover moved metal-internal to id=10, not the old id=6), so every
# lookup resolves BY CIDR, never by a hardcoded subnet ID. (PATTERN-1.)

# shellcheck shell=bash
# shellcheck disable=SC2034  # constants consumed by sourcing scripts

# Guard: refuse to run directly (it is a library).
if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
  echo "lib-net.sh is a sourced library; do not run it directly." >&2
  exit 2
fi

# --- The six MAAS spaces / planes (D-052 / D-053). ---
PLANE_CIDRS=( "10.12.4.0/22" "10.12.8.0/22" "10.12.12.0/22" "10.12.16.0/22" "10.12.32.0/22" "10.12.36.0/22" )
declare -A PLANE_NAME=(
  ["10.12.4.0/22"]="provider-public"
  ["10.12.8.0/22"]="metal-admin"
  ["10.12.12.0/22"]="metal-internal"
  ["10.12.16.0/22"]="data-tenant"
  ["10.12.32.0/22"]="storage"
  ["10.12.36.0/22"]="replication"
)
SPACES6=( provider-public metal-admin metal-internal data-tenant storage replication )

# Names that MUST be gone after the D-052 / D-053 cutover (deploy fails or mis-binds if any reappear).
STALE_SPACES=( provider metal data fabric-data lbaas provider-vip )

# Gateways. VR0 AS-BUILT: provider-public and metal-admin both route; the other four
# are gw=none. 10.12.8.1 is VR0's REAL as-built value (phase-00-maas-standup.sh:119) and
# stays here for the vr0-dc0 arm. It does NOT hold for either VR1 DC -- see the per-arm
# overrides below (D-134; measured 2026-07-27).
declare -A PLANE_GW=( ["10.12.4.0/22"]="10.12.4.1" ["10.12.8.0/22"]="10.12.8.1" )

# The four non-API, non-PXE planes whose host NICs MAAS must have provisioned.
DATA_PLANE_CIDRS=( "10.12.12.0/22" "10.12.16.0/22" "10.12.32.0/22" "10.12.36.0/22" )

# metal-internal is a TAGGED VLAN bridged on the metal fabric; host links land on br-internal.
METAL_INTERNAL_CIDR="10.12.12.0/22"
METAL_INTERNAL_VID="103"
METAL_INTERNAL_IFACE="br-internal"

# Host identity (hostnames, octets, boot MACs, system_id resolution) now lives in
# scripts/lib-hosts.sh, keyed by HOSTNAME -- system_ids are re-minted on every
# (re-)enrollment, so the old SYSID-keyed maps here were a landmine and were retired
# (DOCFIX-040). Source lib-hosts.sh for HOSTS / HOST_OCTET / host_sysid().

# Triple HA VIPs (D-020 + D-052): each API charm carries provider/admin/internal columns,
# matching last octet, in the .50-.60 band. 11 clustered API charms.
VIP_PREFIX_PROVIDER="10.12.4"   # provider-public leg (public API VIPs share the FIP plane; Pattern A)
VIP_PREFIX_ADMIN="10.12.8"
VIP_PREFIX_INTERNAL="10.12.12"
VIP_OCTET_MIN=50
# D-020 AMENDMENT / R11 (RULED 2026-07-27): band widened 60 -> 99 and the expected
# count 11 -> 13 to admit vault .61 and designate .62, both RULED-BUT-NOT-BUILT --
# so pre-flight-checks CHECK 1 reports 11 against an expected 13 until those VIPs
# land in the deploy artifacts. That is the ruled gate reporting real work owed, not
# a regression. NOTE these two constants are SEPARATELY NAMED from
# provider-bundle-check.py's own OCTET_LO/OCTET_HI + EXPECT_PUBLIC_VIP -- a two-file
# change, and EXPECT_PUBLIC_VIP deliberately STAYS 11 (neither vault nor designate
# carries a `public` binding, so they do not join that count).
VIP_OCTET_MAX=99
VIP_COUNT_EXPECT=13

# D-003 provider FIP allocation pool: a RESERVED iprange on the provider-ext subnet
# so neutron owns it without colliding with a MAAS auto-static primary (KI-P3-001).
# Single source of truth for phase-04-network-{create,verify} (create stays
# env-overridable via FIP_START/FIP_END; verify pins to these).
FIP_POOL_START="10.12.5.0"
FIP_POOL_END="10.12.7.254"

# Keystone PUBLIC endpoint host (provider VIP, D-020/D-052 -- a .50-.60 provider VIP).
# Single source of truth for the KEYSTONE_VIP / KEYSTONE_HOSTPORT defaults across the
# tenant-* family + phase-03/phase-06 scripts (each stays env-overridable). Roosevelt:
# change here once, not in 6 scripts.
KEYSTONE_VIP_DEFAULT="10.12.4.50"

# --- $DC selector convention (2026-07-09, tooling gap register #1) ---
#
# Backward compatible by construction: sourcing this file with no further
# action continues to populate PLANE_CIDRS / PLANE_NAME / PLANE_GW / etc.
# exactly as above (VR0/DC0's real, measured values) -- every existing
# script that sources lib-net.sh keeps working completely unchanged.
#
# DC-aware scripts call `lib_net_select_dc "$DC"` ONCE, immediately after
# sourcing this file, before using any of the flat variables. This mirrors
# the $REPO convention (DOCFIX-139/141): one explicit, measured selector,
# never an inferred default.
#
# SELECTORS ARE REGION-QUALIFIED (D-119, ADOPTED 2026-07-14). The selector
# string IS the NetBox apex site slug -- vr0-dc0 / vr1-dc0 / vr1-dc1 -- so the
# shell, the OpenTofu modules and the IPAM authority all speak ONE namespace.
#
# WHY, because a bare `dcN` looks harmless: before D-119, `dc0` meant VR0's
# LIVE testcloud HERE, but VR1's FIRST DC in the NetBox importer. One string,
# two clouds, one of them in production. A bare dcN is therefore REJECTED --
# accepting it "for compatibility" would preserve exactly the ambiguity D-119
# exists to delete.
lib_net_select_dc() {
  local dc="${1:?usage: lib_net_select_dc <vr0-dc0|vr1-dc0|vr1-dc1>}"
  # ONE selection per shell (the convention above: call ONCE, right after
  # sourcing). Mechanically enforced since the vr1-dc1 arm DIVERGES (2026-07-21):
  # the vr0-dc0/vr1-dc0 arms are no-ops over the file's flat defaults, so
  # CHANGING selection after a diverging arm ran would leave stale cross-DC
  # values in scope -- refuse instead of silently mixing DCs. Re-selecting the
  # SAME DC stays a no-op; re-source the lib to genuinely switch.
  if [ -n "${_LIBNET_DC_SELECTED:-}" ] && [ "$_LIBNET_DC_SELECTED" != "$dc" ]; then
    echo "FAIL: DC already selected as '$_LIBNET_DC_SELECTED' in this shell -- one selection per shell; re-source lib-net.sh to switch DC" >&2
    return 1
  fi
  case "$dc" in
    vr0-dc0)
      : # VR0's DC0 -- the LIVE testcloud. The literals sourced above ARE its
        # real, measured values. Explicit no-op, not a guess.
      ;;
    vr1-dc0)
      # VR1's FIRST DC. Same PLANE values as vr0-dc0, but for a DIFFERENT REASON:
      # D-101 rules that VR1's first DC INHERITS VR0 DC0's v4 layout UNCHANGED.
      # This is a SEPARATE case arm ON PURPOSE. The day D-101 stops holding,
      # this arm diverges and vr0-dc0's must not. Folding the two arms together
      # would encode the coincidence and lose the reason -- and the reason is
      # the thing that changes (VR1 DC0 gains its own v6 per the family matrix).
      #
      # THAT DAY ARRIVED for two groups (executed 2026-07-29). The arm is no longer
      # a pure no-op: D-101's inheritance is value-for-value only where the value is
      # still TRUE for VR1.
      #
      # (1) METAL_INTERNAL_VID=103 / METAL_INTERNAL_IFACE=br-internal are VR0 CARVE
      #     ARTIFACTS ONLY. D-133 ADOPTED: VR1 DC nodes carve every plane FLAT on its
      #     dedicated NIC, metal-internal untagged on enp3s0, "no br-metal.103 /
      #     br-internal stack exists in VR1. VID 103 is a VR0-only fact." D-133's own
      #     "Affected surfaces" list names THIS arm as owing an explicit unset, WITH its
      #     harness, at the carve delivery -- that is this. Leaving them set made every
      #     DC-aware consumer inherit an expectation that can never pass on a VR1 DC
      #     (pre-flight-checks CHECK 3's VID assertion was exactly that failure).
      #     UNSET, not re-pointed: they must fail loud under `set -u`, never quietly
      #     carry a substitute for a fact that does not exist. Mirrors the vr1-dc1 arm.
      unset METAL_INTERNAL_VID METAL_INTERNAL_IFACE
      # (2) metal-admin has NO ROUTER in either VR1 DC. MEASURED 2026-07-27 -- cited by
      #     PHRASE, not by line offset: grep "metal-admin has no router" in
      #     docs/audit/stage5-committee-raw-20260727.md, and "held by NOTHING" in
      #     docs/CURRENT-STATE.md, which reconciles it.
      #     `maas admin subnets read` returns gateway
      #     `none` for 10.12.8.0/22, and `ping 10.12.8.1` from the dc0 rack is 100%
      #     loss with an INCOMPLETE ARP entry -- the address is held by NOTHING.
      #     Control: 10.12.4.1 answers 0% loss and carries the rack default route.
      #     MAAS is RIGHT and the inherited pin was the defect. Consistent with the
      #     D-134 carve, which rules `.1` gateways for the PROVIDER subnets only;
      #     VR1's edges carry a provider-public LAN leg and nothing on metal-admin.
      #     A whole-array assignment RESETS an associative array in bash, so this
      #     drops the 10.12.8.0/22 key rather than adding to it.
      PLANE_GW=( ["10.12.4.0/22"]="10.12.4.1" )
      ;;
    vr1-dc1)
      # VR1's SECOND DC -- literals RATIFIED by the D-124 AMENDMENT (2026-07-21)
      # and apex-VERIFIED against office1-netbox the same day (all six planes
      # exist scoped dcim.site:vr1-dc1 -- docs/audit/dc1-apex-confirm-20260721.txt),
      # which is this arm's own landing rule (NetBox assigns, then the literals
      # enter the lib). DIVERGES from dc0 BY DESIGN: contiguous /22s in the
      # 10.12.64.0/19 supernet (D-115), dc0's role ORDER kept -- dc0's
      # 4/8/12/16/32/36 offsets cannot fit inside a /19. Values-of-record twin:
      # opentofu/variables.tf `vr1_dc1_planes` (change one, change BOTH in the
      # same commit).
      PLANE_CIDRS=( "10.12.64.0/22" "10.12.68.0/22" "10.12.72.0/22" "10.12.76.0/22" "10.12.80.0/22" "10.12.84.0/22" )
      PLANE_NAME=(
        ["10.12.64.0/22"]="provider-public"
        ["10.12.68.0/22"]="metal-admin"
        ["10.12.72.0/22"]="metal-internal"
        ["10.12.76.0/22"]="data-tenant"
        ["10.12.80.0/22"]="storage"
        ["10.12.84.0/22"]="replication"
      )
      # provider-public gw 10.12.64.1 is the RULED edge LAN gateway (D-124 amendment).
      # metal-admin's 10.12.68.1 was REMOVED 2026-07-29: it came from the D-120 ".1 site
      # gateway" convention, not from a ruling or a measurement, and MEASUREMENT refutes
      # it -- `maas admin subnets read` returns gateway `none` for 10.12.68.0/22 and
      # `ping 10.12.68.1` from the dc1 rack is 100% loss with a FAILED ARP entry
      # (grep "metal-admin has no router" in docs/audit/stage5-committee-raw-20260727.md;
      # docs/CURRENT-STATE.md records the dc0 twin -- grep "held by NOTHING" -- and
      # predicts this one "will fail identically"). D-134's
      # carve rules `.1` gateways for the PROVIDER subnets only. Same change as the
      # vr1-dc0 arm above, for the same reason.
      PLANE_GW=( ["10.12.64.0/22"]="10.12.64.1" )
      DATA_PLANE_CIDRS=( "10.12.72.0/22" "10.12.76.0/22" "10.12.80.0/22" "10.12.84.0/22" )
      METAL_INTERNAL_CIDR="10.12.72.0/22"
      # FIP pool RULED 2026-07-27 (GA-R5, operator: "Rule dc1 FIP pool
      # 10.12.65.0-10.12.67.254"; recorded on the D-134 R4 amendment). It sits inside
      # provider-public 10.12.64.0/22, is 767 addresses -- the SAME size as dc0's
      # 10.12.5.0-10.12.7.254, so the DCs stay structurally symmetric -- and does not
      # overlap the D-134 .64.4-.49 / .64.50-.99 bands. Consumed by
      # scripts/dc-plane-ipam.sh reserve and asserted by phase-04-network-verify.sh:100.
      FIP_POOL_START="10.12.65.0"
      FIP_POOL_END="10.12.67.254"
      # --- R9 group: the OpenStack-layer VIP literals (item 3.6 residue) ----------
      # POPULATED 2026-07-29. These were unset "until ruled"; R9 (the D-119 AMENDMENT,
      # RULED 2026-07-27) ruled them, and overlays/vr1-dc1-vips.yaml is the ratified,
      # deploy-input copy the ruling names as the single AUTHORED source. Leaving them
      # unset was costing more than it bought: `lib_net_select_dc vr1-dc1` left
      # KEYSTONE_VIP_DEFAULT unbound, so every selector-CALLER that touched it died
      # under `set -u` mid-run (measured 2026-07-29), while the non-callers silently
      # used dc0's 10.12.4.50 -- the silent half being the dangerous half, exactly as
      # R9's "MEASURED BASIS" paragraph states.
      #
      # HONEST STATUS -- do not read these as "generated": R9's ruled shape is DERIVE
      # + drift-check, and only the DRIFT half exists. The values below are HAND-COPIED
      # from overlays/vr1-dc1-vips.yaml (read 2026-07-29) and pinned against it by
      # tests/dc-selector/run-tests.sh, which re-parses the overlay and fails on
      # divergence in either direction. R9's GENERATOR half is still OWED; when it
      # lands it replaces this block and the harness becomes its drift gate.
      #   VIP_PREFIX_*      the three v4 legs of every `vip:` line, in ruled column
      #                     order provider-public / metal-admin / metal-internal.
      #   VIP_COUNT_EXPECT  13 = the `vip:` line count in that overlay (R11's ruled
      #                     11 -> 13, vault .61 + designate .62).
      #   KEYSTONE_VIP_DEFAULT  keystone's PROVIDER-PUBLIC v4 leg (.50). Same
      #                     definition as dc0's own KEYSTONE_VIP_DEFAULT=10.12.4.50:
      #                     bundle.yaml:11-13 (B5) makes the public catalog endpoint the
      #                     provider VIP. NOT settled here: R2 made these dual-family and
      #                     the overlay carries `prefer-ipv6: true`, so whether the public
      #                     catalog endpoint ultimately becomes the v6 GUA leg is a
      #                     RULING-shaped question, raised and left open on purpose.
      # VIP_OCTET_MIN/MAX are NOT unset by this arm and stay at the R11 band 50-99.
      VIP_PREFIX_PROVIDER="10.12.64"
      VIP_PREFIX_ADMIN="10.12.68"
      VIP_PREFIX_INTERNAL="10.12.72"
      VIP_COUNT_EXPECT=13
      KEYSTONE_VIP_DEFAULT="10.12.64.50"
      # --- NOT in the R9 group, and must STAY unset ------------------------------
      # The VR0 metal fabric facts (VLAN id, bridge iface) are not "unruled" -- D-133
      # ABOLISHED them for VR1 (flat per-NIC carve; metal-internal untagged on enp3s0).
      # They genuinely do not exist, so they must fail loud under `set -u` rather than
      # carry a substitute. R9's own GUARD paragraph says the same in terms: a
      # derivation that populated all nine would silently reintroduce a retired stack.
      # Consumers assert D-133's invariant instead (untagged VLAN + a `physical`
      # interface type) -- see scripts/pre-flight-checks.sh CHECK 3 / CHECK 2.
      unset METAL_INTERNAL_VID METAL_INTERNAL_IFACE
      ;;
    dc0|dc1|dc2)
      echo "FAIL: bare '$dc' is RETIRED (D-119). It was AMBIGUOUS ACROSS REGIONS: 'dc0' meant VR0's live DC0 here, but VR1's FIRST DC in the NetBox importer. Use the region-qualified selector: vr0-dc0 | vr1-dc0 | vr1-dc1" >&2
      return 1
      ;;
    *)
      echo "FAIL: unknown DC '$dc' (expected vr0-dc0, vr1-dc0, or vr1-dc1)" >&2
      return 1
      ;;
  esac
  _LIBNET_DC_SELECTED="$dc"   # only successful arms reach here (failed arms return 1)
}

# --- tiny read-only helpers ---

# need_jq: jq is required (present on the jumphost). Returns non-zero if absent.
need_jq() {
  command -v jq >/dev/null 2>&1 || { echo "FAIL: jq not found on PATH (jumphost should have it)" >&2; return 1; }
}

# fourth_octet <ip>: echo the last dotted octet of an IPv4 address.
fourth_octet() { local ip="$1"; echo "${ip##*.}"; }
