#!/usr/bin/env bash
set -euo pipefail

MODE="${1:-}"
STATE_DIR="/tmp/soria-routing-ha"
FRR_INIT="/usr/lib/frr/frrinit.sh"
OSPF_NS=(m5-ha m5-hb m5-r1 m5-r2 m5-r3)
VRRP_NS=(m5-vcli m5-vsrv m5-gw1 m5-gw2 m5-lsw m5-wsw)

usage() {
  cat <<'USAGE'
Usage:
  sudo ./module5b-routing-ha-lab.sh ospf
  sudo ./module5b-routing-ha-lab.sh ospf-fail
  sudo ./module5b-routing-ha-lab.sh ospf-recover
  sudo ./module5b-routing-ha-lab.sh vrrp
  sudo ./module5b-routing-ha-lab.sh vrrp-fail
  sudo ./module5b-routing-ha-lab.sh vrrp-recover
  sudo ./module5b-routing-ha-lab.sh status
  sudo ./module5b-routing-ha-lab.sh cleanup

ospf          create a redundant three-router OSPF topology.
ospf-fail     disable the direct R1-R2 link.
ospf-recover  restore the direct R1-R2 link.
vrrp          create two gateways sharing 10.90.10.1 with Keepalived.
vrrp-fail     stop Keepalived on the preferred gateway.
vrrp-recover  restart Keepalived on the preferred gateway.
USAGE
}

require_root() {
  if [[ ${EUID} -ne 0 ]]; then
    echo "Exécute ce script avec sudo." >&2
    exit 1
  fi
}

