#!/usr/bin/env bash
#
# cachy-auto-update-run: one unattended update pass
#
# Started by cachy-auto-update.service as root. The timer fires hourly and this
# decides whether anything should happen; that is what makes deferrals free.
# Refusing to run is the normal, expected outcome most of the time, so it is
# never treated as an error.
#
# Copyright (C) 2026 Felitendo
# SPDX-License-Identifier: GPL-3.0-or-later

set -uo pipefail

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

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

FORCE=0
DRY_RUN=0

for arg in "$@"; do
	case "$arg" in
		--force)   FORCE=1 ;;
		--dry-run) DRY_RUN=1 ;;
		--debug)   CAU_DEBUG=1 ;;
	esac
done

if [[ $EUID -ne 0 ]]; then
	printf 'cachy-auto-update-run: must run as root\n' >&2
	exit 2
fi

# Force a neutral locale here rather than relying on the unit's Environment=,
# so a run started by hand behaves exactly like one started by the timer.
# pacman failures are classified by matching its output, and on a German system
# that output is German. Text aimed at a person does not come through here: a
# notification is rendered in the recipient's own locale by cau_msg_in.
export LC_ALL=C LANGUAGE=

# Re-exec under an inhibitor so a suspend or shutdown cannot land in the middle
# of a pacman transaction and leave the database half-written. Done before
# anything else so the whole pass is covered, including the lock.
if [[ -z ${CAU_INHIBITED:-} ]] && command -v systemd-inhibit > /dev/null 2>&1; then
	export CAU_INHIBITED=1
	exec systemd-inhibit \
		--what=sleep:shutdown \
		--mode=block \
		--who="CachyOS Auto-Update" \
		--why="Applying system updates" \
		-- "$0" "$@"
fi

cau_config_load
cau_log_open

# Record an interruption rather than leaving the previous run's verdict behind.
# Without this, killing an interactive run leaves last_result at whatever it was
# before. Then the menu can keep reporting a problem from hours ago while the
# machine is in fact fully up to date, which is worse than saying nothing.
# pacman makes the commit phase itself uninterruptible, so the packages either
# all landed or none did; only our own bookkeeping is at risk here.
_cau_interrupted() {
	cau_error "Update run interrupted ($1)"
	# Before anything else: a progress entry whose owner merely disappears is
	# reported by the desktop as an application crash, so a killed run would
	# leave a failure message behind on top of everything else.
	cau_progress_end failed "The update was stopped before it finished."
	cau_state_write last_result interrupted
	exit 130
}
trap '_cau_interrupted SIGINT'  INT
trap '_cau_interrupted SIGTERM' TERM
trap '_cau_interrupted SIGHUP'  HUP

# ---------------------------------------------------------------------------
# Should this run happen at all?
# ---------------------------------------------------------------------------

defer() {
	cau_info "Deferred: $1"
	exit 0
}

if ! cau_acquire_lock; then
	cau_debug "Another run is already in progress"
	exit 0
fi

if (( ! FORCE )); then
	if [[ $CFG_ENABLED != yes ]]; then
		cau_debug "Automatic updates are disabled"
		exit 0
	fi
	if ! cau_update_due; then
		cau_debug "Not due yet (interval $CFG_INTERVAL)"
		exit 0
	fi
fi

cau_state_write last_run "$(date +%s)"

if (( ! FORCE )); then
	CAU_SKIP_REASON=''
	cau_power_ok || defer "$CAU_SKIP_REASON"

	if [[ $CFG_SKIP_GAMING == yes ]]; then
		CAU_SKIP_REASON=''
		if cau_busy; then
			defer "$CAU_SKIP_REASON"
		fi
	fi
fi

# A lock left behind by a power cut during a previous update. It is cleared
# before the busy check, because otherwise every future run would defer on it
# forever and the machine would quietly stop updating.
if cau_recover_stale_lock; then
	cau_notify normal no \
		"Finishing an interrupted update" \
		"The last update was cut short, most likely because the machine was switched off. It is being finished now."
fi

# This one is checked even with --force: proceeding anyway would just hand the
# user a lock error instead of doing anything useful.
CAU_SKIP_REASON=''
if cau_package_manager_busy; then
	if cau_track_stale_lock; then
		cau_warn "pacman's database lock appears to be stale"
		cau_notify normal yes \
			"Package database locked" \
			"pacman's lock file looks left over from an interrupted update. Updates are paused until it is cleared."
	fi
	defer "$CAU_SKIP_REASON"
fi
cau_state_clear stale_lock_count

# ---------------------------------------------------------------------------
# Dry run
# ---------------------------------------------------------------------------

if (( DRY_RUN )); then
	printf '%s\n' "Would update on this machine:"

	if cau_pacman_pending; then
		printf '  repository packages: %s\n' "$(grep -c . <<< "$CAU_PACMAN_PENDING")"
		sed 's/^/    /' <<< "$CAU_PACMAN_PENDING"
	else
		printf '  repository packages: 0\n'
	fi

	if [[ $CFG_AUR == yes ]] && cau_aur_ready; then
		printf '  AUR packages:        %s (%s)\n' "$(cau_aur_pending)" "$CAU_AUR_HELPER"
	else
		printf '  AUR packages:        skipped\n'
	fi

	if [[ $CFG_FLATPAK == yes ]] && cau_have flatpak; then
		printf '  system Flatpaks:     %s\n' "$(cau_flatpak_pending_system)"
	else
		printf '  system Flatpaks:     skipped\n'
	fi

	printf '  AppImages:           %s\n' \
		"$([[ $CFG_APPIMAGE == yes ]] && printf 'checked via Gear Lever' || printf 'skipped')"
	exit 0
