#!/bin/sh -efu
#
# Copyright (C) 2023 Evgeny Sinelnikov <sin@altlinux.org>
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

. shell-args
. shell-error
. shell-getopt

# Set the textdomain for the translations using $"..."
TEXTDOMAIN="alterator-backend-packages"

declare -a temp_files=()

cleanup() {
	rm -f "${temp_files[@]}"
}

trap cleanup EXIT

verbose=

# cdrom:[LABEL] sources can make apt-get loop forever on "Insert the media" when it runs non-interactively.
cdrom_source_labels() {
	local files="/etc/apt/sources.list"
	local f

	for f in /etc/apt/sources.list.d/*.list; do
		[ -e "$f" ] && files="$files $f"
	done

	awk '
		/^[[:space:]]*#/ { next }
		{
			prefix = "cdrom:["
			if (match($0, /cdrom:\[[^]]+\]/)) {
				label = substr($0, RSTART + length(prefix), RLENGTH - length(prefix) - 1)
				if (label != "") print label
			}
		}
	' $files 2>/dev/null | sort -u
}

cdrom_normalize_label() {
	# Normalize a label for comparison (case, '_' vs ' ', extra whitespace).
	printf "%s" "$1" \
		| tr '_' ' ' \
		| tr '[:upper:]' '[:lower:]' \
		| sed -e 's/[[:space:]][[:space:]]*/ /g' -e 's/^ //;s/ $//'
}

cdrom_label_is_available() {
	# Check mounted iso9660/udf media by reading .disk/info, not by device LABEL.
	local want_norm
	want_norm="$(cdrom_normalize_label "$1")"

	local target info
	while IFS= read -r target; do
		[ -n "$target" ] || continue
		[ -r "$target/.disk/info" ] || continue
		IFS= read -r info < "$target/.disk/info" || continue
		[ -n "$info" ] || continue
		[[ "$(cdrom_normalize_label "$info")" == "$want_norm" ]] && return 0
	done < <(findmnt -rn -t iso9660,udf -o TARGET 2>/dev/null || true)

	return 1
}

cdrom_check_noninteractive() {
	[ -t 0 ] && return 0

	local labels label
	labels="$(cdrom_source_labels)"
	[ -n "${labels}" ] || return 0

	while IFS= read -r label; do
		[ -n "${label}" ] || continue
		if cdrom_label_is_available "${label}" 2>/dev/null; then
			return 0
		fi
	done <<-EOF
	${labels}
	EOF

	{
		echo $"Error: cdrom: repository is configured, but required installation media is not available."
		echo $"Configured media labels:"
		printf "%s\n" "${labels}" | sed 's/^/  /'
		echo $"Fix:"
		echo $"  - mount the installation medium/ISO and retry or"
		echo $"  - replace cdrom sources with an online repository."
	} 1>&2
	exit 1
}

show_help() {
	cat <<-EOF
		Usage: $PROG [options] <command>

		Commands:
		  check-apply             check the execution of the installation and removal transaction
		  check-install           check packages list on installation and installation status
		  check-reinstall         check packages list on reinstallation
		  check-remove            check packages list on removal
		  check-dist-upgrade      check packages list on removal and installation for upgrade
		  apply                   run an install/remove transaction using an exclude list of packages for pkgpriorities
		  dist-upgrade            upgrade for each installed packages
		  listall                 list all available packages
		  search <pattern>        search through all packages
		  lastupdate              shows time of last update in format YYYY.MM.DD HH:MM:SS UTC
		  lastdistupgrade         shows time of last dist-upgrade in format YYYY.MM.DD HH:MM:SS UTC

		Options:
		  -V, --version           print program version and exit;
		  -h, --help              show this text and exit.

		Report bugs to http://bugzilla.altlinux.org/

	EOF
	exit
}

print_version() {
	echo "@VERSION@"
	exit
}

get_last_log_entry() {
	if [ ! -f "$1" ]; then
		echo "Error: unable to get last update date, $1 not found" 1>&2
		exit 1
	fi
	tail -n 1 "$1"
}

get_last_update_date() {
	local date_result="$(TZ=UTC stat -c %y "/var/lib/apt/lists" 2>/dev/null)"
	if [[ -z "${date_result}"  ]]; then
		exit 1
	fi

	local parse_result="$(echo "${date_result}" | cut -d '.' -f1)"
	echo "${parse_result} UTC"
}

