#!/bin/bash
# ifplugd Link Tracking
#
# Purpose:
#   Triggered when a watch interface link state changes.
#   When below min link threshold:
#     - applies protodown on target interfaces or notifies the control plane
#       based on per-group state-change-action.
#   When threshold is met or exceeded:
#     - clears protodown or notifies the control plane based on state-change-action.
#
# Args:
#   $1 = interface name (e.g., swp1) when invoked by ifplugd via run-parts,
#        or --all-groups when called by ifplugd-resync-config-change after config change.
#   $2 = up|down when invoked by ifplugd (unused; operstate is checked directly).
#
# Config:
#   /etc/ifplugd/link-tracking.conf
#   Format: GROUP_ID|WATCH_INTERFACES|TARGET_INTERFACES|MIN_LINKS|state-change-action
#   state-change-action: protodown-target-interface or control-plane-action
#
# Logging:
#   Log tag: ifplugd-link-tracking

set -e

CONFIG_FILE="/etc/ifplugd/link-tracking.conf"
LOGGER="logger -t ifplugd-link-tracking"
PROTODOWN_REASON="link-tracking"
STATE_CHANGE_ACTION_PROTODOWN_TARGET="protodown-target-interface"

# When called by our sync script after config change, $1 is --all-groups.
# When called by ifplugd on link change, $1 is the interface name.
if [ "$1" != "--all-groups" ]; then
    INTERFACE="$1"
fi


# Count how many watch interfaces in a group are operationally up.
# $1 = comma-separated watch interface names (e.g. "swp1,swp2,swp3")
count_watch_interfaces_up() {
    local watch_list="$1"
    local up_count=0
    local operstate_file

    IFS=',' read -ra WATCH_ARRAY <<< "$watch_list"
    for iface in "${WATCH_ARRAY[@]}"; do
        operstate_file="/sys/class/net/$iface/operstate"

        if [ -r "$operstate_file" ] && [ "$(cat "$operstate_file" 2>/dev/null)" = "up" ]; then
            up_count=$((up_count + 1))
        fi
    done

    echo "$up_count"
}


get_protodown_reasons() {
    ip -d link show "$1" 2>/dev/null | grep -oP 'protodown_reason <\K[^>]+' || true
}


is_protodown_on() {
    ip link show "$1" 2>/dev/null | grep -q "protodown on"
}


clear_target_interface_protodown() {
    if ip link set dev "$2" protodown off 2>/dev/null; then
        $LOGGER "Group $1: Protodown turned off for target interface $2 ($3)"
    else
        $LOGGER "Group $1: Failed to turn protodown off for target interface $2"
    fi
}


