#!/usr/bin/bash

# Main script for twups (0.2.2)
#
# Copyright (c) 2020 (GPL-3.0 or later)
#     ProgrammerPolymathic (https://codeberg.org/ProgrammerPolymathic)
# A copy of the licence should have been distributed with the program; it may also be found in the source repository at
#     https://codeberg.org/ProgrammerPolymathic/twups

TWUPS_VERSION=0.2.2


setDefaults() {
	## Sets initial variables

	unstableup=0
	oldup=0
	noup=0
	tomatch=5

	# Check if output is to terminal; set default colours variable accordingly
	if [[ -t 1 ]] ; then
		colours=1
	else
		colours=0
	fi

	# Get current version from local files
	installedver=$(grep -oP "VERSION_ID=\"\K(\d+)" /etc/os-release)
}


processArgs() {
	## Handle all initial input
	## Run getopt to set variables and check for leftover output (exit if there is any)

	args=$(getopt \
		--options urns:c:hv \
		--longoptions unstable-upgrades,rollback,no-upgrade,search:,count:,color:,help,version \
		-n 'twups' -- "$@") \
		|| exit 64 # Don't continue if input is not recognized

	eval set -- "${args}"

	while true ; do
		case "$1" in
			-u|--unstable-upgrades) # Enable upgrade to snapshots not rated as stable
				unstableup=1 ; shift ;;

			-r|--rollback) # Enable selection of snapshots older than current version
				oldup=1 ; shift ;;

			-n|--no-upgrade) # View snapshots only, don't upgrade
				noup=1 ; shift ;;

			-s|--search) # Search for specific snapshots, supports regex
				srch="$2" ; shift 2 ;;

			-c|--count) # Number of snapshots to display
				setCount "$2" ; shift 2 ;;

			--color) # Manually enable or disable colour escapes
				setColour "$2" ; shift 2 ;;

			-h|--help) # Show help
				printHelp ;;

			-v|--version) # Show version
				echo "${TWUPS_VERSION}"
				exit ;;

			--) # Exit loop when done
				shift ; break ;;
		esac
	done

	# Remove result limit if searching without defined result count
	if [[ "${srch}" != "" ]] && [[ "${matchset}" == "" ]] ; then
		tomatch=""
	fi

	# Complain about left over input
	if [[ $# != 0 ]] ; then
		echo "Unrecognized input; any arguments must follow a flag (use 'twups --help' to see accepted flags)" >&2
		exit 64
	fi
}

setCount() {
	## Check that the input (parameter given for --count) is valid and set its variable if so
	## Sets matchset variable to prevent count from being set to unbound later

	if ! [ "$1" -eq "$1" ] 2> /dev/null ; then
		echo "-c requires integer input" >&2
		exit 65
	elif [[ "$1" -eq 0 ]] ; then
		echo "Count cannot be zero" >&2
		exit 65
	else
		tomatch="$1"
		matchset="true"
	fi
}

setColour() {
	## Enable or disable coloured output according to given parameter (or exit if not recognized)

	case "$1" in
		always)
			colours=1 ;;
		never)
			colours=0 ;;
		auto)
			;; # Accept as input, but do nothing
		*)
			echo "Unrecognized colour setting '$1'" >&2
			exit 65 ;;
	esac
}

printHelp() {
	## Output help text

	echo "twups - Tumbleweed Update Scrutinizer"
	echo "A tool to check openSUSE Tumbleweed snapshot status and then optionally update based on rating"
	echo ""
	echo "Options are listed below; arguments are only taken for the --count, --color, and --search options"
	echo "-c, --count             Print the specified number of snapshots (default is 5 unless searching)"
	echo "--color                 Specify whether to use colour escapes in output"
	echo "-h, --help              Print this information and exit"
	echo "-n, --no-upgrade        Do not prompt to upgrade to any displayed snapshots"
	echo "-r, --rollback          Allow installing snapshots older than the current version"
	echo "-s, --search            Search for snapshots by date, supports regex"
	echo "-u, --unstable-upgrades Allow installing snapshots not rated \"stable\""
	echo "-v, --version           Print version and exit"
	echo ""
	echo "See the manpage for more details."
	exit
}


