#!/usr/bin/env bash
set -euo pipefail

# Finalize the installed system without evaluating data from the live kernel
# command line or following attacker-controlled files from /tmp.

readonly max_live_config_bytes=$((16 * 1024 * 1024))
# shellcheck disable=SC2034
readonly live_state_directory=/tmp
readonly live_theme_file=/tmp/big_desktop_theme
readonly live_desktop_file=/tmp/big_desktop_changed
readonly live_keyboard_file=/tmp/big_keyboard
readonly live_gnome_layout_file=/tmp/big_gnome_layout
readonly live_gnome_settings_file=/tmp/big_gnome_settings
readonly live_jamesdsp_file=/tmp/big_enable_jamesdsp
readonly live_display_profile_file=/tmp/big_improve_display
declare -a temporary_files=()
created_temporary_file=
root_mount=

cleanup_temporary_files() {
	local temporary_file
	for temporary_file in "${temporary_files[@]}"; do
		[[ -n $temporary_file ]] && rm -f -- "$temporary_file"
	done
	return 0
}

trap cleanup_temporary_files EXIT
trap 'exit 1' HUP INT TERM

die() {
	printf 'biglinux-install-setup: %s\n' "$*" >&2
	exit 1
}

load_kernel_options() {
	local installed_library=/usr/lib/biglinux-livecd/kernel-options
	local script_directory development_library
	if [[ -r $installed_library ]]; then
		# shellcheck disable=SC1090,SC1091
		source "$installed_library"
		return
	fi
	script_directory=$(realpath -e -- "$(dirname -- "${BASH_SOURCE[0]}")") || return 1
	development_library=$(realpath -e -- "$script_directory/../lib/biglinux-livecd/kernel-options") || return 1
	# shellcheck disable=SC1090,SC1091
	source "$development_library"
}

canonical_root_mount() {
	local requested=${1:-} resolved
	[[ -n $requested && -d $requested ]] || die "invalid root mount point: $requested"
	resolved=$(realpath -e -- "$requested") || die "cannot resolve root mount point: $requested"
	[[ $resolved != / ]] || die 'refusing to modify the running root filesystem'
	printf '%s\n' "$resolved"
}

path_is_within_root() {
	local resolved=$1
	[[ $resolved == "$root_mount" || $resolved == "$root_mount/"* ]]
}

ensure_directory() {
	local directory=$1 mode=${2:-0755}
	local resolved
	resolved=$(realpath -m -- "$directory") || die "cannot resolve directory: $directory"
	path_is_within_root "$resolved" || die "directory escapes installation root: $directory"
	install -d -m "$mode" -- "$directory"
	resolved=$(realpath -e -- "$directory") || die "cannot verify directory: $directory"
	path_is_within_root "$resolved" || die "directory escapes installation root: $directory"
}

make_temporary_file() {
	local target=$1 parent base temporary_file
	parent=$(dirname -- "$target")
	base=$(basename -- "$target")
	[[ -d $parent && ! -L $parent ]] || die "unsafe destination directory: $parent"
	temporary_file=$(mktemp -- "$parent/.${base}.XXXXXX") || die "cannot create temporary file for $target"
	temporary_files+=("$temporary_file")
	created_temporary_file=$temporary_file
}

forget_temporary_file() {
	local completed=$1 index
	for index in "${!temporary_files[@]}"; do
		if [[ ${temporary_files[$index]} == "$completed" ]]; then
			temporary_files[index]=
			return
		fi
	done
}

commit_temporary_file() {
	local temporary_file=$1 target=$2 mode=$3
	local owner group parent
	[[ ! -L $target ]] || die "refusing symbolic-link destination: $target"
	parent=$(dirname -- "$target")
	if [[ -e $target ]]; then
		[[ -f $target ]] || die "destination is not a regular file: $target"
		owner=$(stat -c '%u' -- "$target")
		group=$(stat -c '%g' -- "$target")
	else
		owner=$(stat -c '%u' -- "$parent")
		group=$(stat -c '%g' -- "$parent")
	fi
	chmod "$mode" -- "$temporary_file"
	chown "$owner:$group" -- "$temporary_file"
	sync -f -- "$temporary_file"
	mv -fT -- "$temporary_file" "$target"
	forget_temporary_file "$temporary_file"
	sync -f -- "$parent"
}

