# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Maxime Leroy, Free Mobile

test_frr=true

. $(dirname $0)/_init.sh

route_nh_id() {
	local route_nh nh_id
	local prefix="$1"
	local vrf="$2"
	local nh_type="$3"

	route_nh=$(grcli -j route show | jq -re \
		".[] | select(.destination == \"$prefix\" and .vrf == \"$vrf\") | .next_hop") ||
		fail "cannot find route to $prefix in vrf $vrf"

	case "$route_nh" in
	"type=group "*)
		nh_id=$(echo $route_nh | sed -En 's,.*\<id\(([0-9]+)/[0-9]+\).*,\1,p')
		;;
	"type=$nh_type "*)
		nh_id=$(echo $route_nh | sed -En 's,.*\<id=([0-9]+)\>.*,\1,p')
		;;
	*)
		fail "invalid nexthop type for route $prefix: '$route_nh': expected type=$nh_type"
	esac

	echo "$nh_id"
}

assert_nexthop() {
	local nh_id="$1"
	local jq_pattern="$2"

	grcli -j nexthop show id "$nh_id" | jq -e "$jq_pattern" || {
		grcli -j nexthop show id "$nh_id" | jq
		fail "nexthop id=$nh_id does not match expected: $jq_pattern"
	}
}

create_vrf() {
	local name="$1"
	local max_tries=5
	local count=0

	grcli interface add vrf "$name"

	while vtysh -c "show interface $name" 2>&1 | grep -q "% Can't find interface"; do
		if [ "$count" -ge "$max_tries" ]; then
			fail "VRF $name not found after $max_tries attempts."
		fi
		sleep 0.2
		count=$((count + 1))
	done
}

create_interface() {
	local p="$1"
	shift
	local max_tries=5
	local count=0

	port_add $p "$@"

	while vtysh -c "show interface $p" 2>&1 | grep -q "% Can't find interface"; do
		if [ "$count" -ge "$max_tries" ]; then
			fail "Interface $p not found after $max_tries attempts."
		fi
		sleep 0.2
		count=$((count + 1))
	done
}

# _apply_frr_config <persist> <wait_pattern> <config>
#
# Push <config> via vtysh (default) or append it to frr.conf
# (persist=1). In vtysh mode "configure terminal" is prepended
# automatically and wait_event waits for <wait_pattern> when non-empty.
# In persist mode the block is written verbatim and no event is awaited
# (config takes effect at the next FRR start).
#
_apply_frr_config() {
	local persist="$1"
	local wait_pattern="$2"
	local config="$3"

	mark_events
	vtysh <<EOF
configure terminal
$config
EOF
	[ -n "$wait_pattern" ] && wait_event -t 10 "$wait_pattern"

	if [ "$persist" = 1 ]; then
		cat >>"$builddir/frr_install/etc/frr/frr.conf" <<EOF
$config
EOF
	fi
}

set_ip_address() {
	local persist=0
	[ "$1" = "--persist" ] && { persist=1; shift; }
	local p="$1"
	local ip_cidr="$2"

	if echo "$ip_cidr" | grep -q ':'; then
		# IPv6
		local frr_ip="ipv6"
		local addr="addr6"
	else
		# IPv4
		local frr_ip="ip"
		local addr="addr4"
	fi

	_apply_frr_config "$persist" "$addr add: iface=$p $ip_cidr" \
"interface ${p}
 ${frr_ip} address ${ip_cidr}
exit"
}

set_ip_route() {
	local persist=0
	[ "$1" = "--persist" ] && { persist=1; shift; }
	local prefix="$1"
	local next_hop="$2"
	local vrf_name="${3:-default}"
	local nexthop_vrf_name="${4:-}"

	local nexthop_vrf_clause=""
	if [[ -n "$nexthop_vrf_name" ]]; then
		nexthop_vrf_clause=" nexthop-vrf ${nexthop_vrf_name}"
	fi

	if echo "$prefix" | grep -q ':'; then
		# IPv6
		local frr_ip="ipv6"
		local route="route6"
	else
		# IPv4
		local frr_ip="ip"
		local route="route4"
	fi

	# strip optional nexthop interface
	local nh=${next_hop% *}
	local gr_vrf_name="${vrf_name}"
	[ "$gr_vrf_name" = "default" ] && gr_vrf_name="main"

	_apply_frr_config "$persist" \
		"$route add: vrf=$gr_vrf_name $prefix origin=zebra_static" \
		"${frr_ip} route ${prefix} ${next_hop} vrf ${vrf_name}${nexthop_vrf_clause}"

	local nh_id
	nh_id=$(route_nh_id "$prefix" "$gr_vrf_name" L3)
	case "$nh" in
	*.*|*:*)
		assert_nexthop "$nh_id" ".vrf == \"${nexthop_vrf_name:-$gr_vrf_name}\" and .addr ==\"$nh\""
		;;
	*)
		assert_nexthop "$nh_id" ".vrf == \"${nexthop_vrf_name:-$gr_vrf_name}\" and .iface ==\"$nh\""
		;;
	esac
}

