#! /bin/bash
#
# Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# This software product is a proprietary product of Mellanox Technologies Ltd.
# (the "Company") and all right, title, and interest in and to the software
# product, including all associated intellectual property rights, are and
# shall remain exclusively with the Company.
#
# This software product is governed by the End User License Agreement
# provided with the software product.
#

# Supported Mellanox PCI device IDs (vendor 15b3):
#   a2d2 = ConnectX-5
#   a2d6 = ConnectX-5 Ex
#   a2dc = ConnectX-6 Dx / BlueField-3 (DPA)
#   a2df = BlueField-4 (DPA)
#   101d = ConnectX-7
#   1021 = ConnectX-8 (DPA)
#   1025 = ConnectX-9 (DPA)
DEVICE_ID_REGEX='a2d[26cf]|101d|1021|1025'

# DPA-capable devices (need dpa.provider generation)
DPA_DEVICE_REGEX='a2d[cf]|1021|1025'

# Helper functions for JSON manipulation
# These use jq (a real JSON parser); jq is a runtime dependency of the package.
# The previous sed/awk fallback handled JSON as line-oriented text and corrupted
# valid multi-line configs (Redmine 5098963), so it was removed.

# Read a JSON field value
json_get() {
	local file="$1"
	local field="$2"
	local default="$3"

	if [[ ! -f "$file" ]]; then
		echo "${default:-null}"
		return
	fi

	# Capture jq's exit status separately from its output so an unparseable or
	# unreadable file is reported as a failure (return 1) rather than being
	# silently treated as a missing field. Only an empty result from a
	# successful jq run maps to the default.
	local value
	if ! value=$(jq -r ".${field} // empty" "$file" 2>/dev/null); then
		echo "Error: failed to read JSON field '${field}' from $file" >&2
		return 1
	fi
	if [[ -z "$value" ]]; then
		echo "${default:-null}"
	else
		echo "$value"
	fi
}

# Set a JSON field value
json_set() {
	local file="$1"
	local field="$2"
	local value="$3"

	# Integer values (e.g. single_port) must be written as JSON numbers, not
	# strings, or the controller's strict validator rejects them. Everything
	# else is written as a JSON string.
	local jqval=(--arg v "$value")
	if [[ "$value" =~ ^-?[0-9]+$ ]]; then
		jqval=(--argjson v "$value")
	fi

	# Stage jq output in a temp file in the destination directory so the final
	# mv is a same-filesystem atomic rename and a jq failure never truncates
	# the target.
	local tmp
	tmp=$(mktemp "$(dirname "$file")/.virtnet.conf.XXXXXX" 2>/dev/null)
	if [[ -z "$tmp" ]]; then
		echo "Error: failed to create temp file for $file" >&2
		return 1
	fi

	if [[ -f "$file" ]]; then
		# Update existing file. mktemp creates the temp as 0600 owned by us, so
		# preserve the original file's mode and ownership across the atomic
		# replace; otherwise the config would silently become 0600 root-only.
		chmod --reference="$file" "$tmp" 2>/dev/null
		chown --reference="$file" "$tmp" 2>/dev/null
		if jq "${jqval[@]}" ".${field} = \$v" "$file" > "$tmp" 2>/dev/null && mv -f "$tmp" "$file"; then
			return
		fi
	else
		# Create new file with the conventional config mode (mktemp is 0600;
		# this matches the umask-based mode a direct redirect would produce).
		chmod 0644 "$tmp" 2>/dev/null
		if echo "{}" | jq "${jqval[@]}" ".${field} = \$v" > "$tmp" 2>/dev/null && mv -f "$tmp" "$file"; then
			return
		fi
	fi
	rm -f "$tmp"
	echo "Error: failed to update $field in $file" >&2
	return 1
}

# Parse command-line arguments
DEBUG_MODE=0
while [[ $# -gt 0 ]]; do
	case "$1" in
		--debug|-d)
			DEBUG_MODE=1
			shift
			;;
		--help|-h)
			echo "Usage: $0 [--debug]"
			echo "  --debug, -d    Enable debug output"
			exit 0
			;;
		*)
			echo "Unknown option: $1"
			echo "Usage: $0 [--debug]"
			exit 1
			;;
	esac
done

mftconfig=mstconfig
if [ -x /usr/bin/mlxconfig ]; then
	mftconfig=mlxconfig
fi

devlink=devlink
if [ -x /opt/mellanox/iproute2/sbin/devlink ]; then
	devlink=/opt/mellanox/iproute2/sbin/devlink
fi

get_eswitch_mode()
{
	pci_dev=$1
	shift

	$devlink dev eswitch show pci/${pci_dev} 2> /dev/null | cut -d ' ' -f 3
}

pci=`lspci -nD -d 15b3: | grep -E "$DEVICE_ID_REGEX" | head -n 1`
emu_manager=`echo $pci | cut -d ' ' -f 1`

