#!/usr/bin/env bash
#
# cachy-auto-update: unattended updates for CachyOS
#
# The command line front end. Everything it does is either flipping a switch in
# /etc/cachy-auto-update/cachy-auto-update.conf, driving the systemd timer, or
# handing off to the runner in @LIBEXECDIR@.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later

set -uo pipefail

CAU_LIBDIR="${CAU_LIBDIR:-@LIBDIR@}"
CAU_LIBEXECDIR="${CAU_LIBEXECDIR:-@LIBEXECDIR@}"

for _mod in common config users conditions locks notify menu; do
	# shellcheck source=/dev/null
	if ! source "$CAU_LIBDIR/$_mod.sh"; then
		printf 'cachy-auto-update: cannot load %s/%s.sh\n' "$CAU_LIBDIR" "$_mod" >&2
		exit 14
	fi
done
unset _mod

CAU_SELF="$(readlink -f "${BASH_SOURCE[0]}")"
CAU_ARGV=("$@")

# ---------------------------------------------------------------------------
# Privilege
# ---------------------------------------------------------------------------

# Re-executes itself with elevation when a human is at the terminal, so that
# typing `cachy-auto-update` just works instead of printing a lecture.
cau_need_root() {
	cau_is_root && return 0

	local candidate elevate=''
	if [[ -t 0 && -t 1 ]]; then
		for candidate in sudo run0 doas; do
			if cau_have "$candidate"; then elevate="$candidate"; break; fi
		done
	fi

	if [[ -n $elevate ]]; then
		exec "$elevate" "$CAU_SELF" "${CAU_ARGV[@]}"
	fi

	cau_bad "$(cau_msg "This command has to run as root.")"
	exit 2
}

# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

cau_do_enable() {
	cau_need_root

	# A broken sudoers drop-in would take down sudo for the whole machine, so
	# it is verified before anything is switched on.
	if [[ -f /etc/sudoers.d/cachy-auto-update ]]; then
		if cau_have visudo && ! visudo -cf /etc/sudoers.d/cachy-auto-update > /dev/null 2>&1; then
			cau_bad "$(cau_msg "/etc/sudoers.d/cachy-auto-update is invalid; refusing to enable.")"
			return 1
		fi
	fi

	cau_config_set Enabled yes || { cau_bad "$(cau_msg "Could not write the configuration file.")"; return 1; }

	if cau_have systemctl; then
		systemctl enable --now cachy-auto-update.timer > /dev/null 2>&1 \
			|| cau_bad "$(cau_msg "Could not enable the systemd timer.")"
	fi

	cau_ok "$(cau_msg "Automatic updates are on.")"

	cau_config_load
	if [[ $CFG_AUR == yes ]] && ! cau_aur_detect_quiet; then
		cau_note "$(cau_msg "No AUR helper found. Install paru or yay for AUR updates.")"
	fi
	if [[ $CFG_AUR == yes ]] && ! cau_have makepkg; then
		cau_note "$(cau_msg "base-devel is missing, so AUR packages cannot be built.")"
	fi

	cau_offer_disable_cachy_update
	cau_offer_disable_reboot_hook
}

# CachyOS ships a pacman hook that pops up "Reboot recommended!" the moment a
# kernel, driver or systemd package is unpacked. During a manual upgrade that
# is fine, because the transaction is the last thing happening. During an unattended
# one it lands in the middle: the run still has AUR packages to build and
# Flatpaks to pull, and a notification asking for a restart right then is an
# invitation to cut the update in half.
#
# pacman lets a hook be overridden by name from /etc/pacman.d/hooks, which takes
# precedence over /usr/share/libalpm/hooks, and a symlink to /dev/null there
# disables it. That is the standard way to switch off a distribution hook, and
# it is undone by deleting the link.
#
# This one is offered, never decided: it is another package's behaviour and it
# applies to manual upgrades too.
CAU_REBOOT_HOOK="cachyos-reboot-required.hook"

cau_offer_disable_reboot_hook() {
	local system="/usr/share/libalpm/hooks/$CAU_REBOOT_HOOK"
	local override="/etc/pacman.d/hooks/$CAU_REBOOT_HOOK"
	local answer

	[[ -t 0 && -t 1 ]] || return 0
	[[ -f $system ]] || return 0
	[[ -e $override || -L $override ]] && return 0

	printf '\n  %s\n  ' \
		"$(cau_msg "CachyOS shows a \"Reboot recommended\" notification while an update is still running. Turn it off? [Y/n]")"
	read -r answer || return 0

	case "${answer,,}" in
		''|y|yes|j|ja) ;;
		*) return 0 ;;
	esac

	if mkdir -p /etc/pacman.d/hooks 2>/dev/null && ln -sfn /dev/null "$override" 2>/dev/null; then
		cau_ok "$(cau_msg "The reboot notification has been turned off. Undo it by deleting %s." "$override")"
	else
		cau_bad "$(cau_msg "Could not write %s." "$override")"
	fi
}

# Whether an AUR helper exists at all, without the logging cau_aur_detect does.
cau_aur_detect_quiet() {
	local c
	if [[ -n ${CFG_AUR_HELPER:-} && $CFG_AUR_HELPER != auto ]]; then
		cau_have "$CFG_AUR_HELPER"
		return $?
	fi
	for c in paru yay pikaur; do cau_have "$c" && return 0; done
	return 1
}