# set_srv6_localsid [--persist] <locator> <sid-prefix> <sid-local> [behavior] [vrf]
#
# Examples:
#   set_srv6_localsid myloc fd00:202 fc00:100:64:10::666 end.dt4
#   set_srv6_localsid myloc fd00:202 fc00:100:64:10::667 end.dt4 vrf2
#
# zebra's SRv6 manager keeps one SID per (behavior, vrf) ctx; coexisting
# localsids must therefore use distinct VRFs (or behaviors).
#
set_srv6_localsid() {
	local persist=0
	[ "$1" = "--persist" ] && { persist=1; shift; }
	local locator="$1"
	local sid_prefix="$2"
	local sid_local="$3"
	local grout_behavior="${4:-end.dt4}"   # default behaviour
	local vrf_name="${5:-default}"

	# ---- translate behaviour aliases --------------------------------------
	local frr_behavior
	case "${grout_behavior,,}" in           # ,, = lower-case
		end)      frr_behavior="uN" ;;
		end.dt4)  frr_behavior="uDT4" ;;
		end.dt6)  frr_behavior="uDT6" ;;
		end.dt46) frr_behavior="uDT46" ;;
		*) echo "Unsupported behavior '${grout_behavior}'."; exit 1 ;;
	esac

	# --- push the config into FRR ------------------------------------------
	local vrf_clause="vrf ${vrf_name}"
	case "${frr_behavior}" in
		uN) vrf_clause="" ;;
	esac
	local gr_vrf_name=main
	if [ "$vrf_name" != "default" ]; then
		gr_vrf_name="$vrf_name"
	fi

	_apply_frr_config "$persist" \
		"route6 add: vrf=.+ $sid_local/48 origin=zebra_static" \
"segment-routing
 srv6
  locators
   locator ${locator}
    prefix ${sid_prefix}::/32 block-len 16 node-len 16 func-bits 16
   exit
  exit
  static-sids
    sid ${sid_local}/48 locator ${locator} behavior ${frr_behavior} ${vrf_clause}
  exit
 exit
exit"

	local nh_id
	nh_id=$(route_nh_id "$sid_local/48" main SRv6-local)
	assert_nexthop "$nh_id" ".vrf == \"$gr_vrf_name\" and .behavior == \"$grout_behavior\""
}

