#!/bin/sh
# Copyright (C) 2025 iopsys Software Solutions AB
# Author: IMEN Bhiri <imen.bhiri@pivasoftware.com>
# Author: AMIN Ben Romdhane <amin.benromdhane@iopsys.eu>
# Author: Husaam Mehdi <husaam.mehdi@iopsys.eu>

. /usr/share/libubox/jshn.sh

ROOT="$(dirname "${0}")"
. "${ROOT}"/bbf_api

DOWNLOAD_TIMEOUT=1800

get_free_port() {
	local base max max_attempts attempt port pid

	base=40000
	max=60000
	max_attempts=30
	attempt=0

	while [ "${attempt}" -lt "${max_attempts}" ]; do
		attempt=$((attempt + 1))

		# Generate random port
		port=$((base + (RANDOM % (max - base + 1))))

		# Try to bind the port (TCP)
		socat TCP-LISTEN:${port},reuseaddr - >/dev/null 2>&1 &
		pid=$!

		# Give kernel a moment
		sleep 0.05

		# If still running, port is free
		if kill -0 "${pid}" 2>/dev/null; then
			# Export pid so caller can release it later
			kill "${pid}" 2>/dev/null
			echo "${port}"
			return 0
		fi

		# Otherwise, make sure process is gone
		kill "${pid}" 2>/dev/null
	done

	logger -p err -t "bbf_download" "Failed to reserve a free port after ${max_attempts} attempts"
	return 1
}

get_download_status() {
	local exitcode="${1}"
	local state="Complete"

	[ "${exitcode}" != "0" ] && {
		state="Error_Other"
	}

	[ "${exitcode}" = "6" ] && {
		state="Error_CannotResolveHostName"
	}

	[ "${exitcode}" = "7" ] && {
		state="Error_InitConnectionFailed"
	}

	[ "${exitcode}" = "22" ] && {
		state="Error_NoResponse"
	}

	[ "${exitcode}" = "27" ] && {
		state="Error_IncorrectSize"
	}

	[ "${exitcode}" = "28" ] && {
		state="Error_Timeout"
	}

	echo "$state"
}

download_error() {
	local status
	local lock_file="/var/lock/tr143_download_error.lock"

	# open lock file
	exec 211>"${lock_file}" || exit 1

	# acquire exclusive lock (non-blocking)
	flock -n 211 || {
		logger -t "bbf_download" "download_error: lock already held, return"
		exec 211>&-
		exit 1
	}

	# ---- critical section starts ----

	status=$(uci_get_bbf_dmmap dmmap_diagnostics.download.Status)
	[ "${status}" = "complete" ] && {
		logger -t "bbf_download" "download_error: Status already complete, return"
		exec 211>&-
		exit 1
	}

	json_init
	json_add_string "Status" "${1}"
	json_dump

	# store data in dmmap_diagnostics for both protocols (cwmp/usp)
	[ "${2}" = "both_proto" ] && {
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.DiagnosticState="${1}"
	}

	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="complete"
	$UCI_COMMIT_BBF_DMMAP

	# ---- critical section ends ----

	exec 211>&-

	exit 1
}

