#!/bin/bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2023 SUSE LLC
set -e
shopt -s nullglob

# set to --keep-title to use altenate screen. Better for debugging but causes flicker
dialog_altenate_screen=
dialog_backtitle="Systemd-boot"
interactive=
verbose=
nl=$'\n'
shimdir="/usr/share/efi/$(uname -m)"
sdboot_vendor="systemd"
sdboot_dst="/EFI/$sdboot_vendor"
arg_esp_path="$SYSTEMD_ESP_PATH"
arg_entry_token=
arg_arch=
arg_no_variables=
arg_no_reuse_initrd=
# for x in vmlinuz image vmlinux linux bzImage uImage Image zImage; do
image=vmlinuz

rollback=()

tmpdir=$(mktemp -d -t sdboot.XXXXXX)
cleanup()
{
	local i
	for i in "${rollback[@]}"; do
		if [ -e "$i.bak" ]; then
			log_info "restoring $i"
			mv "$i.bak" "$i"
		else
			log_info "removing $i"
			rm -f "$i"
		fi
	done
	rm -rf "$tmpdir"
}
trap cleanup EXIT

entryfile="$tmpdir/entries.json"
snapperfile="$tmpdir/snapper.json"
title_sort_file="$tmpdir/title_sort.txt"
tmpfile="$tmpdir/tmp"

helpandquit()
{
	cat <<-EOF
		Usage: $0 [OPTIONS] [COMMAND]
		OPTIONS:
		  --verbose  verbose
		  -h         help screen

		COMMAND:
		add-kernel VERSION [SUBVOL]
			   Create boot entry for specified kernel

		add-all-kernels [SNAPSHOT]
			   Create boot entries for all kernels in SNAPSHOT

		remove-kernel VERSION [SUBVOL]
			   Remove boot entry for specified kernel

		remove-all-kernels [SNAPSHOT]
			   Remove boot entries for all kernels in SNAPSHOT

		list-kernels [SNAPSHOT]
			   List all kernels related to SNAPSHOT

		set-default-snapshot [SNAPSHOT]
			   Make SNAPSHOT the default for next boot.
			   Also install all kernels if needed

		is-bootable [SNAPSHOT]
			   Check whether SNAPSHOT has any kernels registered, ie
			   is potentially bootable

		install    Install systemd-boot and shim into ESP

		UI commands:
		kernels    Open kernel menu
		snapshots  Open snapshots menu
		entries    Open entry menu

	EOF
	exit 0
}

log_info()
{
	[ "${verbose:-0}" -gt 0 ] || return 0
	echo "$@"
}

d(){
	local retval=0
	# Bash makes it a bit annoying to read the output of a different FD into a variable, it
	# only supports reading stdout by itself. So redirect 3 to stdout and 1 to the real stdout.
	exec {stdoutfd}>&1
	result="$(dialog $dialog_altenate_screen --backtitle "$dialog_backtitle" --output-fd 3 "$@" 3>&1 1>&${stdoutfd})" || retval=$?
	# Word splitting makes it necessary to use eval here.
	eval "exec ${stdoutfd}>&-"
	return "$retval"
}

err()
{
	if [ "$interactive" = 1 ]; then
		d --title 'Error' --ok-label "Quit" --colors --aspect 60 --msgbox "\Z1Error:\Zn $*" 0 0
	else
		echo "Error: $*" >&2
	fi
	exit 1
}

warn()
{
	if [ "$interactive" = 1 ]; then
		d --title 'Warning' --ok-label "Continue" --colors --aspect 60 --msgbox "\Z1Warning:\Zn $*" 0 0
	else
		echo "Warning: $*" >&2
	fi
}

reset_rollback()
{
	for i in "${rollback[@]}"; do
		[ -e "$i.bak" ] || continue
		log_info "removing $i.bak"
		rm -f "$i.bak"
	done
	rollback=()
}

run_command_live_output()
{
	if [ "$interactive" = 1 ]; then
		"$@" 2>&1 | dialog $dialog_altenate_screen --backtitle "$dialog_backtitle" --title "$1" --aspect 60 --progressbox 0 0
	else
		"$@"
	fi
}