# set_srv6_route [--persist] [--encap-src <addr>] <prefix> <next-hop|iface>
#                [<vrf> [<nexthop-vrf>]] <sid1> [sid2 sid3 ...]
#
# EXAMPLES
#   # default VRF (no vrf args), three SIDs:
#   set_srv6_route 192.168.0.0/16 gmydsn1 \
#       fd00:202::2 fd00:202::3 fd00:202::4
#
#   # L3VPN encap: route in vrf2, encap iface in default. nexthop-vrf
#   # is passed explicitly as the 4th positional.
#   set_srv6_route 192.168.0.0/24 p0 vrf2 default fd00:202:100::
#
set_srv6_route() {
	local persist=0
	[ "$1" = "--persist" ] && { persist=1; shift; }
	local encap_src=""
	[ "$1" = "--encap-src" ] && { encap_src=$2; shift 2; }
	local prefix="$1"
	local nhop="$2"
	shift 2

	# 3rd & 4th positional are vrf and nexthop-vrf (mirrors set_ip_route).
	# Both optional; SIDs come last and always contain ':'. Peek $1: if
	# it is colon-free we treat it as the vrf, then likewise for nh_vrf.
	local vrf_name="default"
	local nexthop_vrf_name=""
	if [ -n "$1" ] && [[ "$1" != *:* ]]; then
		vrf_name="$1"
		shift
		if [ -n "$1" ] && [[ "$1" != *:* ]]; then
			nexthop_vrf_name="$1"
			shift
		fi
	fi

	# ----- collect SIDs ----------------------------------------------------
	local sids=()
	while [[ "$1" =~ : ]]; do     # anything with a ":" is assumed to be a SID
		sids+=("$1")
		shift
	done
	[[ ${#sids[@]} -eq 0 ]] && { echo "set_srv6_route: need at least one SID" >&2; return 1; }

	# ----- choose FRR keyword ---------------------------------------------
	local frr_ip route
	if [[ "$prefix" == *:* ]]; then
		frr_ip="ipv6"
		route="route6"
	else
		frr_ip="ip"
		route="route4"
	fi

	# ----- build CLI form -------------------------------------------------
	local seg_frr="${sids[*]}"
	seg_frr=${seg_frr// //}

	# grout names the default VRF "main"; rest match by name.
	local gr_vrf_name="${vrf_name}"
	[ "$gr_vrf_name" = "default" ] && gr_vrf_name="main"

	local nh_vrf_clause=""
	local gr_nh_vrf=""
	if [ -n "$nexthop_vrf_name" ]; then
		nh_vrf_clause=" nexthop-vrf ${nexthop_vrf_name}"
		gr_nh_vrf="$nexthop_vrf_name"
	else
		gr_nh_vrf="$gr_vrf_name"
	fi
	[ "$gr_nh_vrf" = "default" ] && gr_nh_vrf="main"

	local encap_src_clause=""
	[ -n "$encap_src" ] && encap_src_clause=" encap-source ${encap_src}"

	_apply_frr_config "$persist" \
		"$route add: vrf=$gr_vrf_name $prefix origin=zebra_static" \
		"${frr_ip} route ${prefix} ${nhop} segments ${seg_frr}${encap_src_clause} vrf ${vrf_name}${nh_vrf_clause}"

	local nh_id
	nh_id=$(route_nh_id "$prefix" "$gr_vrf_name" SRv6)
	assert_nexthop "$nh_id" ".vrf == \"$gr_nh_vrf\" and .seglist[0] == \"${sids[0]}\""
}

# kill_frr_daemons <daemon> [<daemon>...]
#
# Simulate a crash of the named FRR daemons (e.g. zebra, staticd) and
# block until watchfrr has respawned each one. PIDs are read from the
# per-test pidfiles under $builddir/frr_install/var/run/frr/ so this is
# safe to call from tests running in parallel; a host-wide pkill would
# clobber daemons spawned by sibling smoke tests.
#
# Caller MUST export WATCHFRR_EXTRA_OPTS="--min-restart-interval=1"
# before start_frr (the value is cached by watchfrr at startup).
# Default is 60s, which silently postpones the respawn well past the
# 10s budget enforced below and would make this helper always fail.
#
# Fails after 10s if any daemon is not respawned with a different pid.
kill_frr_daemons() {
	local frr_run="$builddir/frr_install/var/run/frr"
	local daemon f
	declare -A pids

	for daemon in "$@"; do
		f="$frr_run/$daemon.pid"
		pids[$f]=$(cat "$f")
	done

	{ sleep 0.2 && kill -9 "${pids[@]}"; } &

	while [ "${#pids[@]}" -gt 0 ] && read -r f; do
		if [[ -v pids[$f] ]]; then
			pkill -0 --pidfile "$f"
			unset pids[$f]
		fi
	done < <(timeout 10s inotifywait -mq -e modify --format %w%f "$frr_run" || :)

	[ "${#pids[@]}" -eq 0 ] || fail "watchfrr did not respawn ${pids[@]} within 10s"
}

#   <namespace> : optional netns name ("" = root namespace)
#   <use_grout> : "1" -> enable dplane_grout, anything else -> no grout
start_frr() {
	local namespace="$1"
	local use_grout="$2"

	local frr_etc="$builddir/frr_install/etc/frr"
	local frr_logdir="$builddir/frr_install/var/log/frr"
	# $frr_log is intentionally global (not local) so tests sourcing
	# _init_frr.sh can grep it after start_frr completes.
	local conf_dir daemons_file frrconf_file frr_global_opts
	local zebra_options="-s 90000000${ZEBRA_EXTRA_OPTS:+ $ZEBRA_EXTRA_OPTS}"

	mkdir -p "$frr_etc"
	mkdir -p "$frr_logdir"

	# Common config dir + files + log file + opts
	conf_dir="$frr_etc${namespace:+/$namespace}"
	frr_log="$frr_logdir/frr-${namespace:-grout}.log"

	mkdir -p "$conf_dir"
	touch "$conf_dir/vtysh.conf"

	daemons_file="$conf_dir/daemons"
	frrconf_file="$conf_dir/frr.conf"
	frr_global_opts="-A 127.0.0.1 --log file:$frr_log"

	if [ "$use_grout" = "1" ]; then
		zebra_options="$zebra_options -M dplane_grout"
	fi

	# daemons
	cat >"$daemons_file" <<EOF
bfdd=yes
bgpd=yes
isisd=yes
ospfd=yes
ospf6d=yes
vtysh_enable=yes
frr_global_options="$frr_global_opts"
zebra_options="$zebra_options"
EOF

	if [ -n "$namespace" ] || [ -n "${WATCHFRR_EXTRA_OPTS:-}" ]; then
		local wopts=""
		[ -n "$namespace" ] && wopts="--netns=$namespace"
		[ -n "${WATCHFRR_EXTRA_OPTS:-}" ] && \
			wopts="${wopts:+$wopts }$WATCHFRR_EXTRA_OPTS"
		cat >>"$daemons_file" <<EOF
watchfrr_options="$wopts"
EOF
	fi

	# frr.conf
	cat >"$frrconf_file" <<EOF
hostname ${namespace:-grout}
debug zebra dplane dpdk
EOF

	# reset log
	rm -f "$frr_log"
	touch "$frr_log"

	# logging: root strips ts, ns gets [ns] prefix
	local sed_expr color
	if [ -n "$namespace" ]; then
		sed_expr="s,^[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[$namespace],"
		color="\033[32m"
	else
		sed_expr="s,^[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} ,,"
		color="\033[35m"
	fi

	# Run the log tail pipeline in a subshell so we can kill all
	# pipeline processes by killing the subshell's children.
	if [ -t 1 ]; then
		(tail -F "$frr_log" | sed -u -E "$sed_expr" | \
			stdbuf -oL awk -v color="$color" '{print color $0 "\033[0m"}') &
	else
		(tail -F "$frr_log" | sed -u -E "$sed_expr") &
	fi
	local tailpid=$!

	if [ -n "$namespace" ]; then
		ip netns add "$namespace"
		# Match netns_add's defaults: skip IPv6 DAD and ignore kernel RA
		# processing so FRR peer netns behave consistently with the rest
		# of the smoke suite.
		ip netns exec "$namespace" sysctl -wq net.ipv6.conf.all.accept_dad=0
		ip netns exec "$namespace" sysctl -wq net.ipv6.conf.default.accept_dad=0
		ip netns exec "$namespace" sysctl -wq net.ipv6.conf.all.accept_ra=0
		ip netns exec "$namespace" sysctl -wq net.ipv6.conf.default.accept_ra=0
	fi

	# cleanup -- kill the subshell children (tail, sed, awk)
	cat >>"$tmp/cleanup" <<EOF
frrinit.sh stop ${namespace:+$namespace}
pkill -P $tailpid 2>/dev/null || true
EOF
	if [ -n "$namespace" ]; then
		cat >>"$tmp/cleanup" <<EOF
ip netns pids "$namespace" | xargs -r kill -TERM 2>/dev/null || true
sleep 0.5
ip netns pids "$namespace" | xargs -r kill -KILL 2>/dev/null || true
ip netns del "$namespace"
EOF
	fi

	# start FRR
	frrinit.sh start ${namespace:+$namespace}

	# wait for all daemons (staticd is always started by FRR init)
	local daemon pattern
	for daemon in staticd bfdd bgpd isisd ospfd ospf6d; do
		SECONDS=0
		pattern="$daemon"
		[ -n "$namespace" ] && pattern="$daemon -N $namespace"

		while ! pgrep -f "$pattern" >/dev/null 2>&1; do
			if [ "$SECONDS" -ge 5 ]; then
				if [ -n "$namespace" ]; then
					fail "$daemon daemon not started for namespace $namespace"
				else
					fail "$daemon daemon not started"
				fi
			fi
			sleep 0.1
		done
	done

	# extra check when using Grout: wait for iface/ip events
	if [ "$use_grout" = "1" ]; then
		local attempts=25
		while ! grep -q "GROUT:.*iface/ip events" "$frr_log" 2>/dev/null; do
			if [ "$attempts" -le 0 ]; then
				fail "Zebra is not listening grout events."
			fi
			sleep 0.2
			attempts=$((attempts - 1))
		done
	fi

	if [ "${INTERACTIVE:-false}" = true ]; then
		if [ -n "$namespace" ]; then
			tmux_new_window "vtysh-$namespace" vtysh -N "$namespace"
		else
			tmux_new_window vtysh vtysh
		fi
	fi
}

if [ "$test_frr" = true ] && [ "$run_frr" = true ]; then
	chmod 0777 $tmp # to access on tmp

	if [ -n "${builddir}" ]; then
		smoke_setenv PATH "$builddir/frr_install/sbin:$builddir/frr_install/bin:$PATH"
		# isolate FRR state dirs so parallel tests don't collide;
		# FRR daemons drop privileges to the user that compiled
		# them (--enable-user), use mode=1777 so they can write
		for d in etc/frr var/log/frr var/run/frr var/lib/frr; do
			mkdir -p "$builddir/frr_install/$d"
			mount -t tmpfs -o mode=1777 tmpfs "$builddir/frr_install/$d"
		done
	fi

	start_frr "" 1
fi