# takes one parameter:
# $1 is a string with packages to exclude from pkgpriorities in format: "pkg1\npkg2\n..."
create_pkgpriorities() {
	if [ $# -eq 1 ]; then
		local exclude_pkgs="$1"
	elif [ $# -eq 0 ]; then
		local exclude_pkgs=""
	else
		echo $"The wrong number of arguments was passed to create the pkgpriorities file" 1>&2
		exit 1
	fi

	local pkgpriorities
	pkgpriorities=$(mktemp "${TMPDIR:-/tmp}/alterator-pkgpriorities.XXXXXXXXXXXX") || {
		echo $"Error creating temporary pkgpriorities file" 1>&2
		exit 1
	}

	local -i exit_code=0
	{
		echo "Required:"
		# get manually installed packages, filter out those to be removed
		apt-mark showmanual | sed '1,2d' | grep -vFxf <(printf "%s" "${exclude_pkgs}") | sed -e 's/^/  /'
	} > "$pkgpriorities" || exit_code=${?}

	if [[ ${exit_code} -ne 0 ]]; then
		rm -f "$pkgpriorities"
		echo $"Failed to populate the pkgpriorities file with data" 1>&2
		exit ${exit_code}
	fi

	echo "$pkgpriorities"
}

transaction_start_signal() {
	echo $"Preparing the transaction..."
}

apply() {
	transaction_start_signal
	cdrom_check_noninteractive
	local to_remove=""
	local exclude_pkgs
	local pkgpriorities
	for pkg in "$@"; do
		if [[ $pkg == *- ]]; then
			to_remove+="${pkg%-}"$'\n'
		fi
	done

	exclude_pkgs=$(echo "$1" | tr ' ' '\n')
	exclude_pkgs="$to_remove$exclude_pkgs"

	# сreate pkgpriorities with manually installed packages
	local -i exit_code=0
	pkgpriorities=$(create_pkgpriorities "$exclude_pkgs") || exit_code=${?}

	if [[ ${exit_code} -ne 0 ]]; then
		echo "${pkgpriorities}" 1>&2
		exit ${exit_code}
	fi

	temp_files+=("$pkgpriorities")

	apt-get install -y -q "${@:2}" -o Dir::Etc::pkgpriorities="$pkgpriorities"
}

check_apply() {
	local reply
	local pkgpriorities
	local to_remove=""
	for pkg in "$@"; do
		if [[ $pkg == *- ]]; then
			to_remove+="${pkg%-}"$'\n'
		fi
	done
	to_remove="${to_remove%$'\n'}"

	# сreate pkgpriorities with manually installed packages
	local -i exit_code=0
	pkgpriorities=$(create_pkgpriorities "$to_remove") || exit_code=${?}

	if [[ ${exit_code} -ne 0 ]]; then
		echo "${pkgpriorities}" 1>&2
		exit ${exit_code}
	fi

	temp_files+=("$pkgpriorities")

	reply="$(LANG=en apt-get install --just-print -q "${@}" -o Dir::Etc::pkgpriorities="$pkgpriorities")" || exit_code=${?}

	if [[ ${exit_code} -ne 0 ]]; then
		echo "${reply}" 1>&2
		exit ${exit_code}
	fi

	local install_pkgs
	local remove_pkgs
	local extra_remove_pkgs

	install_pkgs="$(echo "${reply}" | awk '/Inst/ { print $2 }' | sed "s/^/\"/;s/$/\"/" | tr '\n' ',' | sed 's/,$//')"
	remove_pkgs="$(echo "${reply}" | awk '/Remv/ { print $2 }' | sed "s/^/\"/;s/$/\"/" | tr '\n' ',' | sed 's/,$//')"
	extra_remove_pkgs="$(echo "${reply}" \
		| awk '/This should NOT be done unless you know exactly what you are doing!/ {flag=1; next} /^[0-9]+ upgraded/ {flag=0} flag' \
		| tr -s '\n' ' ' \
		| sed 's/^[ \t]*//' \
		| sed 's/([^)]*)//g' \
		| sed -E 's/[[:space:]]+/\n/g' \
		| grep -v '^$' \
		| sed "s/^/\"/;s/$/\"/" \
		| tr '\n' ',' \
		| sed 's/,$//')"

	echo "{\"install_packages\": [${install_pkgs}], \"remove_packages\": [${remove_pkgs}], \"extra_remove_packages\": [${extra_remove_pkgs}]}"
}

check_reinstall() {
	local reply
	local -i exit_code=0
	reply="$(apt-get reinstall -s -q ${@})" || exit_code=${?}
	if [[ ${exit_code} -ne 0 ]]; then
		echo "${reply}" 1>&2
		exit ${exit_code}
	fi

	echo "${reply}" | awk '/Inst/ { print $2 }'
	echo "${reply}" | awk '/Remv/ { print $2 }' 1>&2
}

dist_upgrade() {
	transaction_start_signal
	cdrom_check_noninteractive
	local reply
	local -i exit_code=0
	reply="$(apt-get dist-upgrade --assume-yes -q)" || exit_code=${?}
	if [[ ${exit_code} -ne 0 ]]; then
		echo "${reply}" 1>&2
		exit ${exit_code}
	fi
}

TEMP=$(getopt -n $PROG -o $getopt_common_opts -l $getopt_common_longopts -- "$@") ||
	show_usage
eval set -- "$TEMP"

while :; do
	case "$1" in
	--)
		shift
		break
		;;
	*)
		parse_common_option "$1"
		;;
	esac
	shift
done

[ "$#" -gt 0 ] ||
	show_usage

case "$1" in
listall)
	apt-cache search . --names-only | awk '{ print $1 }'
	;;
search)
	[ "$#" -gt 1 ] ||
		show_usage
	apt-cache search "$2" | awk '{ print $1 }'
	;;
lastupdate)
	get_last_update_date
	;;
lastdistupgrade)
	get_last_log_entry "/var/log/alterator/apt/dist-upgrades.log"
	;;
apply)
	apply "${@:2}"
	;;
check-apply)
	check_apply "${@:2}"
	;;
check-reinstall)
	check_reinstall "${@:2}"
	;;
dist-upgrade)
	dist_upgrade
	;;
esac