run_command_output()
{
	if [ "$interactive" = 1 ]; then
		"$@" > "$tmpfile" 2>&1
		[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
	else
		"$@"
	fi
}

# Given the number of total item pairs, outputs the number of items to display at once
menuheight() {
	local height=$(($1 / 2))
	[ "$height" -le "$dh_menu" ] || height="$dh_menu"
	echo "$height"
}

stty_size() {
	set -- $(stty size 2>/dev/null)
	LINES="$1"
	COLUMNS="$2"
	# stty size can return zero when not ready or
	# its a serial console
	if [ "$COLUMNS" = "0" ] || [ "$LINES" = "0" ]; then
		LINES=24
		COLUMNS=80
	fi

	dh_menu=$((LINES-15))
	dh_text=$((LINES-5))
}

# check whether it's a transactional system
is_transactional()
{
	[ "$(stat -f -c %T /etc)" = "overlayfs" ]
}

subvol_is_ro()
{
	local subvol="${1:?}"
	while read -r line; do
		[ "$line" = "ro=true" ] && return 0
	done < <(btrfs prop get -t s "${subvol#@}" ro)
	return 1
}

detect_parent() {
	local subvol="$1"
	parent_uuid="$(btrfs subvol show "${subvol#@}" | sed -ne 's/\s*Parent UUID:\s*//p')"
	[ "$parent_uuid" != '-' ] || parent_uuid=
	[ -n "$parent_uuid" ] || return 0
	parent_subvol="$(/sbin/btrfs subvol show -u "$parent_uuid" "${subvol#@}" | head -1)"
	parent_snapshot="${parent_subvol#@/.snapshots/}"
	if [ "$parent_subvol" = "$parent_snapshot" ]; then
		unset parent_subvol parent_snapshot
	else
		parent_snapshot="${parent_snapshot%/snapshot}"
	fi
}

sedrootflags()
{
	local subvol="${1:?}"
	# - delete BOOT_IMAGE= and initrd=
	# - make sure root= refers to uuid
	# - replace or add rootflags to point at correct subvolume
	sed -e "s/[ \t]\+/ /g;s/\<\(BOOT_IMAGE\|initrd\)=[^ ]* \?//;s/\<root=[^ ]*/root=UUID=$root_uuid/;tr;:r;s,\<rootflags=subvol=[^ ]*,rootflags=subvol=$subvol,;tx; s,\$, rootflags=subvol=$subvol,;:x"
}

remove_kernel()
{
	local subvol="$1"
	local kernel_version="$2"
	local snapshot="${subvol#@/.snapshots/}"
	snapshot="${snapshot%/*}"
	local id="$entry_token-$kernel_version-$snapshot.conf"
	run_command_output bootctl unlink "$id"
}

install_with_rollback()
{
	local src="${1:?}"
	local dst="${2:?}"

	if [ -e "$dst" ]; then
		if cmp -s "$src" "$dst"; then
			log_info "$dst unchanged"
			return 0
		fi
		mv "$dst" "$dst.bak" || return "$?"
	fi
	rollback+=("$dst")
	install -m 0644 "$src" "$dst" || return "$?"
	chown root:root "$dst" 2>/dev/null || :
	log_info "installed $dst"
}

update_snapper()
{
    snapper --jsonout --no-dbus list --disable-used-space > "$snapperfile"
}

set_snapper_title_and_sortkey()
{
	snapshot="${1:?}"
	local type date desc important pre_num
	local snapshot_info

	update_snapper

	IFS="|" read -r type date desc important pre_num <<< \
		$(jq -r --arg snapshot "$snapshot" \
		'.["root"][]|select(.number==( $snapshot|tonumber))|[.type,.date,(.description|gsub("\\|";"_")),.userdata.important,."pre-number"//""]|join("|")'\
		< "$snapperfile")

	if [ -z "$desc" ] && [ "$type" = "post" ] && [ -n "$pre_num" ]; then
		read -r desc <<< $(jq -r --arg snapshot "$pre_num" '.["root"][]|select(.number==($snapshot|tonumber))|.description' < "$snapperfile")
	fi

	if [ "$important" = "yes" ]; then important="*"; else important=""; fi
	[ "$type" = "single" ] && type=""
	snapshot_info="$snapshot,$kernel_version,$date${type:+, $type}${desc:+, $desc}"

	# shellcheck disable=SC2154
	title="Snapper: ${important}$title ($snapshot_info)"
	sort_key="snapper-$sort_key"
}

reuse_initrd() {
	local snapshot="${1:?}"
	local subvol="${2:?}"

	[ -z "$arg_no_reuse_initrd" ] || return 1

	local conf="$boot_root/loader/entries/$entry_token-$kernel_version-$snapshot.conf"
	if [ -e "$conf" ]; then
		local k
		local v
		while read -r k v; do
			[ "$k" = 'initrd' ] || continue
			log_info "found existing initrd $v"
			dstinitrd="$v"
			break
		done < "$conf"
		[ -z "$dstinitrd" ] || return 0
	fi

	# check if we can reuse the initrd from the parent
	# to avoid expensive regeneration
	detect_parent "$subvol"
	local parent_conf="$boot_root/loader/entries/$entry_token-$kernel_version-$parent_snapshot.conf"
	if [ -n "$parent_subvol" ] && [ -e "$parent_conf" ]; then
		#subvol_is_ro "$parent_subvol" || err "Parent snapshot $parent_snapshot is not read-only, can't reuse initrd"
		local k
		local v
		while read -r k v; do
			[ "$k" = 'initrd' ] || continue
			log_info "found parent initrd $v"
			dstinitrd="$v"
			break
		done < "$parent_conf"
		[ -z "$dstinitrd" ] || return 0
	fi

	return 1
}

install_kernel()
{
	local subvol="$1"
	local kernel_version="$2"
	local dstinitrd=""
	local src="${subvol#@}/lib/modules/$kernel_version/$image"
	test -e "$src" || err "Can't find $src"

	calc_chksum "$src"
	local dst="/$entry_token/$kernel_version/linux-$chksum"

	# XXX: fix calling with snapshot instead of subvol
	local snapshot="${subvol#@/.snapshots/}"
	snapshot="${snapshot%/snapshot}"

	local initrd="${src%/*}/initrd"

	mkdir -p "$boot_root${dst%/*}"

	if [ -e "$initrd" ]; then
		ln -s "$initrd" "$tmpdir/initrd"
	elif ! reuse_initrd "$snapshot" "$subvol"; then
		local dracut_args=()
		if [ "$subvol" != "$root_subvol" ]; then
			dracut_args=('--sysroot' "/.snapshots/$snapshot/snapshot" '--tmpdir' '/var/tmp')
		fi
		log_info "generating new initrd"
		run_command_live_output dracut --quiet --reproducible "${dracut_args[@]}" "$tmpdir/initrd" "$kernel_version"
	fi

	local boot_options=
	for i in /etc/kernel/cmdline /usr/lib/kernel/cmdline /proc/cmdline; do
		[ -f "$i" ] || continue
		boot_options="$(sedrootflags "$subvol" < "$i")"
		break
	done

	if [ -z "$dstinitrd" ] && [ -e "$tmpdir/initrd" ]; then
		calc_chksum "$tmpdir/initrd"
		dstinitrd="${dst%/*}/initrd-$chksum"
	fi

	title="${os_release_PRETTY_NAME:-Linux $kernel_version}"
	# TW pretty name does not include the version
	[ -n "$os_release_VERSION" ] || title="$title $os_release_VERSION_ID"
	# shellcheck disable=SC2154
	sort_key="$os_release_ID"

	if ! is_transactional  && subvol_is_ro "$subvol"; then
		set_snapper_title_and_sortkey "$snapshot"
	fi

	local entry_machine_id=
	[ "$entry_token" = "$machine_id" ] && entry_machine_id="$machine_id"

	cat > "$tmpdir/entry.conf" <<-EOF
	# Boot Loader Specification type#1 entry
	title      $title
	version    $snapshot@$kernel_version${entry_machine_id:+${nl}machine-id $entry_machine_id}${sort_key:+${nl}sort-key   $sort_key}
	options    $boot_options
	linux      $dst
	initrd     $dstinitrd
	EOF

	local failed=
	if [ ! -e "$boot_root$dst" ]; then
		install_with_rollback "$src" "$boot_root$dst" || failed=kernel
	else
		log_info "reusing $boot_root$dst"
	fi
	if [ -z "$failed" ] && [ -e "$tmpdir/initrd" ] && [ ! -e "$boot_root$dstinitrd" ]; then
		install_with_rollback "$tmpdir/initrd" "$boot_root$dstinitrd" || failed=initrd
	fi
	if [ -z "$failed" ]; then
		loader_entry="$boot_root/loader/entries/$entry_token-$kernel_version-$snapshot.conf"
		install_with_rollback "$tmpdir/entry.conf" "$loader_entry" || failed="bootloader entry"
	fi
	rm -f "$tmpdir/initrd"
	rm -f "$tmpdir/entry.conf"
	[ -z "$failed" ] || err "Failed to install $failed"
	reset_rollback
}

install_all_kernels()
{
	local subvol="@/.snapshots/${1:?}/snapshot"
	find_kernels "$subvol"
	for kv in "${!found_kernels[@]}"; do
		log_info "installing $kv"
		install_kernel "${subvol}" "$kv"
	done

}

remove_all_kernels()
{
	local subvol="@/.snapshots/${1:?}/snapshot"
	find_kernels "$subvol"
	for kv in "${!found_kernels[@]}"; do
		remove_kernel "${subvol}" "$kv"
	done

}

entry_filter=("cat")
update_entries()
{
	[ -z "$1" ] || entry_filter=("$@")
	bootctl list --json=short | "${entry_filter[@]}" > "$entryfile"
}

update_entries_for_subvol()
{
	local subvol="$1"
	update_entries jq "[.[]|select(has(\"options\"))|select(.options|match(\"root=UUID=$root_uuid .*rootflags=subvol=$subvol\"))]"
}



update_entries_for_snapshot()
{
	local n="$1"
	update_entries_for_subvol "@/.snapshots/$n/snapshot"
}

show_entries()
{
	local dialogtitle="${1:-Entries}"

	[ -s "$entryfile" ] || update_entries

	while true; do
		local list=()
		local n=0
		local default=
		while read -r isdefault isreported type title; do
			color=
			if [ "$isdefault" = "true" ]; then
				default="$n"
				color="\\Zb\Zu"
			fi
			if [ "$isreported" = "false" ]; then
				color="$color\\Z2"
			fi
			if [ "$type" = "loader" ]; then
				color="$color\\Z5"
			fi
			list+=("$n" "$color$title\\Zn")
			n=$((++n))
		done < <(jq '.[]|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .showTitle]|join(" ")' -r < "$entryfile")
		if [ "${#list}" = 0 ]; then
			d --msgbox "No entries" 0 0
			return 0
		fi
		d --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "$dialogtitle" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		show_entry ".[$n]"
	done
}

show_entry()
{
	local filter="$1"
	local type
	local isreported
	local isdefault
	local new=

	read -r isdefault isreported type title < <(jq "$filter"'|[.isDefault, if has("isReported") then .isReported else 0 end, if has("type") then .type else "unknown" end, .showTitle]|join(" ")' -r < "$entryfile")

	[ "$isdefault" = true ] || isdefault=
	[ "$isreported" = true ] || new=1

	if [ -n "$isdefault$new" ]; then
		title="$title ["
		[ -z "$isdefault" ] || title="${title}default"
		[ -z "$new" ] || title="${title}${isdefault:+,}new"
		title="$title]"
	fi

	while true; do
		local list=(show json)
		if [ "$type" = "type1" ]; then
			list+=(cat Raw edit Edit)
		fi
		if [ -z "$isdefault" ]; then
			list+=(set-default "set as default" oneshot "set as one-shot")
			if [ "$type" != "loader" ]; then
				list+=(delete delete)
			fi
		fi
		d --no-tags --menu "Entry #$title" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
		action="$result"

		case "$action" in
			show)
				jq "$filter" < "$entryfile" > "$tmpfile"
				d --textbox "$tmpfile" 0 0
				;;
			cat)
				read -r fn < <(jq -r "$filter|.path" < "$entryfile")
				d --textbox "$fn" 0 0
				;;
			edit)
				read -r fn < <(jq -r "$filter|.path" < "$entryfile")
				${EDITOR:-vim} "$fn"
				update_entries
				;;
			delete)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl unlink "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
			set-default)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl set-default "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
			oneshot)
				read -r id < <(jq -r "$filter|.id" < "$entryfile")
				bootctl set-oneshot "$id" > "$tmpfile" 2>&1
				[ -s "$tmpfile" ] && d --textbox "$tmpfile" 0 0
				update_entries
				break
				;;
		esac
	done
}