require_commands() {
  local missing=0 command
  for command in "$@"; do
    if [[ "$command" == */* ]]; then
      [[ -x "$command" ]] || { echo "Commande manquante : $command" >&2; missing=1; }
    elif ! command -v "$command" >/dev/null 2>&1; then
      echo "Commande manquante : $command" >&2
      missing=1
    fi
  done
  [[ $missing -eq 0 ]] || exit 1
}

create_ns() {
  local ns="$1"
  ip netns add "$ns"
  ip -n "$ns" link set lo up
}

link_ns() {
  local ns_a="$1" tmp_a="$2" if_a="$3"
  local ns_b="$4" tmp_b="$5" if_b="$6"

  ip link add "$tmp_a" type veth peer name "$tmp_b"
  ip link set "$tmp_a" netns "$ns_a"
  ip link set "$tmp_b" netns "$ns_b"
  ip -n "$ns_a" link set "$tmp_a" name "$if_a"
  ip -n "$ns_b" link set "$tmp_b" name "$if_b"
  ip -n "$ns_a" link set "$if_a" up
  ip -n "$ns_b" link set "$if_b" up
}

kill_ns_processes() {
  local ns="$1" pids
  pids="$(ip netns pids "$ns" 2>/dev/null || true)"
  if [[ -n "$pids" ]]; then
    kill $pids 2>/dev/null || true
    sleep 0.3
    kill -9 $pids 2>/dev/null || true
  fi
}

stop_frr_pathspace() {
  local pathspace="$1"
  if [[ -x "$FRR_INIT" && -d "/etc/frr/$pathspace" ]]; then
    "$FRR_INIT" stop "$pathspace" >/dev/null 2>&1 || true
  fi
}

cleanup_ospf() {
  local ns
  for ns in m5-r1 m5-r2 m5-r3; do
    stop_frr_pathspace "$ns"
  done
  for ns in "${OSPF_NS[@]}"; do
    kill_ns_processes "$ns"
    ip netns del "$ns" 2>/dev/null || true
  done
  rm -rf /etc/frr/m5-r1 /etc/frr/m5-r2 /etc/frr/m5-r3
}

kill_keepalived() {
  local node="$1" file
  for file in "$STATE_DIR/$node/keepalived.pid" "$STATE_DIR/$node/vrrp.pid" "$STATE_DIR/$node/wrapper.pid"; do
    if [[ -f "$file" ]]; then
      kill "$(cat "$file")" 2>/dev/null || true
    fi
  done
}

cleanup_vrrp() {
  local ns
  kill_keepalived m5-gw1
  kill_keepalived m5-gw2
  for ns in "${VRRP_NS[@]}"; do
    kill_ns_processes "$ns"
    ip netns del "$ns" 2>/dev/null || true
  done
}

cleanup() {
  cleanup_ospf
  cleanup_vrrp
  rm -rf "$STATE_DIR"
}

prepare_frr_pathspace() {
  local ns="$1" router_id="$2" networks="$3" costs="$4"
  local dir="/etc/frr/$ns"

  mkdir -p "$dir"
  cp /etc/frr/daemons "$dir/daemons"
  sed -i -E 's/^zebra=.*/zebra=yes/; s/^ospfd=.*/ospfd=yes/; s/^vtysh_enable=.*/vtysh_enable=yes/' "$dir/daemons"
  if grep -q '^watchfrr_options=' "$dir/daemons"; then
    sed -i 's|^watchfrr_options=.*|watchfrr_options="--netns"|' "$dir/daemons"
  else
    printf '%s\n' 'watchfrr_options="--netns"' >> "$dir/daemons"
  fi

  cat > "$dir/frr.conf" <<EOF_FRR
frr defaults traditional
hostname $ns
service integrated-vtysh-config
!
$costs
!
router ospf
 ospf router-id $router_id
 passive-interface lan0
$networks
!
line vty
!
EOF_FRR

  chown -R frr:frr "$dir"
  chmod 640 "$dir/frr.conf" "$dir/daemons"
}

setup_ospf() {
  cleanup
  require_commands ip ping vtysh getent "$FRR_INIT"
  getent passwd frr >/dev/null || { echo "Utilisateur frr absent." >&2; exit 1; }
  [[ -f /etc/frr/daemons ]] || { echo "/etc/frr/daemons absent." >&2; exit 1; }

  mkdir -p "$STATE_DIR"
  local ns
  for ns in "${OSPF_NS[@]}"; do create_ns "$ns"; done

  link_ns m5-ha haeth eth0 m5-r1 r1lan lan0
  link_ns m5-r1 r1p12 p12 m5-r2 r2p12 p12
  link_ns m5-r1 r1p13 p13 m5-r3 r3p13 p13
  link_ns m5-r3 r3p23 p23 m5-r2 r2p23 p23
  link_ns m5-r2 r2lan lan0 m5-hb hbeth eth0

  ip -n m5-ha addr add 10.80.10.10/24 dev eth0
  ip -n m5-r1 addr add 10.80.10.1/24 dev lan0
  ip -n m5-r1 addr add 10.80.12.1/30 dev p12
  ip -n m5-r2 addr add 10.80.12.2/30 dev p12
  ip -n m5-r1 addr add 10.80.13.1/30 dev p13
  ip -n m5-r3 addr add 10.80.13.2/30 dev p13
  ip -n m5-r3 addr add 10.80.23.1/30 dev p23
  ip -n m5-r2 addr add 10.80.23.2/30 dev p23
  ip -n m5-r2 addr add 10.80.20.1/24 dev lan0
  ip -n m5-hb addr add 10.80.20.10/24 dev eth0

  ip -n m5-ha route add default via 10.80.10.1
  ip -n m5-hb route add default via 10.80.20.1
  for ns in m5-r1 m5-r2 m5-r3; do
    ip netns exec "$ns" sysctl -qw net.ipv4.ip_forward=1
  done

  prepare_frr_pathspace m5-r1 1.1.1.1 \
' network 10.80.10.0/24 area 0.0.0.0
 network 10.80.12.0/30 area 0.0.0.0
 network 10.80.13.0/30 area 0.0.0.0' \
'interface p12
 ip ospf cost 10
!
interface p13
 ip ospf cost 20'

  prepare_frr_pathspace m5-r2 2.2.2.2 \
' network 10.80.20.0/24 area 0.0.0.0
 network 10.80.12.0/30 area 0.0.0.0
 network 10.80.23.0/30 area 0.0.0.0' \
'interface p12
 ip ospf cost 10
!
interface p23
 ip ospf cost 20'

  prepare_frr_pathspace m5-r3 3.3.3.3 \
' network 10.80.13.0/30 area 0.0.0.0
 network 10.80.23.0/30 area 0.0.0.0' \
'interface p13
 ip ospf cost 20
!
interface p23
 ip ospf cost 20'

  for ns in m5-r1 m5-r2 m5-r3; do
    "$FRR_INIT" start "$ns"
  done
  sleep 4

  cat <<'READY'
Laboratoire OSPF prêt.

Vérifications :
  sudo vtysh -N m5-r1 -c 'show ip ospf neighbor'
  sudo vtysh -N m5-r1 -c 'show ip route ospf'
  sudo ip netns exec m5-ha ping -c 3 10.80.20.10

Mesure de convergence :
  sudo ip netns exec m5-ha ping -D -i 0.2 10.80.20.10
  sudo ./module5b-routing-ha-lab.sh ospf-fail
READY
}

ospf_fail() {
  ip netns list | grep -q '^m5-r1' || { echo "Lance d'abord le mode ospf." >&2; exit 1; }
  ip -n m5-r1 link set p12 down
  echo "Lien direct R1-R2 désactivé. Observe la route via R3."
}

ospf_recover() {
  ip netns list | grep -q '^m5-r1' || { echo "Lance d'abord le mode ospf." >&2; exit 1; }
  ip -n m5-r1 link set p12 up
  echo "Lien direct R1-R2 restauré."
}

create_bridge_ns() {
  local ns="$1" bridge="$2"
  create_ns "$ns"
  ip -n "$ns" link add "$bridge" type bridge
  ip -n "$ns" link set "$bridge" up
}

attach_to_bridge() {
  local endpoint_ns="$1" tmp_ep="$2" endpoint_if="$3"
  local switch_ns="$4" tmp_sw="$5" bridge="$6"
  link_ns "$endpoint_ns" "$tmp_ep" "$endpoint_if" "$switch_ns" "$tmp_sw" "$tmp_sw"
  ip -n "$switch_ns" link set "$tmp_sw" master "$bridge"
}

write_keepalived_conf() {
  local node="$1" state="$2" priority="$3" src_ip="$4"
  local dir="$STATE_DIR/$node"
  mkdir -p "$dir"
  cat > "$dir/keepalived.conf" <<EOF_KA
vrrp_instance VI_GATEWAY {
    state $state
    interface lan0
    virtual_router_id 51
    priority $priority
    advert_int 1
    unicast_src_ip $src_ip
    unicast_peer {
        $( [[ "$node" == m5-gw1 ]] && echo 10.90.10.3 || echo 10.90.10.2 )
    }
    virtual_ipaddress {
        10.90.10.1/24 dev lan0
    }
}
EOF_KA
}

start_keepalived() {
  local node="$1" dir="$STATE_DIR/$node"
  mkdir -p "$dir"
  ip netns exec "$node" keepalived -n -P -l -D \
    -f "$dir/keepalived.conf" \
    -p "$dir/keepalived.pid" \
    -r "$dir/vrrp.pid" \
    > "$dir/keepalived.log" 2>&1 &
  echo $! > "$dir/wrapper.pid"
}

load_gateway_nat() {
  local node="$1"
  cat > "$STATE_DIR/$node/nat.nft" <<'EOF_NFT'
table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    oifname "wan0" ip saddr 10.90.10.0/24 masquerade
  }
}
EOF_NFT
  ip netns exec "$node" nft -f "$STATE_DIR/$node/nat.nft"
}