protodown_target_interfaces() {
    IFS=',' read -ra TARGET_ARRAY <<< "$2"

    for target_iface in "${TARGET_ARRAY[@]}"; do
        if [ ! -d "/sys/class/net/$target_iface" ]; then
            $LOGGER "Group $1: Target interface $target_iface does not exist, skipping"
            continue
        fi

        reasons=$(get_protodown_reasons "$target_iface")
        if [ "$3" = "on" ]; then
            if echo "$reasons" | grep -qw "$PROTODOWN_REASON"; then
                $LOGGER "Group $1: Target interface $target_iface already has protodown set (reason: $PROTODOWN_REASON)"
                continue
            fi

            if is_protodown_on "$target_iface"; then
                if ip link set dev "$target_iface" protodown_reason "$PROTODOWN_REASON" on 2>/dev/null; then
                    $LOGGER "Group $1: Added $PROTODOWN_REASON reason to existing protodown on $target_iface"
                else
                    $LOGGER "Group $1: Failed to add $PROTODOWN_REASON reason for $target_iface"
                fi
            else
                if ip link set dev "$target_iface" protodown on protodown_reason "$PROTODOWN_REASON" on 2>/dev/null; then
                    $LOGGER "Group $1: Protodown set ON for target interface $target_iface (reason: $PROTODOWN_REASON)"
                else
                    $LOGGER "Group $1: Failed to set protodown ON for target interface $target_iface"
                fi
            fi

        else
            $LOGGER "Group $1: Watch interface restored, clearing $PROTODOWN_REASON protodown reason for target interface $target_iface"

            if echo "$reasons" | grep -qw "$PROTODOWN_REASON"; then
                if ip link set dev "$target_iface" protodown_reason "$PROTODOWN_REASON" off 2>/dev/null; then
                    $LOGGER "Group $1: $PROTODOWN_REASON reason cleared for $target_iface"
                else
                    $LOGGER "Group $1: Failed to clear $PROTODOWN_REASON reason for $target_iface"
                fi

                updated=$(get_protodown_reasons "$target_iface")
                if [ -z "$updated" ]; then
                    clear_target_interface_protodown "$1" "$target_iface" "$PROTODOWN_REASON cleared, no other reasons"
                fi

            elif [ -n "$reasons" ]; then
                $LOGGER "Group $1: Target interface $target_iface has other protodown reasons (<$reasons>), leaving protodown on"

            elif is_protodown_on "$target_iface"; then
                clear_target_interface_protodown "$1" "$target_iface" "no reasons"

            else
                $LOGGER "Group $1: Target interface $target_iface already has protodown off, no change"
            fi
        fi
    done
}


control_plane_notify() {
    $LOGGER "Group $1: control-plane-action $3 for target interfaces: $2"
    # TODO: notify FRR/zebra (e.g. vtysh or socket) when implementing
}


apply_group_action() {
    if [ "$4" = "$STATE_CHANGE_ACTION_PROTODOWN_TARGET" ]; then
        protodown_target_interfaces "$1" "$2" "$3"
    else
        control_plane_notify "$1" "$2" "$3"
    fi
}


apply_or_clear_target_interface_action_by_threshold() {
    local up_count
    up_count=$(count_watch_interfaces_up "$2")

    if [ "$up_count" -lt "${4:-1}" ]; then
        $LOGGER "Group $1: $up_count watch interface(s) up (min_links=${4:-1}), applying action to target interfaces: $3"
        apply_group_action "$1" "$3" "on" "$5"
    else
        $LOGGER "Group $1: $up_count watch interface(s) up (min_links=${4:-1}), clearing action on target interfaces: $3"
        apply_group_action "$1" "$3" "off" "$5"
    fi
}


process_tracking_groups() {
    local mode="$1"
    local logged_all_groups=

    if [ "$mode" = "interface" ]; then
        $LOGGER "Interface $INTERFACE link state changed, checking link tracking groups"
    fi

    while IFS='|' read -r group_id watch_ifaces target_interfaces min_links state_change_action; do
        [ -z "$group_id" ] && continue
        [[ "$group_id" =~ ^#.*$ ]] && continue

        min_links=${min_links:-1}
        state_change_action=${state_change_action:-$STATE_CHANGE_ACTION_PROTODOWN_TARGET}

        if [ "$mode" = "interface" ]; then
            echo "$watch_ifaces" | grep -qw "$INTERFACE" || continue
            $LOGGER "Group $group_id: Watch interface $INTERFACE is part of this group (min_links=$min_links)"
        fi

        [ -z "$target_interfaces" ] && continue

        if [ "$mode" = "all" ] && [ -z "$logged_all_groups" ]; then
            $LOGGER "Processing all link tracking groups"
            logged_all_groups=1
        fi

        apply_or_clear_target_interface_action_by_threshold \
            "$group_id" "$watch_ifaces" "$target_interfaces" "$min_links" "$state_change_action"
    done < "$CONFIG_FILE"
}


if [ ! -f "$CONFIG_FILE" ]; then
    $LOGGER "Configuration file $CONFIG_FILE not found"
    exit 0
fi

if [ "$1" = "--all-groups" ]; then
    process_tracking_groups all
else
    process_tracking_groups interface
fi

exit 0