show_snapper()
{
	if ! update_snapper 2>"$tmpfile"; then
		d --title "Error" --textbox "$tmpfile" 0 0
		exit 1
	fi

	while true; do
		local list=()
		local n=0
		local default=
		while read -r n isdefault title; do
			[ "$n" != "0" ] || continue
			if [ "$isdefault" = "true" ]; then
				default="$n"
				title="\\Zb\Zu$title\\Zn"
			fi
			update_kernels "@/.snapshots/$n/snapshot"
			[ "$is_bootable" = 1 ] || title="!$title"
			list+=("$n" "$title")
		done < <(jq '.root|.[]|[.number, .default, .description]|join(" ")' -r < "$snapperfile")
		if [ "${#list}" = 0 ]; then
			d --msgbox "No snapshots" 0 0
			return 0
		fi
		d --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "Snapshots" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		while true; do
			list=(kernels kernels entries entries show json)
			if [ "$n" != "$default" ]; then
				list+=(delete delete)
			fi
			d --no-tags --menu "Snapshot #$n" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
			action="$result"

			case "$action" in
				show)
					jq ".root|.[]|select(.number==$n)" < "$snapperfile" > "$tmpfile"
					d --textbox "$tmpfile" 0 0
					;;
				entries)
					#read -r MACHINE_ID < /etc/machine-id
					#update_entries jq "[.[]|select(.machineId==\"$MACHINE_ID\")|select(has(\"options\"))|select(.options|match(\"@/.snapshots/$n/snapshot\"))]"
					update_entries_for_snapshot "$n"
					show_entries "Entries for Snapshot $n"
					;;
				kernels)
					show_kernels "$n"
					;;
			esac
		done
	done
}