trim_whitespace() {
	local value=$1
	value=${value#"${value%%[![:space:]]*}"}
	value=${value%"${value##*[![:space:]]}"}
	printf '%s\n' "$value"
}

decode_double_quoted_value() {
	local value=$1 decoded='' character next
	local index
	for ((index = 0; index < ${#value}; index++)); do
		character=${value:index:1}
		if [[ $character == \\ && $((index + 1)) -lt ${#value} ]]; then
			next=${value:index+1:1}
			# The single-character patterns are literal shell escapes.
			# shellcheck disable=SC1003
			case "$next" in
			'\\' | '"' | '$' | '`')
				decoded+=$next
				index=$((index + 1))
				continue
				;;
			esac
		fi
		decoded+=$character
	done
	printf '%s\n' "$decoded"
}

decode_grub_value() {
	local value
	value=$(trim_whitespace "$1")
	if [[ ${#value} -ge 2 && ${value:0:1} == '"' && ${value: -1} == '"' ]]; then
		decode_double_quoted_value "${value:1:${#value}-2}"
	elif [[ ${#value} -ge 2 && ${value:0:1} == "'" && ${value: -1} == "'" ]]; then
		printf '%s\n' "${value:1:${#value}-2}"
	else
		printf '%s\n' "$value"
	fi
}

escape_grub_double_quoted_value() {
	local value=$1
	value=${value//\\/\\\\}
	value=${value//\"/\\\"}
	value=${value//\$/\\\$}
	value=${value//\`/\\\`}
	printf '%s\n' "$value"
}

current_grub_kernel_arguments() {
	local grub_file=$1 line value=
	local matches=0
	while IFS= read -r line || [[ -n $line ]]; do
		if [[ $line =~ ^[[:space:]]*GRUB_CMDLINE_LINUX_DEFAULT[[:space:]]*=(.*)$ ]]; then
			matches=$((matches + 1))
			value=${BASH_REMATCH[1]}
		fi
	done <"$grub_file"
	((matches <= 1)) || die "ambiguous GRUB_CMDLINE_LINUX_DEFAULT entries in $grub_file"
	decode_grub_value "$value"
}

write_grub_configuration() {
	local grub_file=$1 existing_arguments live_arguments combined_arguments escaped_arguments
	local temporary_file line
	local wrote_arguments=0 wrote_savedefault=0 has_early_initrd=0
	[[ -f $grub_file && ! -L $grub_file ]] || die "unsafe GRUB configuration: $grub_file"
	existing_arguments=$(current_grub_kernel_arguments "$grub_file")
	live_arguments=$(installed_kernel_arguments) || die 'cannot read live kernel arguments'
	combined_arguments=$(sanitize_installed_kernel_arguments "$existing_arguments $live_arguments")
	escaped_arguments=$(escape_grub_double_quoted_value "$combined_arguments")
	make_temporary_file "$grub_file"
	temporary_file=$created_temporary_file
	while IFS= read -r line || [[ -n $line ]]; do
		if [[ $line =~ ^[[:space:]]*GRUB_CMDLINE_LINUX_DEFAULT[[:space:]]*= ]]; then
			if ((wrote_arguments == 0)); then
				printf 'GRUB_CMDLINE_LINUX_DEFAULT="%s"\n' "$escaped_arguments" >>"$temporary_file"
				wrote_arguments=1
			fi
		elif [[ $line =~ ^[[:space:]]*GRUB_SAVEDEFAULT[[:space:]]*= ]]; then
			if ((wrote_savedefault == 0)); then
				printf 'GRUB_SAVEDEFAULT=false\n' >>"$temporary_file"
				wrote_savedefault=1
			fi
		else
			printf '%s\n' "$line" >>"$temporary_file"
			[[ $line =~ ^[[:space:]]*GRUB_EARLY_INITRD_LINUX_STOCK[[:space:]]*= ]] && has_early_initrd=1
		fi
	done <"$grub_file"
	((wrote_arguments == 1)) || printf 'GRUB_CMDLINE_LINUX_DEFAULT="%s"\n' "$escaped_arguments" >>"$temporary_file"
	((wrote_savedefault == 1)) || printf 'GRUB_SAVEDEFAULT=false\n' >>"$temporary_file"
	((has_early_initrd == 1)) || printf "GRUB_EARLY_INITRD_LINUX_STOCK=''\n" >>"$temporary_file"
	commit_temporary_file "$temporary_file" "$grub_file" "$(stat -c '%a' -- "$grub_file")"
}

atomic_write_text() {
	local target=$1 mode=$2 text=$3 temporary_file
	ensure_directory "$(dirname -- "$target")"
	make_temporary_file "$target"
	temporary_file=$created_temporary_file
	printf '%s\n' "$text" >"$temporary_file"
	commit_temporary_file "$temporary_file" "$target" "$mode"
}

copy_live_config() {
	local source=$1 target=$2
	local temporary_file copied_size
	[[ -f $source && ! -L $source ]] || return 0
	[[ $(LC_ALL=C stat -c '%F' -- "$source") == 'regular file' ]] || die "unsafe live configuration source: $source"
	ensure_directory "$(dirname -- "$target")"
	make_temporary_file "$target"
	temporary_file=$created_temporary_file
	dd if="$source" of="$temporary_file" bs=1M count=17 iflag=nofollow status=none
	copied_size=$(stat -c '%s' -- "$temporary_file")
	((copied_size <= max_live_config_bytes)) || die "live configuration is too large: $source"
	commit_temporary_file "$temporary_file" "$target" 0644
}

copy_gnome_settings_to_homes() {
	local source=$1 home_directory user_name settings_directory settings_file user_ids
	local user_id group_id resolved_home
	[[ -f $source && ! -L $source ]] || return 0
	copy_live_config "$source" "$root_mount/etc/big-default-config/gnome-settings"
	copy_live_config "$source" "$root_mount/etc/skel/.config/dconf/settings.gnome"
	for home_directory in "$root_mount"/home/*; do
		[[ -d $home_directory && ! -L $home_directory ]] || continue
		resolved_home=$(realpath -e -- "$home_directory") || continue
		path_is_within_root "$resolved_home" || die "home directory escapes installation root: $home_directory"
		user_name=$(basename -- "$home_directory")
		[[ $user_name =~ ^[a-z_][a-z0-9_-]*[$]?$ ]] || continue
		user_ids=$(awk -F: -v user="$user_name" '$1 == user { print $3 ":" $4 }' "$root_mount/etc/passwd" 2>/dev/null || true)
		[[ $user_ids =~ ^[0-9]+:[0-9]+$ ]] || continue
		IFS=: read -r user_id group_id <<<"$user_ids"
		((user_id > 0)) || continue
		settings_directory=$home_directory/.config/dconf
		settings_file=$settings_directory/settings.gnome
		ensure_directory "$settings_directory"
		copy_live_config "$source" "$settings_file"
		chown "$user_id:$group_id" "$home_directory/.config" "$settings_directory" "$settings_file"
	done
}

valid_xkb_component() {
	[[ $1 =~ ^[A-Za-z0-9_.+-]+$ ]]
}

valid_live_xkb_layout() {
	[[ $1 =~ ^[A-Za-z0-9_.+-]+(\([A-Za-z0-9_.+-]+\))?$ ]]
}

read_live_keyboard_layout() {
	local keyboard layout variant
	[[ -f $live_keyboard_file && ! -L $live_keyboard_file ]] || return 1
	keyboard=$(<"$live_keyboard_file")
	valid_live_xkb_layout "$keyboard" || return 1
	layout=$keyboard
	variant=
	if [[ $layout =~ ^([^()]+)\(([^()]+)\)$ ]]; then
		layout=${BASH_REMATCH[1]}
		variant=${BASH_REMATCH[2]}
	fi
	printf '%s\t%s\n' "$layout" "$variant"
}

read_installed_keyboard_layout() {
	local keyboard_config layout variant
	keyboard_config=$root_mount/etc/X11/xorg.conf.d/00-keyboard.conf
	[[ -f $keyboard_config && ! -L $keyboard_config ]] || return 1
	layout=$(awk -F'"' '$0 ~ /^[[:space:]]*Option[[:space:]]+"XkbLayout"/ { print $4; exit }' "$keyboard_config")
	variant=$(awk -F'"' '$0 ~ /^[[:space:]]*Option[[:space:]]+"XkbVariant"/ { print $4; exit }' "$keyboard_config")
	valid_xkb_component "$layout" || return 1
	[[ -z $variant ]] || valid_xkb_component "$variant" || return 1
	printf '%s\t%s\n' "$layout" "$variant"
}

selected_keyboard_layout() {
	read_installed_keyboard_layout || read_live_keyboard_layout
}

xkb_layout_state_value() {
	if [[ -n $2 ]]; then
		printf '%s(%s)\n' "$1" "$2"
	else
		printf '%s\n' "$1"
	fi
}

fcitx5_keyboard_input_method() {
	if [[ -n $2 ]]; then
		printf 'keyboard-%s-%s\n' "$1" "$2"
	else
		printf 'keyboard-%s\n' "$1"
	fi
}

plasma_keyboard_config() {
	local layout=$1 variant=$2
	printf '[Layout]\n'
	printf 'DisplayNames=\n'
	printf 'LayoutList=%s\n' "$layout"
	printf 'Model=pc105\n'
	printf 'Options=terminate:ctrl_alt_bksp\n'
	printf 'ResetOldOptions=true\n'
	printf 'Use=true\n'
	printf 'VariantList=%s\n' "$variant"
}

fcitx5_keyboard_profile() {
	local layout=$1 variant=$2 input_method
	input_method=$(fcitx5_keyboard_input_method "$layout" "$variant")
	printf '[Groups/0]\n'
	printf '# Group Name\n'
	printf 'Name="Live Keyboard"\n'
	printf '# Layout\n'
	printf 'Default Layout=%s\n' "$layout"
	printf '# Default Input Method\n'
	printf 'DefaultIM=%s\n\n' "$input_method"
	printf '[Groups/0/Items/0]\n'
	printf '# Name\n'
	printf 'Name=%s\n' "$input_method"
	printf '# Layout\n'
	printf 'Layout=\n\n'
	printf '[GroupOrder]\n'
	printf '0="Live Keyboard"\n'
}

write_owned_text() {
	local target=$1 mode=$2 text=$3 owner_spec=${4:-}
	atomic_write_text "$target" "$mode" "$text"
	[[ -z $owner_spec ]] || chown "$owner_spec" "$target"
}

target_has_plasma_session() {
	[[ -x $root_mount/usr/bin/startkde-biglinux ||
		-e $root_mount/usr/share/wayland-sessions/plasmawayland.desktop ||
		-e $root_mount/usr/share/xsessions/plasma.desktop ]]
}

write_plasma_keyboard_config() {
	local config_directory=$1 owner_spec=$2 layout=$3 variant=$4 fcitx_directory
	ensure_directory "$config_directory"
	write_owned_text "$config_directory/kxkbrc" 0644 "$(plasma_keyboard_config "$layout" "$variant")" "$owner_spec"
	fcitx_directory=$config_directory/fcitx5
	ensure_directory "$fcitx_directory"
	write_owned_text "$fcitx_directory/profile" 0644 "$(fcitx5_keyboard_profile "$layout" "$variant")" "$owner_spec"
	[[ -z $owner_spec ]] || chown "$owner_spec" "$config_directory" "$fcitx_directory"
}

apply_installed_keyboard_config() {
	local layout variant keyboard_state home_directory user_name user_ids user_id group_id resolved_home
	if ! read -r layout variant < <(selected_keyboard_layout); then
		return 0
	fi
	keyboard_state=$(xkb_layout_state_value "$layout" "$variant")
	atomic_write_text "$root_mount/etc/big-default-config/keyboard" 0644 "$keyboard_state"
	target_has_plasma_session || return 0
	write_plasma_keyboard_config "$root_mount/etc/skel/.config" "" "$layout" "$variant"
	for home_directory in "$root_mount"/home/*; do
		[[ -d $home_directory && ! -L $home_directory ]] || continue
		resolved_home=$(realpath -e -- "$home_directory") || continue
		path_is_within_root "$resolved_home" || die "home directory escapes installation root: $home_directory"
		user_name=$(basename -- "$home_directory")
		[[ $user_name =~ ^[a-z_][a-z0-9_-]*[$]?$ ]] || continue
		user_ids=$(awk -F: -v user="$user_name" '$1 == user { print $3 ":" $4 }' "$root_mount/etc/passwd" 2>/dev/null || true)
		[[ $user_ids =~ ^[0-9]+:[0-9]+$ ]] || continue
		IFS=: read -r user_id group_id <<<"$user_ids"
		((user_id > 0)) || continue
		write_plasma_keyboard_config "$home_directory/.config" "$user_id:$group_id" "$layout" "$variant"
	done
}

current_gnome_settings_source() {
	local live_home_root=${1:-/home}
	local fallback=${2:-$live_gnome_settings_file}
	local resolved_root home_directory candidate resolved_home resolved_candidate
	resolved_root=$(realpath -e -- "$live_home_root") || {
		printf '%s\n' "$fallback"
		return 0
	}
	for home_directory in "$resolved_root"/*; do
		[[ -d $home_directory && ! -L $home_directory ]] || continue
		resolved_home=$(realpath -e -- "$home_directory") || continue
		[[ $resolved_home == "$resolved_root/"* ]] || continue
		candidate=$home_directory/.config/dconf/settings.gnome
		[[ -f $candidate && ! -L $candidate ]] || continue
		resolved_candidate=$(realpath -e -- "$candidate") || continue
		[[ $resolved_candidate == "$resolved_home/"* ]] || continue
		printf '%s\n' "$resolved_candidate"
		return 0
	done
	printf '%s\n' "$fallback"
}

main() {
	local grub_file sddm_state config_directory session gnome_settings_source
	((EUID == 0)) || die 'this setup must run as root'
	load_kernel_options || die 'cannot load kernel option parser'
	root_mount=$(canonical_root_mount "${1:-}")
	grub_file=$root_mount/etc/default/grub
	sddm_state=$root_mount/var/lib/sddm/state.conf
	config_directory=$root_mount/etc/big-default-config

	[[ ! -e $grub_file ]] || write_grub_configuration "$grub_file"
	if kernel_has_argument wayland; then
		session=/usr/share/wayland-sessions/plasmawayland.desktop
	else
		session=/usr/share/xsessions/plasma.desktop
	fi
	atomic_write_text "$sddm_state" 0644 $'[Last]\nSession='"$session"
	ensure_directory "$config_directory"
	copy_live_config "$live_theme_file" "$config_directory/theme"
	copy_live_config "$live_desktop_file" "$config_directory/desktop"
	apply_installed_keyboard_config
	copy_live_config "$live_gnome_layout_file" "$config_directory/gnome-layout"
	gnome_settings_source=$(current_gnome_settings_source /home)
	copy_gnome_settings_to_homes "$gnome_settings_source"
	copy_live_config "$live_jamesdsp_file" "$config_directory/jamesdsp"
	copy_live_config "$live_display_profile_file" "$config_directory/display-profile"
	printf '%s\n' 'BigLinux installation setup completed successfully'
}

if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
	main "$@"
fi
