#!/usr/bin/env bash
# scripts/pre-flight-checks.sh
#
# Pre-deploy sanity gate. Read-only; no state changes. Run from inside the repo
# (or set REPO=) BEFORE 'juju add-model' / 'juju deploy'. Surfaces issues that
# would cause the deploy to fail or mis-bind during settle.
#
# Covers:
#   - repo HEAD / cleanliness (informational)
#   - octavia-pki overlay sanity (5 keys + ASCII; no key material printed)
#   - triple-VIP validator (provider/admin/internal columns, aligned, .50-.60)
#   - MAAS six-plane layout resolved BY CIDR (id/vid/gw/dns; metal-internal VID 103)
#   - per-host data/storage NIC links resolved BY CIDR (metal-internal on br-internal)
#   - the four KVM nodes Ready / power state
#
# NOT covered here (by design):
#   - juju per-model SPACE names  -> scripts/juju-spaces-check.sh (runs AFTER add-model)
#   - YAML / deploy-plan validity  -> 'juju deploy --dry-run' (phase-01 Step 1.2)
#   - OSD secondary-disk blank     -> scripts/osd-blank-check.sh (needs sudo)
#
# Pinned values come from scripts/lib-net.sh (single source of truth).
# Exit codes: 0 all checks pass | 1 fatal (do NOT deploy) | 2 warning (review then decide)

set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true
IFS=$'\n\t'

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"

FATAL=0
WARN=0
fail() { echo "FAIL: $*" >&2; FATAL=$((FATAL+1)); }
warn() { echo "WARN: $*" >&2; WARN=$((WARN+1)); }
pass() { echo "PASS: $*"; }
note() { echo "NOTE: $*"; }
hdr()  { echo; echo "=== $* ==="; }

finish() {
  echo
  echo "Summary: ${FATAL} fatal, ${WARN} warning"
  if   [ "$FATAL" -gt 0 ]; then exit 1
  elif [ "$WARN"  -gt 0 ]; then exit 2
  fi
  exit 0
}

MAAS_PROFILE="${MAAS_PROFILE:-admin}"
REPO="${REPO:-$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || dirname "$SCRIPT_DIR")}"
cd "$REPO" || { fail "cannot cd to REPO=$REPO"; finish; }

# ---------------------------------------------------------------------------
hdr "Repo (informational)"
note "REPO=$REPO"
note "HEAD: $(git --no-pager log --oneline -1 2>/dev/null || echo '(not a git tree)')"
DIRTY="$( { git status --porcelain 2>/dev/null || true; } | wc -l | tr -d ' ')"
if [ "$DIRTY" = "0" ]; then note "working tree clean"; else warn "working tree has $DIRTY modified file(s)"; fi

# ---------------------------------------------------------------------------
hdr "CHECK 0: octavia-pki overlay (no key material printed)"
OVL="overlays/octavia-pki.yaml"
if [ -f "$OVL" ]; then
  KEYS="$(grep -cE 'lb-mgmt-' "$OVL" || true)"
  if [ "$KEYS" -eq 5 ]; then pass "overlay present with 5 lb-mgmt-* keys"; else fail "overlay has $KEYS lb-mgmt-* keys (want 5)"; fi
  if LC_ALL=C grep -qP '[^\x00-\x7F]' "$OVL"; then fail "overlay contains non-ASCII bytes"; else pass "overlay ASCII clean"; fi
else
  fail "MISSING $OVL (gitignored secret; place it or regenerate via runbook Step 1.0-GEN before deploy)"
fi

# ---------------------------------------------------------------------------
hdr "CHECK 1: bundle VIPs -- TRIPLE column .${VIP_OCTET_MIN}-.${VIP_OCTET_MAX} (provider/admin/internal)"
if [ ! -f bundle.yaml ]; then
  fail "bundle.yaml not found in $REPO"