calc_chksum() {
    # shellcheck disable=SC2046
    set -- $(sha1sum "$1")
    chksum="$1"
}

# map with kernel version as key and checksum as value
declare -A found_kernels
find_kernels()
{
	local subvol="${1:?}"
	found_kernels=()

	for fn in "${subvol#@}"/usr/lib/modules/*/"$image"; do
		kv="${fn%/*}"
		kv="${kv##*/}"
		calc_chksum "$fn"
		found_kernels["$kv"]="$chksum"
		log_info "found kernel $kv = $chksum"
	done
}

# map that uses expected path on the ESP for each kernel. The value indicates
# whether that kernel is installed, stale or missing in the ESP
declare -A known_kernels
is_bootable=
has_stale=
update_kernels()
{
	local subvol="${1:?}"
	known_kernels=()
	is_bootable=
	has_stale=
	find_kernels "$subvol"
	for kv in "${!found_kernels[@]}"; do
		known_kernels["/$entry_token/$kv/linux-${found_kernels[$kv]}"]=missing
	done
	update_entries_for_subvol "$subvol"

	while read -r k id; do
		# kernel in ESP that is not installed
		if [ -z "${known_kernels[$k]}" ]; then
			known_kernels["$k"]="!$id"
			has_stale=1
		elif [ "${known_kernels[$k]}" = missing ]; then
			known_kernels["$k"]="$id"
			is_bootable=1
		fi
	done < <(jq -r '.[]|select(has("linux"))|[.linux,.id]|join(" ")'< "$entryfile")
}