echoAction() {
	## Echo version of currently installed snapshot and then announce action being taken

	echo "Installed snapshot version: ${installedver:0:4}-${installedver:4:2}-${installedver:6:2}"
	if [[ "${srch}" == "" ]] ; then
		echo "Checking recent snapshots..."
	else
		echo "Searching snapshots..."
	fi
}


getSnapshots() {
	## Pull info from Boombatower into the array ssinfo
	## Array is populated with "dirty" strings (whole lines) to be formatted later

	readarray -t ssinfo <<< $(curl -Ss https://review.tumbleweed.boombatower.com/data/score.yaml | tac | grep \
		-e "\'" \
		-e 'stability_level' \
		-e 'score')
	snapnum="$((${#ssinfo[@]} / 3))" # Total number of snapshots

	if [[ "${ssinfo[0]}" == "" ]] ; then # Exit if fetch fails (i.e. array is empty)
		echo "Failed to fetch information from Boombatower (see above error from curl, if there is one)" >&2
		exit 75
	fi
}


evaluateSnapshots() {
	## Loop over snapshots, pulling out and displaying information for each

	looped=0 # Number of times looped (can't use for loop because three lines are handled at once, not one)
	matched=0 # Number of snapshots matching search term

	until [[ "${looped}" -eq "${snapnum}" ]] ; do
		looped=$((looped + 1))

		formatSnapshotInfo

		# Pass if search term isn't matched (blank search matches everything)
		if [[ "$(echo "${ver}" | grep "${srch}")" == "" ]] && [[ "$(echo "${verpretty}" | grep "${srch}")" == "" ]] ; then
			continue
		fi

		targetIfNewest

		checkSnapshotAge
		colourRating

		echoSnapshot

		matched=$((matched + 1))

		if [[ "${matched}" == "${tomatch}" ]] ; then # Cut off if result limit is hit
			break
		fi
	done
}

formatSnapshotInfo() {
	## Strip out unwanted parts of strings fetched for each snapshot

	ver=${ssinfo[$((2 + looped * 3))]} # Date in yyyymmdd format, used internally by Boombatower
	ver=${ver:1:8}

	verpretty="${ver:0:4}-${ver:4:2}-${ver:6:2}" # Date in yyyy-mm-dd format

	rating=${ssinfo[$((0 + looped * 3))]//"  stability_level: "/}
	score=${ssinfo[$((1 + looped * 3))]//"  score: "/}
}


targetIfNewest() {
	## Save newest viable version as target
	## Catches and preserves first viable version found, which is newest due to reverse chronological ordering

	if [[ "${target}" == "" ]] && \
		{ { [[ "${unstableup}" == 1 ]] || [[ "${rating}" == 'stable' ]] ; } && \
		{ [[ "${oldup}" == 1 ]] || [[ "${ver}" -gt "${installedver}" ]] ; } } && \
		[[ "${ver}" -ne "${installedver}" ]] ; then
		target="${ver}"
		targetpretty="${verpretty}"
	fi
}

checkSnapshotAge() {
	## Compare date of a snapshot to that of installed snapshot and set a corresponding string

	if [[ "${ver}" -gt "${installedver}" ]] ; then
		if [[ "${colours}" == 1 ]] ; then
			current="\e[1;36mnew\e[0m"
		else
			current="new"
		fi
	elif [[ "${ver}" -eq "${installedver}" ]] ; then
		if [[ "${colours}" == 1 ]] ; then
			current="\e[1mcurrent\e[0m"
		else
			current="current"
		fi
	else
		current="old"
	fi
}

colourRating() {
	## Colour code stability ratings
	## (even if colours are disabled, this capitalizes the first letter of the rating)

	case "${rating}" in
		stable)
			if [[ "${colours}" == 1 ]] ; then
				rating="\e[1;32mStable\e[0m"
			else
				rating="Stable"
			fi ;;
		pending)
			if [[ "${colours}" == 1 ]] ; then
				rating="\e[1;33mPending\e[0m"
			else
				rating="Pending"
			fi ;;
		moderate)
			if [[ "${colours}" == 1 ]] ; then
				rating="\e[1;33mModerate\e[0m"
			else
				rating="Moderate"
			fi ;;
		unstable)
			if [[ "${colours}" == 1 ]] ; then
				rating="\e[1;31mUnstable\e[0m"
			else
				rating="Unstable"
			fi ;;
		*) # If not one of the above, it's probably something bad...
			if [[ "${colours}" == 1 ]] ; then
				rating="\e[1;31m${rating}\e[0m"
			fi ;;
	esac
}