if [[ -f /opt/mellanox/mlnx_virtnet/virtnet.conf ]]; then
	_emu_manager=$(json_get "/opt/mellanox/mlnx_virtnet/virtnet.conf" "ib_dev_p0" "")
	if [[ ! -z $_emu_manager ]] && [[ $_emu_manager != "null" ]]; then
		# Check if the infiniband device path exists
		if [[ -L "/sys/class/infiniband/$_emu_manager/device" ]]; then
			device_path=$(readlink "/sys/class/infiniband/$_emu_manager/device")
			if [[ ! -z "$device_path" ]]; then
				emu_manager=`basename "$device_path"`
			fi
		fi
	fi
fi

if [[ -z $emu_manager ]]; then
	echo "Error: no valid emulation manager is defined"
	exit 1
fi

# Debug output only when --debug flag is passed (avoids noise in systemd logs)
if [[ $DEBUG_MODE -eq 1 ]]; then
	echo "mftconfig: $mftconfig"
	echo "emu_manager: $emu_manager"
	echo "VIRTIO_NET_EMU_MNG_ENABLE: $($mftconfig -d ${emu_manager} -e q VIRTIO_NET_EMU_MNG_ENABLE)"
	echo "VIRTIO_NET_EMULATION_ENABLE: $($mftconfig -d ${emu_manager} -e q VIRTIO_NET_EMULATION_ENABLE)"
	echo "eswitch_mode: $($devlink dev eswitch show pci/${emu_manager} 2>/dev/null)"
fi

is_emu_per_pf=$($mftconfig -d ${emu_manager} -e q VIRTIO_NET_EMU_MNG_ENABLE |
	grep -o "VIRTIO_NET_EMU_MNG_ENABLE.*" | awk '{print $3}' | grep True)
if [[ -z $is_emu_per_pf ]]; then
	is_emu_global=$($mftconfig -d ${emu_manager} -e q VIRTIO_NET_EMULATION_ENABLE |
		grep -o "VIRTIO_NET_EMULATION_ENABLE.*" | awk '{print $3}' | grep True)
	if [[ -z $is_emu_global ]]; then
		echo "${emu_manager} doesn't have VIRTIO_NET_EMULATION_ENABLE or VIRTIO_NET_EMU_MNG_ENABLE set"
		exit 250
	fi
fi

eswitch_mode="get_eswitch_mode ${emu_manager}"
COUNT=0
until [ $COUNT -eq 120 ] || [ "`${eswitch_mode}`" == "switchdev" ]; do
	sleep 1
	echo "Waiting for device ${emu_manager} to be switchdev mode for ${COUNT} seconds"
	let $(( COUNT++ ))
done

if [ "`${eswitch_mode}`" != "switchdev" ]; then
	echo "${emu_manager} is not in switch dev mode"
	exit 2
fi

config_file="/opt/mellanox/mlnx_virtnet/virtnet.conf"
p2=`$mftconfig -d ${emu_manager} -e q LINK_TYPE_P2`
if [[ -n `echo "$p2" 2> /dev/null | grep "Unknown Parameter"` ]]; then
	echo "Single port NIC"
	json_set "$config_file" "single_port" "1" || {
		echo "Error: failed to set single_port in $config_file" >&2
		exit 1
	}
fi

if [[ -z `modinfo mlxdevm 2>&1 | grep -E "ERROR.*mlxdevm not found"` ]]; then
	if [[ -f $config_file ]]; then
		current_sf_provider=$(json_get "$config_file" "sf_provider" "")
		if [[ "$current_sf_provider" != "devlink" ]]; then
			echo "mlxdevm as SF provider"
			json_set "$config_file" "sf_provider" "mlxdevm" || {
				echo "Error: failed to set sf_provider in $config_file" >&2
				exit 1
			}
		else
			echo "mlxdevm available, keeping existing devlink SF provider"
		fi
	fi
else
	echo "devlink as SF provider"
	json_set "$config_file" "sf_provider" "devlink" || {
		echo "Error: failed to set sf_provider in $config_file" >&2
		exit 1
	}
fi

# Check if device supports DPA (ConnectX-6 Dx, CX8, CX9)
dpa_device=`echo $pci | grep -E "$DPA_DEVICE_REGEX"`

if [[ -n $dpa_device ]]; then
	if [[ ! -e /opt/mellanox/mlnx_virtnet/providers/dpa.provider ]]; then
		echo "Generating default dpa.provider for DPA-capable device"
		echo -e "Provider=libprovider-dpa\nScore=200" > \
		      /opt/mellanox/mlnx_virtnet/providers/dpa.provider
	fi
fi

recovery_dir="/opt/mellanox/mlnx_virtnet/recovery"