fi

# ---------------------------------------------------------------------------
# Do the work
# ---------------------------------------------------------------------------

cau_info "Starting update run"
failed=0

# Open the desktop's progress bar, told up front which steps this run will
# perform. Only those count towards the bar, so a machine with no Flatpaks
# does not sit at 85% for the last second of the run.
progress_steps=(resolve download repo)
[[ $CFG_AUR == yes ]]      && progress_steps+=(aur)
[[ $CFG_FLATPAK == yes ]]  && progress_steps+=(flatpak)
[[ $CFG_APPIMAGE == yes ]] && progress_steps+=(appimage)
progress_steps+=(cleanup)
cau_progress_begin "${progress_steps[@]}"

# And carry the run log's last lines along with it, so "Details" shows what the
# update is doing during the stretches that have nothing to count. Most of all
# an AUR package building for a quarter of an hour.
cau_progress_tail_start "$CAU_RUNLOG"

if ! cau_pacman_update; then
	failed=1
fi

if [[ $CFG_AUR == yes ]]; then
	cau_aur_update || failed=1
fi

if [[ $CFG_FLATPAK == yes ]]; then
	cau_flatpak_update || failed=1
fi

if [[ $CFG_APPIMAGE == yes ]]; then
	cau_appimage_update || true   # best effort; never marks the run as failed
fi

cau_pacman_cleanup

# The work is over; the bar goes away and the result takes over from here. No
# label on a failure: the notification below carries that, and it stays up
# until it is dismissed.
if (( failed )); then
	cau_progress_end failed
else
	cau_progress_end ok
fi

pacnew="$(cau_pacman_pacnew_count)"
if [[ $pacnew =~ ^[0-9]+$ ]] && (( pacnew > 0 )); then
	cau_info "$pacnew .pacnew file(s) present; left untouched"
fi

# ---------------------------------------------------------------------------
# Record and report
# ---------------------------------------------------------------------------

total=$(( CAU_PACMAN_COUNT + CAU_AUR_COUNT + CAU_FLATPAK_COUNT + CAU_APPIMAGE_COUNT ))

cau_state_write last_counts \
	"$CAU_PACMAN_COUNT $CAU_AUR_COUNT $CAU_FLATPAK_COUNT $CAU_APPIMAGE_COUNT"

if (( failed )); then
	cau_state_write last_result failed
	cau_error "Update run finished with errors"
	[[ $CFG_NOTIFY_ERROR == yes ]] && cau_notify_tagged run yes critical yes \
		"Update failed" \
		"Something went wrong while updating. Run 'cachy-auto-update log' for details."
else
	cau_state_write last_result ok
	cau_state_write last_success "$(date +%s)"
	cau_info "Update run finished successfully ($total item(s) updated)"

	if (( total > 0 )) && [[ $CFG_NOTIFY_SUCCESS == yes ]]; then
		cau_notify_tagged run yes low no \
			"System updated" "%d updates were installed." "$total"
	fi
fi

# Packages skipped so the rest of the upgrade could proceed. The notification
# only fires when the set changes: a blocker waiting on an upstream fix would
# otherwise produce the same message every single day.
prev_held="$(cau_state_read held_back '')"
if [[ -n $CAU_PACMAN_HELD ]]; then
	cau_state_write held_back "$CAU_PACMAN_HELD"
	cau_warn "Held back: $CAU_PACMAN_HELD"
	if [[ $CAU_PACMAN_HELD != "$prev_held" && $CFG_NOTIFY_ERROR == yes ]]; then
		cau_notify normal yes "Some packages were held back" \
			"%s could not be updated and was skipped. Everything else is up to date." \
			"$CAU_PACMAN_HELD"
	fi
else
	cau_state_clear held_back
fi

# Recorded, deliberately not announced. The running kernel loses its module
# tree the moment pacman unpacks the new one, so this turns true partway
# through a run that still has AUR builds and Flatpaks ahead of it. A
# "restart recommended" bubble arriving then reads as an invitation to restart
# while the update is still going. `cachy-auto-update status` and the menu say
# so instead, where nobody is being interrupted mid-transaction.
if cau_pacman_reboot_needed; then
	cau_state_write reboot_needed 1
	cau_info "A kernel update needs a restart"
else
	cau_state_write reboot_needed 0
fi

# Keep the state readable for the unprivileged `status` and `log` commands.
# Only the plain files: the build account's home lives in here too and must
# keep its own ownership and mode.
chmod 0755 "$CAU_STATEDIR" 2>/dev/null || true
find "$CAU_STATEDIR" -maxdepth 1 -type f -exec chmod 0644 {} + 2>/dev/null || true

exit $(( failed ? 1 : 0 ))
