#!/bin/bash -e
set -euo pipefail
instances=${instances:-}
nicoffset=${OS_AUTOINST_NICOFFSET:-64}
bridge="${bridge:-"br1"}"
ethernet="${ethernet:-""}"
zone="${zone:-"trusted"}"
firewall="${firewall:-"nftables"}"

# Set the MTU of the bridge interface according to https://docs.openvswitch.org/en/latest/faq/issues
# * See https://progress.opensuse.org/issues/151310
# * Use default of 1460 (instead of 1450 as the FAQ suggests) because 1460 should be low enough but
#   is still higher than 1458 which openSUSE MM tests configure within the SUT.
mtu=${mtu:-1460}

usage() {
    cat << EOF
Usage: os-autoinst-setup-multi-machine [OPTIONS]

Options:
 -i, --instances NUM    number of worker instances to configure (auto-detected if omitted)
 -o, --nic-offset NUM   offset for NIC devices (default: 64)
 -b, --bridge NAME      bridge name (default: br1)
 -e, --ethernet DEV     uplink ethernet interface (auto-detected if omitted)
 -z, --zone ZONE        firewall zone (default: trusted)
 -f, --firewall FW      firewall backend: nftables or firewalld (default: nftables)
 -m, --mtu NUM          MTU of the bridge interface (default: 1460)
 -h, --help             display this help
EOF
    exit "$1"
}

opts=$(getopt -o hi:o:b:e:z:f:m: -l help,instances:,nic-offset:,bridge:,ethernet:,zone:,firewall:,mtu: -n "$0" -- "$@") || usage 1
eval set -- "$opts"
while true; do
    case "$1" in
        -i | --instances)
            instances="$2"
            shift 2
            ;;
        -o | --nic-offset)
            nicoffset="$2"
            shift 2
            ;;
        -b | --bridge)
            bridge="$2"
            shift 2
            ;;
        -e | --ethernet)
            ethernet="$2"
            shift 2
            ;;
        -z | --zone)
            zone="$2"
            shift 2
            ;;
        -f | --firewall)
            firewall="$2"
            shift 2
            ;;
        -m | --mtu)
            mtu="$2"
            shift 2
            ;;
        -h | --help) usage 0 ;;
        --)
            shift
            break
            ;;
        *) break ;;
    esac
done

detect_instances() {
    instances=$(systemctl 2> /dev/null | grep --perl-regexp --count 'openqa-worker.*@\d+\.service' || true)
    if [[ -z "$instances" || "$instances" -lt 1 ]]; then
        echo 'Unable to detect the number of instances; specify the number of instances manually via "--instances".' 1>&2
        exit 1
    fi
}

ensure_ip_forwarding() {
    grep -q 1 /proc/sys/net/ipv4/ip_forward || echo -e 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1' > /etc/sysctl.d/ip_forward.conf
}

install_packages() {
    command -v retry > /dev/null || zypper -n in retry
    pkgs=(openvswitch os-autoinst-openvswitch firewalld libcap-progs)
    [[ $network == NetworkManager ]] && pkgs+=(NetworkManager-ovs)
    rpm -q "${pkgs[@]}" > /dev/null || retry -e -s 30 -r 7 -- sh -c "zypper ref && zypper -n in ${pkgs[*]}"
    systemctl enable --now openvswitch os-autoinst-openvswitch
}