is_lag_cur=0
is_lag_cur_valid=1
if [[ -f $config_file ]]; then
	if ! is_lag_cur=$(json_get "$config_file" "is_lag" "0"); then
		echo "Could not read is_lag from $config_file (unreadable/invalid JSON). Skipping recovery purge/update; controller will fail config validation."
		is_lag_cur_valid=0
	fi
fi

if [[ $is_lag_cur_valid -eq 1 && "$is_lag_cur" != "0" && "$is_lag_cur" != "1" ]]; then
	echo "Invalid is_lag value (${is_lag_cur}) in $config_file; expected 0 or 1. Skipping recovery purge/update; controller will fail config validation."
	is_lag_cur_valid=0
fi

mkdir -p ${recovery_dir}
if [[ -f "${recovery_dir}/.virtnet_conf_save" ]]; then
	is_lag_save_valid=1
	if ! is_lag_save=$(json_get "${recovery_dir}/.virtnet_conf_save" "is_lag" "0"); then
		echo "Could not read is_lag from ${recovery_dir}/.virtnet_conf_save (unreadable/invalid JSON). Skipping recovery purge/update."
		is_lag_save_valid=0
	elif [[ "$is_lag_save" != "0" && "$is_lag_save" != "1" ]]; then
		is_lag_save_valid=0
	fi

	if [[ $is_lag_cur_valid -eq 1 && $is_lag_save_valid -eq 1 && "$is_lag_save" != "$is_lag_cur" ]]; then
		cd $recovery_dir
		find . -type f -not \( -name ".virtnet_conf_save" -or -name ".mlxconfig_save" \) -delete
	fi
fi

if [[ -f $config_file && $is_lag_cur_valid -eq 1 ]]; then
	cp "$config_file" "${recovery_dir}/.virtnet_conf_save"
elif [[ ! -f $config_file ]]; then
	echo '{"is_lag": 0}' > "${recovery_dir}/.virtnet_conf_save"
fi

curr_map_count=`cat /proc/sys/vm/max_map_count`

#Define map threshold value to 131060 (65530 * 2) to support upto 2K devices
max_map_threshold=131060

if [[ "$curr_map_count" -lt "$max_map_threshold" ]]; then
	echo $max_map_threshold > /proc/sys/vm/max_map_count
fi

fields=(PF_BAR2_ENABLE \
	HIDE_PORT2_PF \
	PER_PF_NUM_SF \
	SRIOV_EN \
	PF_SF_BAR_SIZE \
	PF_TOTAL_SF)

fields_emu_per_pf=(VIRTIO_NET_EMU_MNG_ENABLE \
	PCI_SWITCH_EMU_MNG_ENABLE \
	PCI_SWITCH_EMU_MNG_NUM_PORT \
	VIRTIO_NET_EMU_MNG_NUM_VF \
	VIRTIO_NET_EMU_MNG_NUM_PF \
	VIRTIO_NET_EMU_MNG_NUM_MSIX)

fields_emu_global=(VIRTIO_NET_EMULATION_ENABLE \
	PCI_SWITCH_EMULATION_ENABLE \
	PCI_SWITCH_EMULATION_NUM_PORT \
	VIRTIO_NET_EMULATION_NUM_VF \
	VIRTIO_NET_EMULATION_NUM_PF \
	VIRTIO_NET_EMULATION_NUM_MSIX)

if [[ -z $is_emu_per_pf ]]; then
	fields=("${fields[@]}" "${fields_emu_global[@]}")
else
	fields=("${fields[@]}" "${fields_emu_per_pf[@]}")
fi
configs=$($mftconfig -d ${emu_manager} -e q "${fields[@]}")

fields_str=""
for f in ${fields[@]};
do
	if [[ $fields_str != "" ]]; then
		fields_str="${fields_str}|${f}"
	else
		fields_str="${f}"
	fi
done

mlxconfig_cur=`echo "$configs" | grep -E $fields_str | tr -s ' '`

if [[ ! -f "${recovery_dir}/.mlxconfig_save" ]]; then
	mkdir -p ${recovery_dir}
	echo "$mlxconfig_cur" > ${recovery_dir}/.mlxconfig_save
	exit 0
fi

mlxconfig_save=`cat ${recovery_dir}/.mlxconfig_save | grep -E $fields_str | tr -s ' '`

for f in ${fields[@]};
do
	val_save=`echo "$mlxconfig_save" | grep -o "$f.*" | awk '{print $3}'`
	val_cur=`echo "$mlxconfig_cur" | grep -o "$f.*" | awk '{print $3}'`
	if [[ "$val_save" != "$val_cur" ]]; then
		echo "$mlxconfig_cur" > ${recovery_dir}/.mlxconfig_save
		cd $recovery_dir
		find . -type f -not \( -name ".virtnet_conf_save" -or -name ".mlxconfig_save" \) -delete
		exit 0
	fi
done

exit 0