list_kernels()
{
	subvol="@/.snapshots/${1:?}/snapshot"
	update_kernels "$subvol"
	local kernelfiles=("${!known_kernels[@]}")
	for k in "${kernelfiles[@]}"; do
		state="${known_kernels[$k]}"
		printf "%-10s %s\n" "$state" "$k"
	done
}

is_bootable()
{
	subvol="@/.snapshots/${1:?}/snapshot"
	update_kernels "$subvol"

	[ "$is_bootable" = 1 ] || return 1
	return 0
}

show_kernels()
{
	subvol="@/.snapshots/${1:?}/snapshot"
	while true; do
		update_kernels "$subvol"
		local list=()
		local n=0
		local default=
		local kernelfiles=("${!known_kernels[@]}")
		local states=()
		for k in "${kernelfiles[@]}"; do
			state="${known_kernels[$k]}"
			if [ "$state" != 'missing' ]; then
				if [ "${state:0:1}" = '!' ]; then
					state="stale"
				else
					state="ok"
				fi
			fi
			states+=("$state")
			s="${k#/*/}"
			list+=("$n" "$(printf "%-10s %s" "$state" "$s")")
			n=$((++n))
		done
		if [ "${#list}" = 0 ]; then
			d --msgbox "No kernels" 0 0
			return 1
		fi
		d --no-tags --no-hot-list --colors --ok-label "Options" --cancel-label "Back" --menu "Kernels associated with $subvol" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		n="$result"

		local id="${known_kernels[${kernelfiles[$n]}]}"
		list=()
		if [ "$id" = "missing" ]; then
			list+=(install "Install")
		else
			[ "${id:0:1}" = '!' ] && id="${id:1}" # stale
			list+=(show "Entry")
		fi
		list+=(entries "Other Entries")

		local kv="${kernelfiles[$n]%/*}"
		kv="${kv##*/}"
		local title="Kernel $kv"
		while true; do
			d --no-tags --no-hot-list --colors --ok-label "Ok" --cancel-label "Back" --menu "$title" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || break
			action="$result"

			case "$action" in
				entries)
					update_entries jq "[.[]|select(has(\"linux\"))|select(.linux|match(\"${kernelfiles[$n]}\"))]"
					show_entries "Entries for kernel ${kernelfiles[$n]#/*/}"
					;;
				show)
					show_entry ".[]|select(.id|match(\"$id\"))"
					break # might have selected delete so refresh
					;;
				install)
					install_kernel "$subvol" "$kv"
					break
					;;
			esac
		done
	done
}