setup_vrrp() {
  cleanup
  require_commands ip ping curl python3 nft keepalived
  mkdir -p "$STATE_DIR"

  create_bridge_ns m5-lsw br0
  create_bridge_ns m5-wsw br0
  for ns in m5-vcli m5-vsrv m5-gw1 m5-gw2; do create_ns "$ns"; done

  attach_to_bridge m5-vcli ceth eth0 m5-lsw swc br0
  attach_to_bridge m5-gw1 g1lan lan0 m5-lsw swg1 br0
  attach_to_bridge m5-gw2 g2lan lan0 m5-lsw swg2 br0
  attach_to_bridge m5-gw1 g1wan wan0 m5-wsw sw1 br0
  attach_to_bridge m5-gw2 g2wan wan0 m5-wsw sw2 br0
  attach_to_bridge m5-vsrv seth eth0 m5-wsw sws br0

  ip -n m5-vcli addr add 10.90.10.10/24 dev eth0
  ip -n m5-gw1 addr add 10.90.10.2/24 dev lan0
  ip -n m5-gw2 addr add 10.90.10.3/24 dev lan0
  ip -n m5-gw1 addr add 10.90.20.2/24 dev wan0
  ip -n m5-gw2 addr add 10.90.20.3/24 dev wan0
  ip -n m5-vsrv addr add 10.90.20.10/24 dev eth0
  ip -n m5-vcli route add default via 10.90.10.1

  for ns in m5-gw1 m5-gw2; do
    ip netns exec "$ns" sysctl -qw net.ipv4.ip_forward=1
    mkdir -p "$STATE_DIR/$ns"
    load_gateway_nat "$ns"
  done

  write_keepalived_conf m5-gw1 MASTER 150 10.90.10.2
  write_keepalived_conf m5-gw2 BACKUP 100 10.90.10.3
  start_keepalived m5-gw1
  start_keepalived m5-gw2

  ip netns exec m5-vsrv python3 -m http.server 8080 --bind 10.90.20.10 \
    > "$STATE_DIR/http.log" 2>&1 &
  echo $! > "$STATE_DIR/http.pid"
  sleep 3

  cat <<'READY'
Laboratoire VRRP prêt.

Vérifications :
  sudo ip -n m5-gw1 -brief address show lan0
  sudo ip -n m5-gw2 -brief address show lan0
  sudo ip netns exec m5-vcli curl -s http://10.90.20.10:8080/ | head
  sudo ip netns exec m5-vcli ip neigh show 10.90.10.1

Basculement :
  sudo ./module5b-routing-ha-lab.sh vrrp-fail
  sleep 4
  sudo ip netns exec m5-vcli curl -s http://10.90.20.10:8080/ | head
READY
}