configure_firewalld() {
    echo "Now configuring firewalld..."
    systemctl enable --now firewalld
    default_zone=$(firewall-cmd --get-default-zone)
    echo "- default_zone=$default_zone"
    echo "- Creating isotovideo firewall service"
    firewall-cmd --permanent --get-services | grep -q isotovideo && firewall-cmd --permanent --delete-service=isotovideo
    firewall-cmd --permanent --new-service isotovideo
    for i in $(seq 1 "$instances"); do firewall-cmd --permanent --service=isotovideo --add-port=$((i * 10 + 20003))/tcp; done
    firewall-cmd --permanent --zone="$zone" --add-service=isotovideo
    if [[ $default_zone != "$zone" ]]; then
        echo "- Setting new default_zone=$zone"
        firewall-cmd -q --permanent --remove-interface="$ethernet" --zone="$default_zone"
        firewall-cmd --set-default-zone="$zone"
    fi
    for itf in "$bridge" "ovs-system" "$ethernet"; do
        echo "- Adding network interface $itf to zone $zone"
        firewall-cmd -q --permanent --remove-interface="$itf" || :
        firewall-cmd --permanent --zone="$zone" --add-interface="$itf"
    done
    if ! firewall-cmd -q --permanent --query-masquerade --zone="$zone"; then
        echo "- Enabling masquerading for zone $zone"
        firewall-cmd --permanent --add-masquerade --zone="$zone"
    fi
    systemctl restart firewalld
    echo "DONE configuring firewalld!"
}

configure_nftables() {
    rpm -q firewalld && systemctl disable --now firewalld
    cat > /etc/systemd/system/nftables.service << EOF
[Unit]
Description=Apply firewall rules from /etc/firewall.nft
After=network-online.target
Wants=network-online.target
Conflicts=firewalld.service

[Install]
WantedBy=multi-user.target

[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/sbin/nft -f /etc/firewall.nft
ExecStop=/usr/sbin/nft flush ruleset
EOF
    cat > /etc/firewall.nft << EOF
#!/usr/sbin/nft -f

# dump with "nft --stateless list ruleset"

flush ruleset

EOF
    for proto in ip ip6; do
        cat >> /etc/firewall.nft << EOF
table $proto nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        oifname "$ethernet" masquerade
        oifname "$bridge" masquerade
        oifname "ovs-system" masquerade
    }
}

EOF
    done
    systemctl daemon-reload
    systemctl enable --now nftables
}

compute_tap_dev_numbers() {
    local nicoffset_1 nicoffset_2
    nicoffset_1=$((1 * nicoffset))
    nicoffset_2=$((2 * nicoffset))
    mapfile -t tap_dev_numbers < <(
        echo 0
        seq 1 "$instances"
        seq "$nicoffset_1" $(("$nicoffset_1" + instances))
        seq "$nicoffset_2" $(("$nicoffset_2" + instances))
    )
}

create_gre_preup_script() {
    local location=$1
    if [[ -f "$location" ]]; then
        echo "Dispatcher script $location already exists, keeping existing configuration."
        chmod +x "$location"
        return 0
    fi
    cat > "$location" << EOF
#!/bin/sh
OVS_CMD=\$(command -v ovs-vsctl)
if [ -z "\$OVS_CMD" ]; then
    logger -- "Error: ovs-vsctl not found. Skipping script \$0"
    exit 0 # Exit 0 to avoid breaking NetworkManager's flow
fi
TARGET_BRIDGE=\$("\$OVS_CMD" list-br)
# NM_DISPATCHER_ACTION env var is set when NM calls scripts via dispatcher service
# Ref: https://networkmanager.dev/docs/api/1.44.4/NetworkManager-dispatcher.html
# 2. Argument Normalization (NetworkManager vs. wicked)
# NetworkManager: \$1 = interface name, \$2 = action
# wicked: \$1 = action, \$2 = interface name
if [ -n "\$NM_DISPATCHER_ACTION" ]; then
    # Environment detected as NetworkManager
    bridge="\$1"
    action="\$2"
else
    # Environment detected as wicked (or manual call)
    action="\$1"
    bridge="\$2"
fi

# use grep -qx to ensure the bridge exists in the OVS database if more than one ovs bridge are in db
if "\$OVS_CMD" list-br | grep -qx "\$bridge"; then
    if [ "\$action" = "pre-up" ] || [ "\$action" = "up" ]; then
        ovs-vsctl set bridge \$bridge rstp_enable=true
        # TODO add entries according to your network topology
        #ovs-vsctl --may-exist add-port \$bridge gre1 -- set interface gre1 type=gre options:remote_ip=<IP address of other host>
    fi
fi
EOF
    chmod +x "$location"
}