sdboot_version()
{
	local fn="$1"
	if [ -z "$1" ]; then
		if [ -e "$shimdir/shim.efi" ]; then
			fn="$boot_root$sdboot_dst/grub.efi"
		else
			local sdboot
			sdboot="$(find_sdboot)"
			fn="$boot_root$sdboot_dst/${sdboot##*/}"
		fi
	fi
	[ -e "$fn" ] || return 1
	read -r _ _ _ v _ < <(grep -ao '#### LoaderInfo: systemd-boot [^#]\+ ####' "$fn")
	[ -n "$v" ] || return 1
	echo "$v"
}

is_installed()
{
	sdboot_version > /dev/null && [ -e "$boot_root/$sdboot_dst/installed_by_sdbootutil" ]
}

find_sdboot()
{
	local prefix="/.snapshots/${1-$root_snapshot}/snapshot"
	# XXX: this is a hack in case we need to inject a signed
	# systemd-boot from a separate package
	local sdboot="$prefix/usr/lib/systemd-boot/systemd-boot$firmware_arch.efi"
	[ -e "$sdboot" ] || sdboot="$prefix/usr/lib/systemd/boot/efi/systemd-boot$firmware_arch.efi"
	[ -e "$sdboot" ] || err "missing $sdboot"
	echo "$sdboot"
}

sdboot_needs_update()
{
	local snapshot="${1-$root_snapshot}"
	local prefix="/.snapshots/${snapshot}/snapshot"
	local v nv
	v="$(sdboot_version)"
	[ -n "$v" ] || return 1
	log_info "deployed version $v"
	nv="$(sdboot_version "$(find_sdboot "$snapshot")")"
	[ -n "$v" ] || return 1
	log_info "system version $nv"
	systemd-analyze compare-versions "$v" lt "$nv" 2>/dev/null || return 1
	log_info "systemd-boot needs to be updated"
	return 0
}

install_sdboot()
{
	local snapshot="${1:-$root_snapshot}"
	local prefix="/.snapshots/${root_snapshot}/snapshot"
	local sdboot blkpart drive partno

	sdboot=$(find_sdboot "$1")

	mkdir -p "$boot_root/loader/entries"

	mountpoint -q "$boot_root" || err "$boot_root is not a valid mountpoint"
	blkpart="$(findmnt -nvo SOURCE "$boot_root")"
	[ -L "/sys/class/block/${blkpart##*/}" ] || err "$blkpart is not a partition"
	drive="$(readlink -f "/sys/class/block/${blkpart##*/}")"
	drive="${drive%/*}"
	drive="/dev/${drive##*/}"
	read -r partno < "/sys/class/block/${blkpart##*/}"/partition

	if [ -e "$prefix$shimdir/shim.efi" ]; then
		log_info "Installing systemd-boot with shim into $boot_root"
		entry="$sdboot_dst/shim.efi"
		for i in MokManager shim; do
			install -D "$prefix$shimdir/$i.efi" "$boot_root$sdboot_dst/$i.efi"
		done
		install -D "$sdboot" "$boot_root$sdboot_dst/grub.efi"

		# boot entry point
		for i in MokManager fallback; do
			install -D "$prefix$shimdir/$i.efi" "$boot_root/EFI/BOOT/$i.efi"
		done
		install -D "$prefix$shimdir/shim.efi" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	else
		log_info "Installing systemd-boot into $boot_root"
		entry="$sdboot_dst/${sdboot##*/}"
		install -D "$sdboot" "$boot_root$entry"
		install -D "$sdboot" "$boot_root/EFI/BOOT/BOOT${firmware_arch^^}.EFI"
	fi
	# this is for shim to create the entry if missing
	echo "${entry##*/},openSUSE Boot Manager" | iconv -f ascii -t ucs2 > "$boot_root/$sdboot_dst/boot.csv"

	mkdir -p "$boot_root/$entry_token"
	echo "$entry_token" > "$boot_root/$sdboot_dst/installed_by_sdbootutil"
	mkdir -p "/etc/kernel"
	[ -s /etc/kernel/entry-token ] || echo "$entry_token" > /etc/kernel/entry-token
	update_random_seed

	[ -s "$boot_root/loader/entries.srel" ] || echo type1 > "$boot_root/loader/entries.srel"

    	[ -e "$boot_root/loader/loader.conf" ] || echo -e "#timeout 3\n#console-mode keep\n" > "$boot_root/loader/loader.conf"

	# Create boot menu entry if it does not exist
	[ -n "$arg_no_variables" ] || efibootmgr | grep -q 'Boot.*openSUSE Boot Manager' || efibootmgr -q --create --disk "$drive" --part "$partno" --label "openSUSE Boot Manager" --loader "$entry" || true
}

settle_entry_token()
{
	case "$arg_entry_token" in
		"") [ -n "$entry_token" ] || entry_token="$machine_id" ;;
		auto)
			if [ -s '/etc/kernel/entry-token' ]; then
				read -r entry_token < '/etc/kernel/entry-token'
			else
				entry_token="$machine_id"
				# bootctl has more here in case the machine id
				# is random falls back to trying IMAGE_ID and
				# ID, only them machine id. We assume there is
				# a valid machine-id
			fi
			;;
		machine-id) entry_token="$machine_id" ;;
		os-id)
			# shellcheck disable=SC2154
			entry_token="$os_release_ID"
			[ -n "$entry_token" ] || err "Missing ID"
			;;
		os-image)
			# shellcheck disable=SC2154
			entry_token="$os_release_IMAGE_ID"
			[ -n "$entry_token" ] || err "Missing IMAGE_ID"
			;;
		*) entry_token="$arg_entry_token" ;;
	esac
	return 0
}

