#!/bin/bash

showHelp() {
	echo "netupgrade [--help] [-f] [-y] [configfilename]"
	echo ""
	echo "  --help         Show this help message"
	echo "  -f             Preselect all nodes in the checklist"
	echo "  -y             No confirmation for supported package managers"
	echo "  configfilename Path to a cfg file"
}

checkDependencies() {
	local -a REQUIRED_CMDS=(ssh whiptail cat tee rm touch mv)
	local -a MISSING_CMDS=()
	local CMD

	for CMD in "${REQUIRED_CMDS[@]}"; do
		if ! command -v "${CMD}" >/dev/null 2>&1; then
			MISSING_CMDS+=("${CMD}")
		fi
	done

	if [ ${#MISSING_CMDS[@]} -gt 0 ]; then
		echo "Error: missing required dependencies: ${MISSING_CMDS[*]}"
		exit 1
	fi
}

resolveLogViewer() {
	if [ -n "${EDITOR}" ] && command -v "${EDITOR}" >/dev/null 2>&1; then
		LOGVIEWER="${EDITOR}"
		return
	fi

	local -a CANDIDATES=(nano vi less)
	local CANDIDATE
	for CANDIDATE in "${CANDIDATES[@]}"; do
		if command -v "${CANDIDATE}" >/dev/null 2>&1; then
			LOGVIEWER="${CANDIDATE}"
			return
		fi
	done

	LOGVIEWER=""
}

pressAnyKey(){
	read -n1 -r -p "Press any key to continue."
}

loadConfig(){
	if [ -e "${CONFIGFILENAME}" ]; then
		. "${CONFIGFILENAME}"
	else
		echo "Error : Config file not found :"
		echo "${CONFIGFILENAME}"
		exit 1
	fi
}

parseNode() {
	local NODE_VALUE="${1}"
	local -n NODE_FIELDS_REF="${2}"
	IFS=';' read -r -a NODE_FIELDS_REF <<< "${NODE_VALUE}"
}

parseSelection() {
	local SELECTION_RAW="${1}"
	local -n SELECTION_REF="${2}"
	local ITEM_INDEX=0

	SELECTION_REF=()
	read -r -a SELECTION_REF <<< "${SELECTION_RAW}"

	for ITEM_INDEX in "${!SELECTION_REF[@]}"; do
		SELECTION_REF[ITEM_INDEX]="${SELECTION_REF[ITEM_INDEX]%\"}"
		SELECTION_REF[ITEM_INDEX]="${SELECTION_REF[ITEM_INDEX]#\"}"
	done
}

runSSH() {
	local HOST="${1}"
	shift
	ssh "${SSH_USER}@${HOST}" "$@"
}

prependLogSummary() {
	local SUMMARY_CONTENT="${1}"
	local TEMP_LOGFILENAME="${LOGFILENAME}.tmp"

	{
		echo "Results :"
		echo "---------"
		echo ""
		echo -e "${SUMMARY_CONTENT}"
		echo ""
		echo ""
		cat "${LOGFILENAME}"
	} > "${TEMP_LOGFILENAME}"
	mv "${TEMP_LOGFILENAME}" "${LOGFILENAME}"
}

selectNodes(){
	local -a OPTIONS=()
	local -a SELECTED_ITEMS=()
	local -i INDEX=1
	local -i I=0
	local -a FIELDS=()
	local DESC=""
	local FIELD=""
	local SEL=""
	local DEFAULT_STATE="OFF"

	if [ "${FULL}" -eq 1 ]; then
		DEFAULT_STATE="ON"
	fi

	for NODE in "${NODES[@]}"; do
		FIELDS=()
		DESC=""
		parseNode "${NODE}" FIELDS
		for ((I = 2; I < ${#FIELDS[@]}; ++I)); do
			FIELD="${FIELDS[I]}"
			if [ -z "${DESC}" ]; then
				DESC="${FIELD%%:*}"
			else
				DESC="${DESC}|${FIELD%%:*}"
			fi
		done
		OPTIONS+=("${INDEX}:${FIELDS[0]}" "${FIELDS[1]} [${DESC}]" "${DEFAULT_STATE}")
		INDEX+=1
	done
	if ! SEL=$(whiptail --title "NetUpgrade" --checklist "" 0 0 0 \
		"${OPTIONS[@]}" \
		3>&1 1>&2 2>&3); then
		exit 0
	fi
	if [ -z "${SEL}" ]; then
		exit 0
	fi

	parseSelection "${SEL}" SELECTED_ITEMS
	if [ ${#SELECTED_ITEMS[@]} -eq 0 ]; then
		exit 0
	fi

	if [ -f "${LOGFILENAME}" ]; then
		rm "${LOGFILENAME}"
	fi
	touch "${LOGFILENAME}"
	local RESULT="\n"

	for ITM in "${SELECTED_ITEMS[@]}"; do
		INDEX=1
		for NODE in "${NODES[@]}"; do
			FIELDS=()
			parseNode "${NODE}" FIELDS
			if [ "${ITM}" = "${INDEX}:${FIELDS[0]}" ]; then
				for ((I = 2; I < ${#FIELDS[@]}; ++I)); do
					if runCmd "${FIELDS[0]}" "${FIELDS[1]}" "${FIELDS[I]}"; then
						RESULT+="Ok: ${FIELDS[1]} @ ${FIELDS[0]} : ${FIELDS[I]}\n"
					else
						RESULT+="Error: ${FIELDS[1]} @ ${FIELDS[0]} : ${FIELDS[I]}\n"
					fi
				done
			fi
			INDEX+=1
		done
	done

	prependLogSummary "${RESULT}"
	if [ -n "${LOGVIEWER}" ]; then
		"${LOGVIEWER}" "${LOGFILENAME}"
	else
		echo "Warning: no log viewer found, showing log path instead: ${LOGFILENAME}"
	fi
	rm -i "${LOGFILENAME}"
	echo ""
	echo "Results :"
	echo "---------"
	echo -e "${RESULT}"
}

runCmd() { # $1=host $2=name $3=cmd
	local -r HOST="${1}"
	local -r NAME="${2}"
	local -r ACTION="${3}"
	local -r CMD="${ACTION%%:*}"
	local -r CMDVAL="${ACTION#*:}"
	local -i ERROR=0
	local TITLELENGTH=0
	local SUBTITLE="-----------------------------------------------------------------------------"
	local YESARG=""

	echo "${NAME} @ ${HOST} : ${CMD}" | tee -a "${LOGFILENAME}"
	TITLELENGTH=$((${#NAME} + ${#HOST} + ${#CMD} + 6))
	echo "${SUBTITLE:0:${TITLELENGTH}}" | tee -a "${LOGFILENAME}"
	date +'%Y-%m-%d %H:%M:%S %A' | tee -a "${LOGFILENAME}"
	echo "" | tee -a "${LOGFILENAME}"
	set -o pipefail

	case ${CMD} in
		reboot)
			echo "reboot" | tee -a "${LOGFILENAME}"
			runSSH "${HOST}" reboot | tee -a "${LOGFILENAME}"
		;;
		apt)
			if [ "${YES}" -eq 1 ]; then
				YESARG="-y"
			fi
			echo "apt-get ${YESARG} update" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" apt-get ${YESARG} update | tee -a "${LOGFILENAME}"; then
				ERROR=1
			else
				echo "" | tee -a "${LOGFILENAME}"
				echo "apt-get ${YESARG} dist-upgrade" | tee -a "${LOGFILENAME}"
				if ! runSSH "${HOST}" apt-get ${YESARG} dist-upgrade | tee -a "${LOGFILENAME}"; then
					ERROR=1
				fi
				echo "" | tee -a "${LOGFILENAME}"
				echo "apt-get ${YESARG} autoremove --purge" | tee -a "${LOGFILENAME}"
				runSSH "${HOST}" apt-get ${YESARG} autoremove --purge | tee -a "${LOGFILENAME}"
				echo "" | tee -a "${LOGFILENAME}"
				echo "apt-get ${YESARG} autoclean" | tee -a "${LOGFILENAME}"
				runSSH "${HOST}" apt-get ${YESARG} autoclean | tee -a "${LOGFILENAME}"
				echo "" | tee -a "${LOGFILENAME}"
				echo "apt-get ${YESARG} clean" | tee -a "${LOGFILENAME}"
				runSSH "${HOST}" apt-get ${YESARG} clean | tee -a "${LOGFILENAME}"
				echo "" | tee -a "${LOGFILENAME}"
			fi
		;;
		yum)
			if [ "${YES}" -eq 1 ]; then
				YESARG="-y"
			fi
			echo "yum ${YESARG} update" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" yum ${YESARG} update | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
		;;
		dnf)
			if [ "${YES}" -eq 1 ]; then
				YESARG="-y"
			fi
			echo "dnf ${YESARG} upgrade --refresh" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" dnf ${YESARG} upgrade --refresh | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
		;;
		pkg)
			if [ "${YES}" -eq 1 ]; then
				YESARG="-y"
			fi
			echo "pkg ${YESARG} update" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" pkg ${YESARG} update | tee -a "${LOGFILENAME}"; then
				ERROR=1
			else
				echo "" | tee -a "${LOGFILENAME}"
				echo "pkg upgrade ${YESARG}" | tee -a "${LOGFILENAME}"
				if ! runSSH "${HOST}" pkg upgrade ${YESARG} | tee -a "${LOGFILENAME}"; then
					ERROR=1
				fi
				echo "" | tee -a "${LOGFILENAME}"
				echo "pkg autoremove ${YESARG}" | tee -a "${LOGFILENAME}"
				runSSH "${HOST}" pkg autoremove ${YESARG} | tee -a "${LOGFILENAME}"
				echo "" | tee -a "${LOGFILENAME}"
				echo "pkg clean ${YESARG}" | tee -a "${LOGFILENAME}"
				runSSH "${HOST}" pkg clean ${YESARG} | tee -a "${LOGFILENAME}"
				echo "" | tee -a "${LOGFILENAME}"
			fi
		;;
		pacman)
			if [ "${YES}" -eq 1 ]; then
				YESARG="--noconfirm"
			fi
			echo "pacman -Sy ${YESARG} archlinux-keyring" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" pacman -Sy ${YESARG} archlinux-keyring | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
			echo "pacman -Syu ${YESARG}" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" pacman -Syu ${YESARG} | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
			echo "pacman orphan cleanup" | tee -a "${LOGFILENAME}"
			if [ -n "${YESARG}" ]; then
				if ! runSSH "${HOST}" sh -c 'orphans=$(pacman -Qqtd 2>/dev/null || true); if [ -n "$orphans" ]; then pacman -Rns --noconfirm $orphans; fi' | tee -a "${LOGFILENAME}"; then
					ERROR=1
				fi
			else
				if ! runSSH "${HOST}" sh -c 'orphans=$(pacman -Qqtd 2>/dev/null || true); if [ -n "$orphans" ]; then pacman -Rns $orphans; fi' | tee -a "${LOGFILENAME}"; then
					ERROR=1
				fi
			fi
			echo "pacman -Sc ${YESARG}" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" pacman -Sc ${YESARG} | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
		;;
		apk)
			echo "apk update" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" apk update | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
			echo "apk upgrade" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" apk upgrade | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
		;;
		cmd)
			echo "cmd: ${CMDVAL}" | tee -a "${LOGFILENAME}"
			if ! runSSH "${HOST}" sh -c "${CMDVAL}" | tee -a "${LOGFILENAME}"; then
				ERROR=1
			fi
		;;
		docker-stacks)
			echo "docker stacks update in ${CMDVAL}" | tee -a "${LOGFILENAME}"
			if ! ssh "${SSH_USER}@${HOST}" "STACK_ROOT=$(printf '%q' "${CMDVAL}") bash -s" <<'EOF' | tee -a "${LOGFILENAME}"
stack_root="${STACK_ROOT}"

for dir in "$stack_root"/*; do
	[ -d "$dir" ] || continue
	(
		cd "$dir" || exit 1
		docker compose pull
		docker compose up -d
	) || exit 1
done

docker image prune -f
EOF
			then
				ERROR=1
			fi
		;;
		*)
			echo "Error: Command ${CMD} unknown" | tee -a "${LOGFILENAME}"
			ERROR=1
		;;
	esac
	echo "" | tee -a "${LOGFILENAME}"
	echo "" | tee -a "${LOGFILENAME}"
	if [ "${ERROR}" -eq 1 ]; then
		return 1
	fi
}


declare -i YES=0
declare -i FULL=0
declare CONFIGFILENAME="${HOME}/.config/netupgrade/index.cfg"
declare LOGFILENAME="${HOME}/netupgrade.log"
declare LOGVIEWER=""
declare SSH_USER="root"
declare -a NODES=()

while [[ ${#} -gt 0 ]]; do
	case ${1} in
		--help) showHelp; exit 0;;
		-y) YES=1; shift;;
		-f) FULL=1; shift;;
		*) CONFIGFILENAME=${1}; shift;;
	esac
done

checkDependencies
resolveLogViewer
loadConfig
selectNodes