echoSnapshot() {
	## Echo formatted information about a snapshot

	echo ""
	echo -e "Version:    ${verpretty} (${current})"
	echo -e "Rating:     ${rating}"
	echo    "Score:      ${score}"
}


updateIfAble() {
	## If able to update, prompt the user to do so and call tumbleweed-cli, then zypper

	echo ""

	# Check if tumbleweed-cli is initialized; can't progress if not
	if ! [[ -f "/etc/zypp/vars.d/snapshotVersion" ]] ; then
		echo "tumbleweed-cli is not initialized; run 'tumbleweed init', or see documentation for more information"
		echo "Exiting"
		exit 69
	fi

	echoOptions

	if [[ "${target}" != "" ]] ; then
		promptUpdate

		if [[ "${upconfirm}" == "y" || "${upconfirm}" == "" ]] ; then
			callExternals
		else
			echo "Update refused, exiting"
		fi
	else
		echoNoUpdates
	fi
}

echoOptions() {
	## Announce any options currently affecting selection of snapshots

	if [[ "${unstableup}" == 1 ]] ; then
		echo "Unstable upgrades enabled"
	fi
	if [[ "${oldup}" == 1 ]] ; then
		echo "Rollback enabled"
	fi
}

promptUpdate() {
	## Ask user to update, warning if about to downgrade
	
	echo "Found update candidate ${target}"
	echo ""

	if [[ "${target}" -gt "${installedver}" ]] ; then
		printf "Update to snapshot version %s? [Y/n]: " "${targetpretty}" && read -r 'upconfirm'
	else
		printf "DOWNGRADE to snapshot %s? [Y/n]: " "${targetpretty}" && read -r 'upconfirm'
	fi
}

callExternals() {
	## Call tumbleweed-cli and zypper to carry out snapshot installation

	echo "Selecting snapshot..."
	switched=$(tumbleweed switch "${target//-/}" <<< "y") \
		|| (echo "tumbleweed-cli could not change the selected snapshot, exiting" ; exit 70)
	if [[ "${switched}" == *"invalid"* ]] ; then # returns 0 if snapshot not found, but echos error
		echo "tumbleweed-cli could not find snapshot ${target//-/}, exiting"
		exit 70
	fi

	echo "Running zypper..."
	echo ""
	sudo zypper ref
	sudo zypper dup
}

echoNoUpdates() {
	## Announce that no snapshots listed are suitable for installation
	## Changes depending on options used

	if [[ "${srch}" == "" ]] ; then
		echo "No update candidates in results"
		if [[ "${unstableup}" == 0 ]] && [[ "${oldup}" = 1 ]] ; then
			echo "(use --unstable-upgrades to update to snapshots not marked stable)"
		elif [[ "${unstableup}" == 1 ]] && [[ "${oldup}" = 0 ]] ; then
			echo "(use --rollback to update to snapshots older than the currently installed version)"
		elif [[ "${unstableup}" == 0 ]] && [[ "${oldup}" = 0 ]] ; then
			echo "(use --unstable-upgrades to upgrade to snapshots not marked stable or --rollback for older versions)"
		fi
	else
		echo "No update candidates found for '${srch}'"
	fi
}


setDefaults
processArgs "$@"

echoAction

getSnapshots
evaluateSnapshots

if [[ "${noup}" != 1 ]] ; then
	updateIfAble
fi