download_launch() {
	input="${1}"

	# load provided input
	json_load "${input}"

	# get the values
	json_get_var url url
	json_get_var iface iface
	json_get_var dscp dscp
	json_get_var eth_prio eth_prio
	json_get_var ip_proto ip_proto
	json_get_var num_of_con num_of_con
	json_get_var enable_per_con enable_per_con
	json_get_var proto proto

	# Check if a download process is already running
	download_s=$(uci_get_bbf_dmmap dmmap_diagnostics.download)
	# download not running, add to bbfdm dmmap
	if [ -z "${download_s}" ]; then
		[ ! -f /etc/bbfdm/dmmap/dmmap_diagnostics ] && touch /etc/bbfdm/dmmap/dmmap_diagnostics
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download='download'
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="running"
	else
		# check if running
		Status=$(uci_get_bbf_dmmap dmmap_diagnostics.download.Status)
		[ "${Status}" = "running" ] && {
			logger -t "bbf_download" "Download running, return"
			return
		}

		# set running
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="running"
	fi

	# Cleanup per-connection results
	sections="$(${UCI_SHOW_BBF_DMMAP} dmmap_diagnostics | awk -F'=' '/=DownloadPerConnection$/ {print $1}' | sort -r)"
	for sec in ${sections}; do
		[ -z "${sec}" ] && continue
		$UCI_DELETE_BBF_DMMAP "${sec}"
	done

	$UCI_COMMIT_BBF_DMMAP

	# Fail if url is empty
	[ -z "${url}" ] && {
		logger -p err -t "bbf_download" "No url provided"
		download_error "Error_InitConnectionFailed" "${proto}"
	}

	[ "${url:0:7}" != "http://" ] && [ "${url:0:6}" != "ftp://" ] && [ "${url:0:8}" != "https://" ] && {
		logger -p err -t "bbf_download" "Invalid url protocol"
		download_error "Error_Other" "${proto}"
	}

	if [ -n "${iface}" ]; then
		device=$(ifstatus "${iface}" | jsonfilter -e '@.l3_device')
	else
		device=$(route -n | grep 'UG[ \t]' | awk '{print $8}')
	fi

	# If no device was found, return error
	[ -z "${device}" ] && {
		logger -p err -t "bbf_download" "Could not find device to use"
		download_error "Error_NoRouteToHost" "${proto}"
	}

	ip_addr_used=$(get_ip_addr_used "${url}" "${ip_proto}" "${iface}")

	# Assign default value
	if [ "$ip_proto" = "IPv4" ]; then
		ip_proto_flag="--ipv4"
	elif [ "$ip_proto" = "IPv6" ]; then
		ip_proto_flag="--ipv6"
	else
		ip_proto_flag=""
	fi

	format='{ "size_download": "%{size_download}",
			  "size_header": "%{size_header}",
			  "time_appconnect": "%{time_appconnect}",
			  "time_connect": "%{time_connect}",
			  "time_pretransfer": "%{time_pretransfer}",
			  "time_starttransfer": "%{time_starttransfer}",
			  "time_total": "%{time_total}",
			  "exitcode": "%{exitcode}" }'

	# Temp dir for workers
	TMPDIR=$(mktemp -d /tmp/bbfdm_dl.XXXXXX)
	if [ -z "${TMPDIR}" ]; then
		logger -p err -t "bbf_download" "Could not create tmp dir, return"
		download_error "Error_Other" "${proto}"
	fi
	trap 'rm -rf "${TMPDIR}"' EXIT TERM INT

	# Ensure num_of_con has sensible default
	[ -z "${num_of_con}" ] && num_of_con=1

	logger -t "bbf_download" "Starting parallel download: url=${url} con=${num_of_con}"

	# launch parallel workers
	i=0
	while [ "${i}" -lt "${num_of_con}" ]; do
		idx=$((i+1))

		port="$(get_free_port)"
		if [ -z "${port}" ]; then
			logger -p err -t "bbf_download" "Could not get a free port"
			download_error "Error_Other" "${proto}"
		fi

		worker_meta="${TMPDIR}/worker_${idx}.meta"

		# worker subshell
		(
			time_start=$(date +"%s.282646") # It should be like that time_start=$(date +"%s.%6N") but since OpenWrt busybox has limitations and doesn't support nanoseconds so keep it hardcoded

			logger -t "bbf_download" "Calling curl with port ${port} in worker ${i}"

			if [ -z "${ip_proto_flag}" ]; then
				curl_output="$(curl --fail --silent --local-port "${port}" --max-time "${DOWNLOAD_TIMEOUT}" -w "${format}" "${url}" --output /dev/null 2>/dev/null)"
			else
				curl_output="$(curl ${ip_proto_flag} --fail --silent --local-port "${port}" --max-time "${DOWNLOAD_TIMEOUT}" -w "${format}" "${url}" --output /dev/null 2>/dev/null)"
			fi

			# find conntrack line that contains port and the local IP as src (client side)
			# first grep by sport=<port>, then ensure the local ip appears in the line as src=<ip_addr_used>
			conntrack_line=$(conntrack -L 2>/dev/null | grep -E "sport=${port}[[:space:]]" | grep -F "src=${ip_addr_used}" | head -n1 2>/dev/null || true)

			# check if we got any output
			if [ -z "${curl_output}" ]; then
				logger -p err -t "bbf_download" "Curl produced no output"
				download_error "Error_Other" "${proto}"
			fi

			logger -t "bbf_download" "########### Downloaded: ${url} ###########"
			json_load "${curl_output}" 2>/dev/null || true
			json_get_var size_download size_download 2>/dev/null || size_download=0
			json_get_var size_header size_header 2>/dev/null || size_header=0
			json_get_var time_appconnect time_appconnect 2>/dev/null || time_appconnect=0
			json_get_var time_connect time_connect 2>/dev/null || time_connect=0
			json_get_var time_pretransfer time_pretransfer 2>/dev/null || time_pretransfer=0
			json_get_var time_starttransfer time_starttransfer 2>/dev/null || time_starttransfer=0
			json_get_var time_total time_total 2>/dev/null || time_total=0
			json_get_var exitcode exitcode 2>/dev/null || exitcode=99

			# if curl failed or time_total is 0, kill all parents and workers
			if [ "${time_total}" = "0" ] || [ "${exitcode}" != "0" ]; then
				[ -z "${exitcode}" ] && exitcode=99

				final_status="$(get_download_status "${exitcode}")"
				logger -p err -t "bbf_download" "Time total: ${time_total} with status: ${final_status} in worker ${idx}"

				download_error "${final_status}" "${proto}"
			fi

			# compute per-connection timestamps as epoch floats (float addition)
			tcp_open_request_epoch=$(echo "${time_start}" "${time_appconnect}" | awk '{printf "%.6f", $1 + $2}')
			tcp_open_response_epoch=$(echo "${time_start}" "${time_connect}" | awk '{printf "%.6f", $1 + $2}')
			rom_epoch=$(echo "${time_start}" "${time_pretransfer}" | awk '{printf "%.6f", $1 + $2}')
			bom_epoch=$(echo "${time_start}" "${time_starttransfer}" | awk '{printf "%.6f", $1 + $2}')
			eom_epoch=$(echo "${time_start}" "${time_total}" | awk '{printf "%.6f", $1 + $2}')

			# compute test_rx_bytes from curl reported sizes
			test_rx_bytes=$((size_download + size_header))

			# ----------------------------
			# conntrack parsing to get bytes seen on the network for this local port, that is, this particular worker
			# ----------------------------
			upload_bytes=0
			download_bytes=0
			if [ -n "${conntrack_line}" ]; then
				# extract first two bytes= occurrences
				# First bytes= is upload (client -> server)
				# Second bytes= is download (server -> client)
				u=$(echo "${conntrack_line}" | grep -o "bytes=[0-9]*" | sed -n '1p' | cut -d= -f2)
				d=$(echo "${conntrack_line}" | grep -o "bytes=[0-9]*" | sed -n '2p' | cut -d= -f2)

				# sanity check
				if [ -n "${u}" ] && [ -n "${d}" ]; then
					upload_bytes="${u}"
					download_bytes="${d}"
				fi
			fi

			# sometimes, conntrack shows very few bytes in the stats, in that case, fall back to curl data
			if [ "${download_bytes}" -lt "${test_rx_bytes}" ]; then
				download_bytes="${test_rx_bytes}"
			fi

			# write worker meta JSON with epoch floats for aggregator
			cat > "${worker_meta}" <<EOF
{
  "rom_epoch": "${rom_epoch}",
  "bom_epoch": "${bom_epoch}",
  "eom_epoch": "${eom_epoch}",
  "tcp_open_request_epoch": "${tcp_open_request_epoch}",
  "tcp_open_response_epoch": "${tcp_open_response_epoch}",
  "TestBytesReceived": ${test_rx_bytes},
  "TotalBytesReceived": ${download_bytes},
  "TotalBytesSent": ${upload_bytes},
  "size_download": ${size_download},
  "size_header": ${size_header}
}
EOF

		) &

		i=$((i+1))
	done

	# wait for all workers
	wait

	# ----------------------------
	# Aggregation with epoch floats
	# ----------------------------

	# Helper function to convert epoch float to ISO format
	epoch_float_to_iso() {
		local epoch_float="$1"
		local x=${epoch_float%%[.]*}
		local separator_idx=$((${#x}+1))
		local microsec=${epoch_float:$separator_idx}
		local sec=${epoch_float:0:$((separator_idx-1))}
		date -u +"%Y-%m-%dT%H:%M:%S.${microsec}Z" -d @"${sec}"
	}

	# Initialize tracking variables
	total_test_bytes=0
	total_bytes_received=0
	total_bytes_sent=0
	rom_epoch_min=""
	bom_epoch_min=""
	eom_epoch_max=""
	tcp_request_epoch_max=""
	tcp_response_epoch_max=""

	# build JSON output
	json_init
	json_add_array "DownloadPerConnection"

	j=1
	while [ "$j" -le "${num_of_con}" ]; do
		worker_meta="${TMPDIR}/worker_${j}.meta"

		# if missing, report error
		if [ ! -f "${worker_meta}" ]; then
			json_cleanup
			logger -p err -t "bbf_download" "No download info file found"
			download_error "Error_Other" "${proto}"
		fi

		res="$(cat "${worker_meta}")"

		# Extract values using jsonfilter
		rom_epoch_i=$(echo "${res}" | jsonfilter -e @.rom_epoch)
		bom_epoch_i=$(echo "${res}" | jsonfilter -e @.bom_epoch)
		eom_epoch_i=$(echo "${res}" | jsonfilter -e @.eom_epoch)
		tcp_request_epoch_i=$(echo "${res}" | jsonfilter -e @.tcp_open_request_epoch)
		tcp_response_epoch_i=$(echo "${res}" | jsonfilter -e @.tcp_open_response_epoch)
		TestBytesReceived_i=$(echo "${res}" | jsonfilter -e @.TestBytesReceived)
		TotalBytesReceived_i=$(echo "${res}" | jsonfilter -e @.TotalBytesReceived)
		TotalBytesSent_i=$(echo "${res}" | jsonfilter -e @.TotalBytesSent)

		# Update minimums and maximums using numeric comparison with awk
		if [ -z "${rom_epoch_min}" ] || [ "$(echo "${rom_epoch_i} ${rom_epoch_min}" | awk '{print ($1 < $2)}')" = "1" ]; then
			rom_epoch_min="${rom_epoch_i}"
		fi

		if [ -z "${bom_epoch_min}" ] || [ "$(echo "${bom_epoch_i} ${bom_epoch_min}" | awk '{print ($1 < $2)}')" = "1" ]; then
			bom_epoch_min="${bom_epoch_i}"
		fi

		if [ -z "${eom_epoch_max}" ] || [ "$(echo "${eom_epoch_i} ${eom_epoch_max}" | awk '{print ($1 > $2)}')" = "1" ]; then
			eom_epoch_max="${eom_epoch_i}"
		fi

		if [ -z "${tcp_request_epoch_max}" ] || [ "$(echo "${tcp_request_epoch_i} ${tcp_request_epoch_max}" | awk '{print ($1 > $2)}')" = "1" ]; then
			tcp_request_epoch_max="${tcp_request_epoch_i}"
		fi

		if [ -z "${tcp_response_epoch_max}" ] || [ "$(echo "${tcp_response_epoch_i} ${tcp_response_epoch_max}" | awk '{print ($1 > $2)}')" = "1" ]; then
			tcp_response_epoch_max="${tcp_response_epoch_i}"
		fi

		# sum totals
		total_test_bytes=$((total_test_bytes + TestBytesReceived_i))
		total_bytes_received=$((total_bytes_received + TotalBytesReceived_i))
		total_bytes_sent=$((total_bytes_sent + TotalBytesSent_i))

		# Convert epoch floats to ISO for output
		ROMTime_i=$(epoch_float_to_iso "${rom_epoch_i}")
		BOMTime_i=$(epoch_float_to_iso "${bom_epoch_i}")
		EOMTime_i=$(epoch_float_to_iso "${eom_epoch_i}")
		TCPOpenRequestTime_i=$(epoch_float_to_iso "${tcp_request_epoch_i}")
		TCPOpenResponseTime_i=$(epoch_float_to_iso "${tcp_response_epoch_i}")

		# append per-connection object
		json_add_object ""
		json_add_string "ROMTime" "${ROMTime_i}"
		json_add_string "BOMTime" "${BOMTime_i}"
		json_add_string "EOMTime" "${EOMTime_i}"
		json_add_int "TestBytesReceived" "${TestBytesReceived_i}"
		json_add_int "TotalBytesReceived" "${TotalBytesReceived_i}"
		json_add_int "TotalBytesSent" "${TotalBytesSent_i}"
		json_add_string "TCPOpenRequestTime" "${TCPOpenRequestTime_i}"
		json_add_string "TCPOpenResponseTime" "${TCPOpenResponseTime_i}"
		json_close_object

		if [ "${proto}" = "both_proto" ]; then
			if [ "${enable_per_con}" = "true" ] || [ "${enable_per_con}" = "1" ]; then
				$UCI_ADD_BBF_DMMAP dmmap_diagnostics DownloadPerConnection
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].ROMTime="${ROMTime_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].BOMTime="${BOMTime_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].EOMTime="${EOMTime_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].TestBytesReceived="${TestBytesReceived_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].TotalBytesReceived="${TotalBytesReceived_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].TotalBytesSent="${TotalBytesSent_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].TCPOpenRequestTime="${TCPOpenRequestTime_i}"
				$UCI_SET_BBF_DMMAP dmmap_diagnostics.@DownloadPerConnection[$((j-1))].TCPOpenResponseTime="${TCPOpenResponseTime_i}"
			fi
		fi

		j=$((j+1))
	done

	json_close_array

	# Convert final epoch values to ISO for output
	ROMTime_min=$(epoch_float_to_iso "${rom_epoch_min}")
	BOMTime_min=$(epoch_float_to_iso "${bom_epoch_min}")
	EOMTime_max=$(epoch_float_to_iso "${eom_epoch_max}")
	latest_tcp_request=$(epoch_float_to_iso "${tcp_request_epoch_max}")
	latest_tcp_response=$(epoch_float_to_iso "${tcp_response_epoch_max}")

	# compute PeriodOfFullLoading = latest EOM - earliest BOM	, this is to make it similar with other scripts, data model says otherwis
	PeriodOfFullLoading=0

	if [ -n "${bom_epoch_min}" ] && [ -n "${eom_epoch_max}" ]; then
		# Extract seconds part for calculation
		bom_sec=${bom_epoch_min%%.*}
		eom_sec=${eom_epoch_max%%.*}

		if [ -n "${bom_sec}" ] && [ -n "${eom_sec}" ] && [ "${eom_sec}" -gt "${bom_sec}" ]; then
			PeriodOfFullLoading=$(((eom_sec - bom_sec) * 1000000))
		fi
	fi

	# top-level JSON fields
	json_add_string "Status" "Complete"
	json_add_string "IPAddressUsed" "${ip_addr_used}"
	json_add_string "ROMTime" "${ROMTime_min}"
	json_add_string "BOMTime" "${BOMTime_min}"
	json_add_string "EOMTime" "${EOMTime_max}"
	json_add_int "TestBytesReceived" "${total_test_bytes}"
	json_add_int "TotalBytesReceived" "${total_bytes_received}"
	json_add_int "TotalBytesSent" "${total_bytes_sent}"
	json_add_int "PeriodOfFullLoading" "${PeriodOfFullLoading}"
	json_add_string "TCPOpenRequestTime" "${latest_tcp_request}"
	json_add_string "TCPOpenResponseTime" "${latest_tcp_response}"

	# dump final JSON to stdout
	json_dump

	# Store data in dmmap_diagnostics for both protocols (cwmp/usp)
	[ "${proto}" == "both_proto" ] && {
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.DiagnosticState="Complete"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.IPAddressUsed="${ip_addr_used}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.ROMTime="${ROMTime_min}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.BOMTime="${BOMTime_min}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.EOMTime="${EOMTime_max}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TestBytesReceived="${total_test_bytes}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TotalBytesReceived="${total_bytes_received}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TotalBytesSent="${total_bytes_sent}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.PeriodOfFullLoading="${PeriodOfFullLoading}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TCPOpenRequestTime="${latest_tcp_request}"
		$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.TCPOpenResponseTime="${latest_tcp_response}"
	}

	$UCI_SET_BBF_DMMAP dmmap_diagnostics.download.Status="complete"
	$UCI_COMMIT_BBF_DMMAP
}

if [ -n "$1" ]; then
	download_launch "$1"
else
	download_error "Error_Internal"
fi