cleanup_networkmanager_connections() {
    local prev_conns=()
    while IFS=: read -r uuid type device name; do
        [[ -z "$uuid" ]] && continue
        if [[ "$type" =~ ^ovs-(bridge|port|interface|slave)$ || "$name" =~ ^ovs-(bridge|port|interface|slave) ]]; then
            prev_conns+=("$uuid")
        elif [[ "$type" == "tun" ]]; then
            if [[ "$device" =~ ^tap[0-9]+$ || "$name" =~ ^tap[0-9]+$ || "$name" =~ ^tun(-tap[0-9]+)?$ || "$name" =~ ^tun-[0-9]+$ ]]; then
                prev_conns+=("$uuid")
            else
                local iface
                iface=$(nmcli -g connection.interface-name con show "$uuid" 2> /dev/null || true)
                if [[ "$iface" =~ ^tap[0-9]+$ ]]; then
                    prev_conns+=("$uuid")
                fi
            fi
        fi
    done < <(nmcli -g UUID,TYPE,DEVICE,NAME con show 2> /dev/null || true)

    if [[ ${#prev_conns[@]} -gt 0 ]]; then
        echo "Deleting ${#prev_conns[@]} previous NetworkManager connections..."
        printf '%s\n' "${prev_conns[@]}" | xargs -r nmcli con del || true
    fi
}

cleanup_ovs_bridge() {
    systemctl start openvswitch
    ovs-vsctl --if-exists del-br "$bridge"
}

setup_multi_machine_with_networkmanager() {
    cleanup_networkmanager_connections
    cleanup_ovs_bridge
    # Clear NM state and timestamps
    systemctl stop NetworkManager
    [[ -d /var/lib/NetworkManager ]] && rm -vf /var/lib/NetworkManager/{timestamps,NetworkManager.state}
    # Restart NM to load ovs plugin
    systemctl start NetworkManager
    # Create bridge, port and interface connection
    nmcli con add type ovs-bridge con-name "ovs-bridge-$bridge" con.int "$bridge" ipv4.dad-timeout 0
    nmcli con add type ovs-port con-name "ovs-port-$bridge" con.int "$bridge" con.master "$bridge"
    nmcli con add type ovs-interface con-name "ovs-interface-$bridge" con.int "$bridge" con.master "$bridge" ipv4.method manual ipv4.address 10.0.2.2/15 ipv4.dad-timeout 0 ipv6.method manual ipv6.address fec0::2/63 ethernet.mtu "$mtu" con.zone "$zone"
    # Create tap interfaces
    local worker_uid nogroup_gid
    worker_uid=$(id -u _openqa-worker)
    nogroup_gid=$(getent group nogroup | cut -f3 -d:)
    for i in "${tap_dev_numbers[@]}"; do
        nmcli con add type ovs-port con-name "ovs-port-tap$i" con.int "tap$i" con.master "$bridge"
        nmcli con add type tun con-name "tap$i" mode tap owner "$worker_uid" group "$nogroup_gid" con.int "tap$i" master "tap$i"
    done
    create_gre_preup_script /etc/NetworkManager/dispatcher.d/gre_tunnel_preup.sh
    nmcli con up "ovs-bridge-$bridge"
    nmcli con up "ovs-interface-$bridge"
}

setup_multi_machine_with_wicked() {
    cleanup_ovs_bridge
    ovs-vsctl add-br "$bridge"
    ovs-vsctl set int "$bridge" mtu_request="$mtu"
    cat > "/etc/sysconfig/network/ifcfg-$bridge" << EOF
BOOTPROTO='static'
IPADDR='10.0.2.2/15'
IPADDR_1='fec0::2/63'
STARTMODE='auto'
ZONE="$zone"
OVS_BRIDGE='yes'
PRE_UP_SCRIPT="wicked:gre_tunnel_preup.sh"
OVS_BRIDGE_PORT_DEVICE_0='tap0'
EOF
    cat > /etc/sysconfig/network/ifcfg-tap0 << EOF
BOOTPROTO='none'
IPADDR=''
NETMASK=''
PREFIXLEN=''
STARTMODE='auto'
TUNNEL='tap'
TUNNEL_SET_GROUP='nogroup'
TUNNEL_SET_OWNER='_openqa-worker'
EOF
    for i in "${tap_dev_numbers[@]}"; do ln -sf ifcfg-tap0 "/etc/sysconfig/network/ifcfg-tap$i" && echo "OVS_BRIDGE_PORT_DEVICE_$i='tap$i'" >> "/etc/sysconfig/network/ifcfg-$bridge"; done
    create_gre_preup_script /etc/wicked/scripts/gre_tunnel_preup.sh

    # ensure the zone of the uplink/ethernet device is set in accordance as well
    sed -i -e "s/ZONE=.*/ZONE=$zone/g" "/etc/sysconfig/network/ifcfg-$ethernet"
    wicked ifup "$ethernet" "$bridge"
}

delete_tap_ifaces() {
    for dev in /sys/class/net/tap*; do
        [[ -e "$dev" ]] || continue
        local iface="${dev##*/}"
        if [[ -f "$dev/tun_flags" ]]; then
            ip link delete "$iface" 2> /dev/null || true
        fi
    done
}

cleanup_openvswitch_db() {
    # Delete existing ovs db and its lock but do not delete any ovs db backup
    systemctl stop openvswitch os-autoinst-openvswitch
    [[ -d /var/lib/openvswitch/ ]] && rm -f /var/lib/openvswitch/{conf.db,.conf.db.~lock~}
}

configure_openvswitch() {
    echo "OS_AUTOINST_USE_BRIDGE=$bridge" > /etc/sysconfig/os-autoinst-openvswitch
    systemctl enable os-autoinst-openvswitch
    systemctl restart os-autoinst-openvswitch
}

determine_ethernet_interface() {
    ethernet=${ethernet:-$(ip -json route show default | jq -r 'sort_by(.metric)|.[0].dev')}
}

main() {
    if [[ -z "${instances}" ]]; then
        detect_instances
        echo "Auto-detected $instances worker instances"
    else
        echo "Assuming $instances worker instances as specified"
    fi
    if [[ "$instances" -ge "$nicoffset" ]]; then
        echo "The NIC offset must be greater than the number of instances."
        exit 2
    fi
    network=
    if [[ -L /etc/systemd/system/network.service ]]; then
        network=$(basename -s .service "$(readlink /etc/systemd/system/network.service)")
    fi
    if [[ -z "${network}" ]]; then
        if systemctl is-enabled NetworkManager.service &> /dev/null; then
            network=NetworkManager
        elif systemctl is-enabled wicked.service &> /dev/null; then
            network=wicked
        fi
    fi
    test "$network" = wicked -o "$network" = NetworkManager || {
        echo "This script only works with wicked network daemon or NetworkManager"
        exit 1
    }
    compute_tap_dev_numbers
    ensure_ip_forwarding
    install_packages
    determine_ethernet_interface
    if [[ "${firewall}" == "firewalld" ]]; then
        configure_firewalld
    else
        configure_nftables
    fi
    cleanup_openvswitch_db
    delete_tap_ifaces
    case "$network" in
        wicked)
            setup_multi_machine_with_wicked
            ;;
        NetworkManager)
            setup_multi_machine_with_networkmanager
            ;;
    esac
    configure_openvswitch
}

caller 0 > /dev/null || main "$@"
