#!/usr/bin/env bash
# scripts/checks/d011-04-octavia-lb.sh -- D-011 item 4: Octavia LB pattern
# (round-robin -> member failover -> recovery -> AMPHORA failover), per Bobcat v3 work.
#
# Stands up its OWN 2-replica backend + LoadBalancer Service in a tenant cluster (beta),
# drives the pattern, and self-cleans (trap). Two tiers:
#   ALWAYS (additive, self-cleaning): round-robin distribution + member failover + recovery.
#   --disruptive ONLY: amphora failover (destroys+rebuilds the amphora). Guarded by an N+1
#     scheduler-headroom pre-check -- Octavia STANDALONE failover transiently needs room for
#     one MORE amphora; a cloud at its ceiling cannot heal its own LB, so at-ceiling => HOLD
#     (never FAIL, and never TRIGGER a failover that would strand the LB in ERROR).
#
# Exit: 0 PASS | 1 FAIL (a pattern assertion broke) | 2 HOLD (octavia not ready, LB setup
#       undetermined, or amphora headroom not confidently available) | 4 SKIPPED (only if the
#       WHOLE check is not applicable). Amphora tier not requested => PASS on the additive tiers
#       with an explicit "amphora SKIPPED (needs --include-disruptive)" note.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib-validate.sh
. "$HERE/../lib-validate.sh"
ID=d011-04-octavia-lb; vr_begin "$ID"
CLIENT="${VR_TENANT:-beta}"
AGNHOST="${VR_AGNHOST:-registry.k8s.io/e2e-test-images/agnhost:2.47}"
NPROBE="${VR_RR_PROBES:-12}"
FAILS=0; HELD=0; AMPHORA_NOTE="amphora tier not run"

vr_need kubectl openstack curl jq awk || { emit "$ID" "$VR_HOLD" "missing tool"; exit "$VR_HOLD"; }

# --- precondition: octavia reachable as admin ---
if ! vr_admin_env; then emit "$ID" "$VR_HOLD" "no admin scope (octavia ops need it)"; exit "$VR_HOLD"; fi
if ! vr_json _LBL openstack loadbalancer list -f json; then
  vr_err_tail; emit "$ID" "$VR_HOLD" "octavia not reachable (loadbalancer list failed)"; exit "$VR_HOLD"
fi

# --- tenant kubeconfig ---
CF=""
for c in "$HOME/tenant-${CLIENT}/${CLIENT}-cluster-cred.txt"; do [ -s "$c" ] && CF="$c"; done
KCFG="$HOME/tenant-${CLIENT}/kube/config"
[ -s "$KCFG" ] || { emit "$ID" "$VR_HOLD" "no tenant kubeconfig at $KCFG (run tenant-acceptance/d011-03 first)"; exit "$VR_HOLD"; }
export KUBECONFIG="$KCFG"

SVC="d011lb$RANDOM"
cleanup(){
  kubectl delete svc "$SVC" --ignore-not-found --now >/dev/null 2>&1 || true
  kubectl delete deploy "$SVC" --ignore-not-found --now >/dev/null 2>&1 || true
}
trap cleanup EXIT

# --- stand up 2-replica backend + LoadBalancer ---
echo "  creating 2-replica backend + LB service ($SVC)"
run kubectl create deployment "$SVC" --image="$AGNHOST" --replicas=2 -- /agnhost netexec --http-port=8080 || { emit "$ID" "$VR_HOLD" "deployment create failed"; exit "$VR_HOLD"; }
run kubectl expose deployment "$SVC" --port=80 --target-port=8080 --type=LoadBalancer || { emit "$ID" "$VR_HOLD" "service expose failed"; exit "$VR_HOLD"; }
kubectl wait --for=condition=available --timeout=120s deploy/"$SVC" >/dev/null 2>&1 || true

# wait for FIP
FIP=""
for _ in $(seq 1 40); do
  FIP="$(kubectl get svc "$SVC" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)"
  [ -n "$FIP" ] && break; sleep 15
done
[ -n "$FIP" ] || { emit "$ID" "$VR_FAIL" "LB never got an EXTERNAL-IP (provisioning broken)"; exit "$VR_FAIL"; }
echo "  LB EXTERNAL-IP=$FIP"

rr_distinct(){ # echo count of distinct backend hostnames over NPROBE curls
  local i h; declare -A seen=()
  for i in $(seq 1 "$NPROBE"); do
    h="$(curl -s --max-time 5 "http://$FIP/hostname" 2>/dev/null || true)"
    [ -n "$h" ] && seen["$h"]=1
  done
  echo "${#seen[@]}"
}
codes_ok(){ # echo count of 200s over N curls
  local i c n=0; for i in $(seq 1 "$1"); do
    c="$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://$FIP/hostname" 2>/dev/null || true)"
    [ "$c" = 200 ] && n=$((n+1)); done; echo "$n"
}

# --- TIER 1: round-robin ---
D="$(rr_distinct)"
if [ "$D" -ge 2 ]; then echo "  round-robin: $D distinct backends (PASS)"; else echo "  round-robin: only $D distinct backend(s) (FAIL)"; FAILS=$((FAILS+1)); fi