hex_to_binary()
{
	local s="$1"
	local i
	for ((i=0;i<${#s};i+=2)); do eval echo -n "\$'\x${s:$i:2}'"; done
}

update_random_seed()
{
	local s _p
	read -r s _p < <({ dd if=/dev/urandom bs=32 count=1 status=none; [ -e "$boot_root/loader/random-seed" ] && dd if="$boot_root/loader/random-seed" bs=32 count=1 status=none; } | sha256sum)
	[ "${#s}" = 64 ] || { warn "Invalid random seed"; return 0; }
	hex_to_binary "$s" > "$boot_root/loader/random-seed.new"
	mv "$boot_root/loader/random-seed.new" "$boot_root/loader/random-seed"
}

install_sdboot_interactive()
{
	local v
	v="$(sdboot_version)"
	if [ -n "$v" ]; then
		if sdboot_needs_update; then
			local nv
			nv="$(sdboot_version "$(find_sdboot)")"
			d --aspect 60 --yesno "Update systemd-boot from $v to $nv?" 0 0 || return 0
		else
			d --aspect 60 --yesno "systemd-boot already at current version $v. Install again?" 0 0 || return 0
		fi
	else
		d --aspect 60 --yesno "Are you sure you want to install systemd-boot into $boot_root?\n
			This will overwrite any existing bootloaders" 0 0 || return 0
	fi
	install_sdboot
	d --aspect 60 --msgbox "Installed into $boot_root" 0 0
}

set_default_snapshot()
{
	local num="${1:?}"
	local configs
	update_entries_for_snapshot "$num"
	mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
	configs=("${configs[@]%$nl}")
	if [ -z "${configs[0]}" ]; then
		log_info "snapshot $num has no configs, trying to create them..."
		install_all_kernels "$num"
		update_entries_for_snapshot "$num"
		mapfile configs < <(jq '.[]|[.id]|join(" ")' -r < "$entryfile")
		configs=("${configs[@]%$nl}")
		if [ -z "${configs[0]}" ]; then
			err "snapshot $num has no kernels"
		fi
	fi
	log_info "setting default entry ${configs[0]}"
	bootctl set-default "${configs[0]}"
}

main_menu()
{
	while true; do
		list=(kernels Kernels snapper Snapshots sd-boot Entries install "Install/Update")
		d --no-tags --cancel-label "Quit"  --menu "Main Menu" 0 0 "$(menuheight ${#list[@]})" "${list[@]}" || return 0
		action="$result"

		case "$action" in
			snapper) show_snapper ;;
			sd-boot) update_entries cat; show_entries ;;
			kernels) show_kernels "$root_snapshot";;
			install) install_sdboot_interactive ;;
		esac
	done
}

####### main #######