else
  VIPLINES="$(grep -cE '^[[:space:]]+vip:' bundle.yaml || true)"
  if [ "$VIPLINES" -eq "$VIP_COUNT_EXPECT" ]; then pass "vip: line count = $VIPLINES"; else warn "vip: line count = $VIPLINES (want $VIP_COUNT_EXPECT)"; fi
  VIPOUT="$(awk -v pp="$VIP_PREFIX_PROVIDER" -v pa="$VIP_PREFIX_ADMIN" -v pi="$VIP_PREFIX_INTERNAL" \
                -v lo="$VIP_OCTET_MIN" -v hi="$VIP_OCTET_MAX" '
    BEGIN{ok=0;bad=0}
    /^[[:space:]]+vip:/{
      v=$0; sub(/^[^"]*"/,"",v); sub(/".*/,"",v); n=split(v,a," ");
      if(n!=3){print "    MALFORMED(not 3 IPs): " $0; bad++; next}
      if(index(a[1],pp".")!=1){print "    WRONG provider col: " $0; bad++; next}
      if(index(a[2],pa".")!=1){print "    WRONG admin col: " $0; bad++; next}
      if(index(a[3],pi".")!=1){print "    WRONG internal col: " $0; bad++; next}
      split(a[1],x,"."); split(a[2],y,"."); split(a[3],z,".");
      if(x[4]!=y[4]||y[4]!=z[4]){print "    UNALIGNED last octet: " $0; bad++; next}
      if(x[4]+0<lo||x[4]+0>hi){print "    OUT-OF-RANGE octet: " $0; bad++; next}
      ok++
    }
    END{print "RESULT " ok " " bad}
  ' bundle.yaml)"
  echo "$VIPOUT" | grep -v '^RESULT ' || true
  VOK="$(echo "$VIPOUT" | awk '/^RESULT /{print $2}')"
  VBAD="$(echo "$VIPOUT" | awk '/^RESULT /{print $3}')"
  if [ "${VOK:-0}" -eq "$VIP_COUNT_EXPECT" ] && [ "${VBAD:-1}" -eq 0 ]; then
    pass "aligned triple VIPs OK=$VOK bad=$VBAD"
  else
    fail "VIP validation OK=${VOK:-0} bad=${VBAD:-?} (want OK=$VIP_COUNT_EXPECT bad=0)"
  fi
fi

# ---------------------------------------------------------------------------
hdr "MAAS reachability gate (read-only)"
need_jq || finish
SUBJSON="$(maas "$MAAS_PROFILE" subnets read 2>/dev/null)" || { fail "MAAS unreachable: 'maas $MAAS_PROFILE subnets read' failed"; finish; }
pass "MAAS reachable (profile=$MAAS_PROFILE)"

# ---------------------------------------------------------------------------
hdr "CHECK 3: six planes resolved BY CIDR (id/vid/gw/dns)"
present=0
for c in "${PLANE_CIDRS[@]}"; do
  row="$(printf '%s' "$SUBJSON" | jq -r --arg c "$c" '.[] | select(.cidr==$c) | "id=\(.id) vid=\(.vlan.vid // 0) gw=\(.gateway_ip // "none") dns=\(.dns_servers|tostring)"')"
  if [ -z "$row" ]; then fail "plane ${PLANE_NAME[$c]} ($c) NOT FOUND"; continue; fi
  present=$((present+1))
  printf "    %-15s %-16s %s\n" "${PLANE_NAME[$c]}" "$c" "$row"
  gw="$(printf '%s' "$SUBJSON" | jq -r --arg c "$c" '.[]|select(.cidr==$c)|.gateway_ip // "none"')"
  exp="${PLANE_GW[$c]:-none}"
  if [ "$gw" != "$exp" ]; then
    if [ "$exp" = "none" ]; then warn "${PLANE_NAME[$c]} has gateway $gw (want none -- spurious-gw defect class, D-052)"; else fail "${PLANE_NAME[$c]} gateway=$gw (want $exp)"; fi
  fi
done
if [ "$present" -eq 6 ]; then pass "all six planes present (by CIDR)"; else fail "only $present/6 planes present"; fi
mivid="$(printf '%s' "$SUBJSON" | jq -r --arg c "$METAL_INTERNAL_CIDR" '.[]|select(.cidr==$c)|.vlan.vid // 0')"
if [ "$mivid" = "$METAL_INTERNAL_VID" ]; then pass "metal-internal is VID $METAL_INTERNAL_VID"; else fail "metal-internal VID=$mivid (want $METAL_INTERNAL_VID)"; fi
note "stale-NAME check is juju-side (run scripts/juju-spaces-check.sh after add-model)"

# ---------------------------------------------------------------------------
hdr "CHECK 2: data/storage NIC links BY CIDR (per host; expect octet .40-.43)"
for h in "${HOSTS[@]}"; do
  sid="$(host_sysid "$h" || true)"
  if [ -z "$sid" ]; then fail "$h not enrolled in MAAS"; continue; fi
  echo "  == $h ($sid, octet .${HOST_OCTET[$h]}) =="
  IFJSON="$(maas "$MAAS_PROFILE" interfaces read "$sid" 2>/dev/null)" || { fail "cannot read interfaces for $h"; continue; }
  for c in "${DATA_PLANE_CIDRS[@]}"; do
    line="$(printf '%s' "$IFJSON" | jq -r --arg c "$c" '.[] as $if | $if.links[]? | select(.subnet.cidr==$c) | "\($if.name) \(.ip_address // "(no-ip)")"' | head -1)"
    if [ -z "$line" ]; then fail "$h missing link on ${PLANE_NAME[$c]} ($c)"; continue; fi
    ifname="${line%% *}"; ip="${line##* }"
    printf "    %-14s -> %-16s %s\n" "$ifname" "$c" "$ip"
    if [ "$c" = "$METAL_INTERNAL_CIDR" ] && [ "$ifname" != "$METAL_INTERNAL_IFACE" ]; then
      fail "$h metal-internal on '$ifname' (want $METAL_INTERNAL_IFACE)"
    fi
    o="$(fourth_octet "$ip")"
    if [ "$o" != "${HOST_OCTET[$h]}" ]; then warn "$h ${PLANE_NAME[$c]} ip=$ip (want last octet .${HOST_OCTET[$h]})"; fi
  done
done

# ---------------------------------------------------------------------------
hdr "CHECK 4: the four KVM hosts -- status / power"
MJSON="$(maas "$MAAS_PROFILE" machines read 2>/dev/null)" || { fail "cannot read machines"; finish; }
for h in "${HOSTS[@]}"; do
  row="$(printf '%s' "$MJSON" | jq -r --arg n "$h" '.[]|select(.hostname==$n)|"\(.hostname) \(.status_name) power=\(.power_state)"')"
  if [ -z "$row" ]; then fail "host $h not found in MAAS"; continue; fi
  echo "    $row"
  st="$(printf '%s' "$MJSON" | jq -r --arg n "$h" '.[]|select(.hostname==$n)|.status_name')"
  if [ "$st" = "Ready" ]; then pass "$h Ready"; else fail "$h status=$st (want Ready)"; fi
done

finish