# cachy-update ships an enabled user timer that only ever says "N updates
# available". Once updates apply themselves that notification is pure noise,
# so offer to silence it. But only ask, never decide.
cau_offer_disable_cachy_update() {
	local user uid answer found=0

	[[ -t 0 && -t 1 ]] || return 0
	cau_have systemctl || return 0

	while read -r user uid; do
		[[ -n $user ]] || continue
		if cau_as_user "$user" "$uid" systemctl --user is-enabled --quiet arch-update.timer 2>/dev/null; then
			found=1
			break
		fi
	done < <(cau_active_session_users)

	(( found )) || return 0

	printf '\n  %s\n  ' \
		"$(cau_msg "cachy-update also notifies about available updates. Turn its notifications off? [Y/n]")"
	read -r answer || return 0

	# Defaults to yes: once updates install themselves, cachy-update's "N
	# updates available" is purely noise. "j" is accepted too: the prompt is
	# translated, so a German user types the German letter and used to have
	# that silently read as "no".
	case "${answer,,}" in
		''|y|yes|j|ja) ;;
		*) return 0 ;;
	esac

	while read -r user uid; do
		[[ -n $user ]] || continue
		cau_as_user "$user" "$uid" systemctl --user disable --now arch-update.timer > /dev/null 2>&1 || true
	done < <(cau_active_session_users)

	cau_ok "$(cau_msg "cachy-update's update check has been disabled.")"
}

cau_do_disable() {
	cau_need_root

	cau_config_set Enabled no || { cau_bad "$(cau_msg "Could not write the configuration file.")"; return 1; }

	if cau_have systemctl; then
		systemctl disable --now cachy-auto-update.timer > /dev/null 2>&1 || true
	fi

	cau_ok "$(cau_msg "Automatic updates are off.")"
}

cau_do_notifications() {
	cau_need_root

	case "${1:-}" in
		on|yes|true|1)
			cau_config_set Notifications yes || return 1
			cau_ok "$(cau_msg "Notifications are on.")"
			;;
		off|no|false|0)
			cau_config_set Notifications no || return 1
			cau_ok "$(cau_msg "Notifications are off.")"
			;;
		*)
			cau_bad "$(cau_msg "Usage: cachy-auto-update notifications on|off")"
			return 1
			;;
	esac
}

cau_do_status() {
	cau_config_load
	cau_head "  $CAU_PRETTY"
	cau_ui_status
	cau_ui_status_conditions
	printf '\n'
}

cau_do_run() {
	cau_need_root
	# Not exec'd: this is also called from the menu, which has to survive the
	# run and redraw afterwards.
	"$CAU_LIBEXECDIR/cachy-auto-update-run" "$@"
}

cau_do_log() {
	local file="$CAU_RUNLOG" lines=200 follow=0

	while (( $# )); do
		case "$1" in
			-n) lines="${2:-200}"; shift 2 ;;
			-n*) lines="${1#-n}"; shift ;;
			-f|--follow) follow=1; shift ;;
			-a|--all) file="$CAU_LOGFILE"; shift ;;
			*) shift ;;
		esac
	done

	if [[ ! -r $file ]]; then
		cau_note "$(cau_msg "No log yet.")"
		return 0
	fi

	if (( follow )); then
		tail -n "$lines" -f "$file"
	else
		tail -n "$lines" "$file"
	fi
}

cau_do_help() {
	cat <<- EOF
	$CAU_PRETTY $CAU_VERSION

	$(cau_msg "Usage: cachy-auto-update [command]")

	$(cau_msg "Commands:")
	  enable                    $(cau_msg "Turn automatic updates on")
	  disable                   $(cau_msg "Turn automatic updates off")
	  notifications on|off      $(cau_msg "Turn desktop notifications on or off")
	  status                    $(cau_msg "Show the current state and conditions")
	  run [--force] [--dry-run] $(cau_msg "Run an update now")
	  log [-n N] [-f] [-a]      $(cau_msg "Show the log of the last run")
	  -h, --help                $(cau_msg "Show this help")
	  -V, --version             $(cau_msg "Show the version")

	$(cau_msg "Without a command an interactive menu is shown.")
	EOF
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

main() {
	local cmd="${1:-}"
	[[ $# -gt 0 ]] && shift

	case "$cmd" in
		enable)         cau_do_enable "$@" ;;
		disable)        cau_do_disable "$@" ;;
		notifications)  cau_do_notifications "$@" ;;
		status)         cau_do_status "$@" ;;
		run)            cau_do_run "$@" ;;
		log)            cau_do_log "$@" ;;

		# Invoked unprivileged from the XDG autostart entry to replay
		# notifications that were produced while nobody was logged in.
		--deliver-notifications)
			cau_config_load
			cau_notify_deliver_queue
			;;

		-h|--help|help) cau_do_help ;;
		-V|--version)   printf '%s %s\n' "$CAU_NAME" "$CAU_VERSION" ;;

		'')
			cau_need_root
			cau_ui_menu
			;;
		*)
			cau_bad "$(cau_msg "Unknown command: %s" "$cmd")"
			printf '\n'
			cau_do_help
			exit 1
			;;
	esac
}

main "$@"