getopttmp=$(getopt -o hc:v --long help,flicker,verbose,esp-path:,entry-token:,arch:,image:,no-variables,no-reuse-initrd -n "${0##*/}" -- "$@")
eval set -- "$getopttmp"

while true ; do
        case "$1" in
                -h|--help) helpandquit ;;
		--flicker) dialog_altenate_screen=--keep-tite; shift ;;
		-v|--verbose) verbose=$((++verbose)); shift ;;
		--esp-path) arg_esp_path="$2"; shift 2 ;;
		--arch) arg_arch="$2"; shift 2 ;;
		--entry-token) arg_entry_token="$2"; shift 2 ;;
		--image) image="$2"; shift 2 ;;
		--no-variables) arg_no_variables=1; shift ;;
		--no-reuse-initrd) arg_no_reuse_initrd=1; shift ;;
                --) shift ; break ;;
                *) echo "Internal error!" ; exit 1 ;;
        esac
done

case "$1" in
	install|needs-update|update|force-update|add-kernel|remove-kernel|set-default-snapshot|add-all-kernels|remove-all-kernels|is-installed|list-kernels|is-bootable) ;;
	kernels|snapshots|entries|"") stty_size; interactive=1 ;;
	*) err "unknown command $1" ;;
esac

for i in /etc/os-release /usr/lib/os-release; do
	[ -f "$i" ] || continue
	eval $(sed -ne '/^[A-Z_]\+=/s/^/os_release_/p' < "$i")
	break
done

[ -n "$arg_esp_path" ] && export SYSTEMD_ESP_PATH="$arg_esp_path"

# XXX: bootctl should have json output for that too
eval "$(bootctl 2>/dev/null | sed -ne 's/Firmware Arch: *\(\w\+\)/firmware_arch="\1"/p;s/ *token: *\(\w\+\)/entry_token="\1"/p;s, *\$BOOT: *\([^ ]\+\).*,boot_root="\1",p')"
root_uuid=$(findmnt / -r -n -o UUID)
root_subvol=$(btrfs subvol show / 2>/dev/null|head -1)
[ -s /etc/machine-id ] && read -r machine_id < /etc/machine-id

if [ -n "$arg_esp_path" ] && [ "$boot_root" != "$arg_esp_path" ]; then
	err "mismatch of esp path"
fi
[ -n "$arg_arch" ] && firmware_arch="$arg_arch"
settle_entry_token

[ -n "$boot_root" ] || err "No ESP detected. Legacy system?"
[ -n "$root_uuid" ] || err "Can't determine root UUID"
[ -n "$root_subvol" ] || err "Can't determine root subvolume"
[ -n "$entry_token" ] || err "No entry token. sd-boot not installed?"
[ -n "$firmware_arch" ] || err "Can't determine firmware arch"

root_snapshot="${root_subvol#@/.snapshots/}"
root_snapshot="${root_snapshot%/snapshot}"

if [ "$1" = "install" ]; then
	install_sdboot "${2:-$root_snapshot}"
elif [ "$1" = "needs-update" ]; then
	sdboot_needs_update "${2:-$root_snapshot}"
elif [ "$1" = "update" ]; then
	sdboot_needs_update "${2:-$root_snapshot}" && install_sdboot "${2:-$root_snapshot}"
elif [ "$1" = "force-update" ]; then
	is_installed && install_sdboot "${2:-$root_snapshot}"
elif [ "$1" = "add-kernel" ]; then
	install_kernel "${3:-$root_subvol}" "$2"
elif [ "$1" = "add-all-kernels" ]; then
	install_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "remove-kernel" ]; then
	remove_kernel "${3:-$root_subvol}" "$2"
elif [ "$1" = "remove-all-kernels" ]; then
	remove_all_kernels "${2:-$root_snapshot}"
elif [ "$1" = "set-default-snapshot" ]; then
	set_default_snapshot "${2:-$root_snapshot}"
elif [ "$1" = "is-installed" ]; then
	if is_installed; then
		log_info "systemd-boot was installed using sdbootutil"
		exit 0
	else
		log_info "not installed using this tool"
		exit 1
	fi
elif [ "$1" = "list-kernels" ]; then
	list_kernels "${2:-$root_snapshot}"
elif [ "$1" = "is-bootable" ]; then
	is_bootable "${2:-$root_snapshot}"
elif [ "$1" = "kernels" ]; then
	show_kernels "${2:-$root_snapshot}"
elif [ "$1" = "snapshots" ]; then
	show_snapper
elif [ "$1" = "entries" ]; then
	show_entries
else
	main_menu
fi