# --- TIER 2: member failover + recovery ---
POD1="$(kubectl get pods -l app="$SVC" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
if [ -n "$POD1" ]; then
  echo "  member failover: deleting $POD1"
  run kubectl delete pod "$POD1" --now || true
  OK="$(codes_ok 6)"
  if [ "$OK" -ge 5 ]; then echo "  continuity during member loss: $OK/6 200s (PASS)"; else echo "  continuity broken: $OK/6 200s (FAIL)"; FAILS=$((FAILS+1)); fi
  kubectl wait --for=condition=available --timeout=120s deploy/"$SVC" >/dev/null 2>&1 || true
  D2="$(rr_distinct)"
  if [ "$D2" -ge 2 ]; then echo "  recovery: back to $D2 distinct backends (PASS)"; else echo "  recovery: only $D2 backend(s) after heal (FAIL)"; FAILS=$((FAILS+1)); fi
else echo "  member failover: could not identify a backend pod (FAIL)"; FAILS=$((FAILS+1)); fi

# --- locate the Octavia LB for this service (admin), by name substring ---
LBID="$(openstack loadbalancer list -f json </dev/null 2>/dev/null | jq -r --arg s "$SVC" '.[] | select(.name|test($s)) | .id' | head -1 || true)"

# --- TIER 3: amphora failover (disruptive only, headroom-guarded) ---
amphora_headroom(){ # echo OK|NO|UNKNOWN for +1 amphora of the LB's amphora flavor
  local lb="$1" aj cid sj fl fv fr hj free_ok=0
  aj="$(openstack loadbalancer amphora list --loadbalancer "$lb" -f json </dev/null 2>/dev/null || true)"
  cid="$(jq -r '.[0].compute_id // empty' <<<"$aj" 2>/dev/null || true)"
  [ -n "$cid" ] || { echo UNKNOWN; return; }
  sj="$(openstack server show "$cid" -f json </dev/null 2>/dev/null || true)"
  fl="$(jq -r '.flavor // empty' <<<"$sj" 2>/dev/null | grep -oE '[A-Za-z0-9._-]+' | head -1 || true)"
  [ -n "$fl" ] || { echo UNKNOWN; return; }
  fv="$(openstack flavor show "$fl" -f json </dev/null 2>/dev/null | jq -r '.vcpus // empty' 2>/dev/null || true)"
  fr="$(openstack flavor show "$fl" -f json </dev/null 2>/dev/null | jq -r '.ram // empty' 2>/dev/null || true)"
  { [ -n "$fv" ] && [ -n "$fr" ]; } || { echo UNKNOWN; return; }
  hj="$(openstack hypervisor list --long -f json </dev/null 2>/dev/null || true)"
  [ -n "$hj" ] || { echo UNKNOWN; return; }
  # any hypervisor with free vcpu and ram for +1 amphora flavor?
  free_ok="$(jq -r --argjson v "$fv" --argjson r "$fr" '
    [ .[] | select(((.vcpus // 0)-(.vcpus_used // 0)) >= $v and ((.memory_mb // 0)-(.memory_mb_used // 0)) >= $r) ] | length' <<<"$hj" 2>/dev/null || echo 0)"
  case "$free_ok" in ''|0) echo NO;; *) echo OK;; esac
}

if vr_disruptive_ok; then
  if [ -z "$LBID" ]; then echo "  amphora failover: could not locate Octavia LB for $SVC (HOLD)"; HELD=1; AMPHORA_NOTE="amphora LB not located"
  else
    HR="$(amphora_headroom "$LBID")"
    echo "  amphora headroom (N+1 for STANDALONE failover): $HR"
    if [ "$HR" != OK ]; then
      echo "  amphora failover HELD: headroom $HR -- will not risk failover at/near ceiling"
      HELD=1; AMPHORA_NOTE="amphora failover held (headroom=$HR)"
    else
      echo "  triggering amphora failover on $LBID"
      run openstack loadbalancer failover "$LBID" || { FAILS=$((FAILS+1)); }
      PS=""
      for _ in $(seq 1 30); do
        PS="$(openstack loadbalancer show "$LBID" -f value -c provisioning_status </dev/null 2>/dev/null || true)"
        [ "$PS" = ACTIVE ] && break; [ "$PS" = ERROR ] && break; sleep 10
      done
      OK="$(codes_ok 6)"
      if [ "$PS" = ACTIVE ] && [ "$OK" -ge 5 ]; then echo "  amphora failover recovered: ACTIVE + $OK/6 200s (PASS)"; AMPHORA_NOTE="amphora failover PASS"
      else echo "  amphora failover did NOT recover (status=$PS, $OK/6 200s) (FAIL)"; FAILS=$((FAILS+1)); AMPHORA_NOTE="amphora failover FAIL"; fi
    fi
  fi
else
  echo "  amphora failover SKIPPED (needs --include-disruptive)"; AMPHORA_NOTE="amphora SKIPPED (not disruptive)"
fi

# --- verdict ---
if [ "$FAILS" -gt 0 ]; then emit "$ID" "$VR_FAIL" "$FAILS LB-pattern assertion(s) failed; $AMPHORA_NOTE"; exit "$VR_FAIL"; fi
if [ "$HELD" -gt 0 ]; then emit "$ID" "$VR_HOLD" "LB round-robin+member-failover PASS; $AMPHORA_NOTE"; exit "$VR_HOLD"; fi
emit "$ID" "$VR_PASS" "octavia LB pattern PASS (round-robin+member-failover+recovery); $AMPHORA_NOTE"; exit "$VR_PASS"