vrrp_fail() {
  ip netns list | grep -q '^m5-gw1' || { echo "Lance d'abord le mode vrrp." >&2; exit 1; }
  kill_keepalived m5-gw1
  echo "Keepalived arrêté sur GW1. GW2 doit devenir MASTER."
}

vrrp_recover() {
  ip netns list | grep -q '^m5-gw1' || { echo "Lance d'abord le mode vrrp." >&2; exit 1; }
  [[ -f "$STATE_DIR/m5-gw1/keepalived.conf" ]] || { echo "Configuration GW1 absente." >&2; exit 1; }
  start_keepalived m5-gw1
  echo "GW1 redémarré. Sa priorité supérieure doit lui rendre le VIP."
}

status() {
  echo "=== OSPF ==="
  if ip netns list | grep -q '^m5-r1'; then
    vtysh -N m5-r1 -c 'show ip ospf neighbor' || true
    vtysh -N m5-r1 -c 'show ip route ospf' || true
  else
    echo "lab absent"
  fi

  echo
  echo "=== VRRP ==="
  for ns in m5-gw1 m5-gw2; do
    if ip netns list | grep -q "^$ns"; then
      echo "[$ns]"
      ip -n "$ns" -brief address show lan0
    fi
  done
}

require_root

case "$MODE" in
  ospf) setup_ospf ;;
  ospf-fail) require_commands ip; ospf_fail ;;
  ospf-recover) require_commands ip; ospf_recover ;;
  vrrp) setup_vrrp ;;
  vrrp-fail) require_commands ip; vrrp_fail ;;
  vrrp-recover) require_commands ip keepalived; vrrp_recover ;;
  status) require_commands ip; status ;;
  cleanup) require_commands ip; cleanup; echo "Laboratoires supprimés." ;;
  *) usage; exit 1 ;;
esac
