#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# NodePilot - Agent-Befehl nodepilot_install_update
# -----------------------------------------------------------------------------
# Aenderungslog
# 2026-03-15 | GPT-5.2-Codex | Datei neu erstellt. Fuehrt deterministisch
# `apt install -y nodepilot` aus, liefert immer strukturiertes JSON mit
# status/stdout/stderr/exit_code sowie Metadaten und bricht mit fachlichem
# Fehler ab, falls keine Root-Rechte vorhanden sind.
# 2026-03-18 | GPT-5.2-Codex | Standard-Updatepfad auf den fachlich
# geforderten Agent-Befehl konsolidiert: DEBIAN_FRONTEND export,
# apt-get update und anschliessend gezieltes apt-get install -y nodepilot
# (kein globales Upgrade).
# 2026-03-19 | GPT-5.2-Codex | Operativen Updatepfad finalisiert:
# apt-get update + apt-get install --only-upgrade nodepilot, danach
# zeitversetzter Restart des nodepilot-agent per Hintergrundjob.
# Damit bleibt die Lauf-Rueckmeldung stabil, waehrend die neue Version
# nach dem Befehlslauf sauber aktiv wird.
# 2026-03-22 | GPT-5.2-Codex | Fachliche Upgrade-Bewertung fuer
# nodepilot_install_update gehaertet: Vorher/Nachher-Versionen,
# Kandidatenquelle, APT-Update-Fehlerklassifikation (NodePilot-Repo,
# Fremdrepo-Warnungen/-Blocker, blockierende Fehler) sowie Statuslogik
# gegen False-Positive-OKs umgesetzt. Rueckgabeobjekt um fachliche
# Diagnosefelder fuer Web-Transparenz erweitert.
# 2026-03-24 | GPT-5.3-Codex | Self-Restart-Design korrigiert: kein
# systemctl/nohup/sleep-Hintergrundrestart mehr. Das Script setzt bei
# erfolgreichem Update nur einen Restart-Marker unter /var/lib/nodepilot/state,
# prueft dpkg-Inkonsistenzen deterministisch (inkl. Hinweis auf
# "dpkg --configure -a"), haertet apt-cache-policy-Auswertung ohne dynamische
# Regex-Muster und erweitert die JSON-Diagnose um klare Restart-/DPKG-/Version-
# Signale fuer Journal/Laufdaten.
# -----------------------------------------------------------------------------

set -euo pipefail

RESTART_MARKER_DIR="/var/lib/nodepilot/state"
RESTART_MARKER_DATEI="${RESTART_MARKER_DIR}/restart_requested"

start_ms="$(date +%s%3N)"
ausfuehrung_befehl=$'export DEBIAN_FRONTEND=noninteractive\napt-get update\napt-get install -y --only-upgrade nodepilot\n# bei Erfolg: Markerdatei fuer kontrollierten systemd-Restart setzen'
apt_update_stdout_datei="$(mktemp)"
apt_update_stderr_datei="$(mktemp)"
apt_install_stdout_datei="$(mktemp)"
apt_install_stderr_datei="$(mktemp)"

diagnose_zeilen=()

json_array_aus_liste()
{
	local -n eintraege_ref="$1"
	if [ "${#eintraege_ref[@]}" -eq 0 ]; then
		echo '[]'
		return
	fi
	printf '%s\n' "${eintraege_ref[@]}" | jq -R -s -c 'split("\n") | map(select(length > 0))'
}

erste_zeile_saeubern()
{
	local text="$1"
	printf '%s\n' "${text}" | awk 'NR==1 {gsub(/\r/, "", $0); print; exit}'
}

versionsvergleich_gt()
{
	local version_links="$1"
	local version_rechts="$2"
	if [ -z "${version_links}" ] || [ "${version_links}" = "(none)" ]; then
		return 1
	fi
	if [ -z "${version_rechts}" ] || [ "${version_rechts}" = "(none)" ]; then
		return 0
	fi
	if dpkg --compare-versions "${version_links}" gt "${version_rechts}"; then
		return 0
	fi
	return 1
}

nodepilot_apt_policy_lesen()
{
	apt-cache policy nodepilot 2>/dev/null || true
}

nodepilot_quelle_aus_policy()
{
	local policy_text="$1"
	local kandidatenversion="$2"
	local installierte_version="$3"

	printf '%s\n' "${policy_text}" | awk -v kandidatenversion="${kandidatenversion}" -v installierte_version="${installierte_version}" '
	BEGIN {
		in_tabelle=0
		aktuelle_version=""
	}

	/^Version table:/ {
		in_tabelle=1
		next
	}

	in_tabelle==0 {
		next
	}

	/^[[:space:]]{2,}(\*\*\*[[:space:]]+)?[^[:space:]]+[[:space:]]+[0-9]+/ {
		zeile=$0
		gsub(/^[[:space:]]+/, "", zeile)
		n=split(zeile, teile, /[[:space:]]+/)
		if (n >= 2 && teile[1] == "***") {
			aktuelle_version=teile[2]
		} else {
			aktuelle_version=teile[1]
		}
		next
	}

	/^[[:space:]]{8,}[^[:space:]]/ {
		if (aktuelle_version == "") {
			next
		}
		zeile=$0
		gsub(/^[[:space:]]+/, "", zeile)
		if (kandidatenversion != "(none)" && aktuelle_version == kandidatenversion) {
			print zeile
			exit
		}
		if (kandidatenversion == "(none)" && installierte_version != "(none)" && aktuelle_version == installierte_version) {
			print zeile
			exit
		}
	}
	'
}

dpkg_inkonsistent_pruefen()
{
	local audit_text
	audit_text="$(dpkg --audit 2>/dev/null || true)"
	audit_text="$(printf '%s' "${audit_text}" | sed '/^[[:space:]]*$/d')"
	if [ -n "${audit_text}" ]; then
		echo "ja"
		return
	fi
	echo "nein"
}

aufraeumen()
{
	rm -f \
		"${apt_update_stdout_datei}" \
		"${apt_update_stderr_datei}" \
		"${apt_install_stdout_datei}" \
		"${apt_install_stderr_datei}"
}
trap aufraeumen EXIT

if [ "${EUID}" -ne 0 ]; then
	ende_ms="$(date +%s%3N)"
	jq -n \
		--arg status "fehler" \
		--arg stdout_text "" \
		--arg stderr_text "Root-Rechte erforderlich: apt-get update && apt-get install -y --only-upgrade nodepilot muss als root ausgefuehrt werden." \
		--argjson exit_code 126 \
		--arg command "${ausfuehrung_befehl}" \
		--argjson dauer_ms "$((ende_ms-start_ms))" \
		'{status:$status,stdout_text:$stdout_text,stderr_text:$stderr_text,exit_code:$exit_code,meta:{command:$command,dauer_ms:$dauer_ms}}'
	exit 126
fi

hostname="$(hostname 2>/dev/null || hostname -s 2>/dev/null || echo "unbekannt")"
version_vorher="$(erste_zeile_saeubern "$(dpkg-query -W -f='${Version}' nodepilot 2>/dev/null || true)")"
if [ -z "${version_vorher}" ]; then
	version_vorher="(none)"
fi

policy_vorher="$(nodepilot_apt_policy_lesen)"
installierte_version_policy="$(printf '%s\n' "${policy_vorher}" | awk -F': ' '/^[[:space:]]*Installed:/{print $2; exit}')"
installierte_version_policy="$(erste_zeile_saeubern "${installierte_version_policy}")"
if [ -z "${installierte_version_policy}" ]; then
	installierte_version_policy="(none)"
fi

kandidatenversion_vorher="$(printf '%s\n' "${policy_vorher}" | awk -F': ' '/^[[:space:]]*Candidate:/{print $2; exit}')"
kandidatenversion_vorher="$(erste_zeile_saeubern "${kandidatenversion_vorher}")"
if [ -z "${kandidatenversion_vorher}" ]; then
	kandidatenversion_vorher="(none)"
fi

nodepilot_quelle="$(nodepilot_quelle_aus_policy "${policy_vorher}" "${kandidatenversion_vorher}" "${installierte_version_policy}")"
nodepilot_quelle="$(erste_zeile_saeubern "${nodepilot_quelle}")"
if [ -z "${nodepilot_quelle}" ]; then
	nodepilot_quelle="unbekannt"
fi

nodepilot_quelle_host="$(printf '%s\n' "${nodepilot_quelle}" | sed -E 's#^[A-Za-z0-9.+-]+://##' | awk 'NR==1 {print $1}' | cut -d'/' -f1)"
if [ -z "${nodepilot_quelle_host}" ] || [ "${nodepilot_quelle_host}" = "unbekannt" ]; then
	nodepilot_quelle_host="nodepilot"
fi

dpkg_inkonsistent="$(dpkg_inkonsistent_pruefen)"
diagnose_zeilen+=("Update-Start: host=${hostname}")
diagnose_zeilen+=("Version vorher=${version_vorher}, Kandidat=${kandidatenversion_vorher}, Policy-Installed=${installierte_version_policy}")
diagnose_zeilen+=("Policy-Quelle=${nodepilot_quelle}")
diagnose_zeilen+=("dpkg inkonsistent (vor apt)=${dpkg_inkonsistent}")

declare -a fremdrepo_warnungen=()
declare -a fremdrepo_blocker=()
declare -a blockierende_fehler=()
declare -a nodepilot_repo_fehler=()
nodepilot_repo_ok="ja"
apt_update_exit_code=0
apt_install_exit_code=0

if [ "${dpkg_inkonsistent}" = "nein" ]; then
	set +e
	{
		export DEBIAN_FRONTEND=noninteractive
		apt-get update
	} >"${apt_update_stdout_datei}" 2>"${apt_update_stderr_datei}"
	apt_update_exit_code="$?"
	set -e
else
	printf '%s\n' "dpkg-Zustand ist inkonsistent; apt-get update/install wird nicht gestartet." > "${apt_update_stderr_datei}"
	apt_update_exit_code=100
fi

apt_update_stdout="$(cat "${apt_update_stdout_datei}" 2>/dev/null || true)"
apt_update_stderr="$(cat "${apt_update_stderr_datei}" 2>/dev/null || true)"

line_ist_nodepilot_bezug()
{
	local zeile="$1"
	local zeile_klein host_klein
	zeile_klein="$(printf '%s' "${zeile}" | tr '[:upper:]' '[:lower:]')"
	host_klein="$(printf '%s' "${nodepilot_quelle_host}" | tr '[:upper:]' '[:lower:]')"
	if printf '%s' "${zeile_klein}" | grep -Fq "nodepilot"; then
		return 0
	fi
	if [ -n "${host_klein}" ] && [ "${host_klein}" != "nodepilot" ] && printf '%s' "${zeile_klein}" | grep -Fq "${host_klein}"; then
		return 0
	fi
	return 1
}

apt_zeilen="$(printf '%s\n%s\n' "${apt_update_stdout}" "${apt_update_stderr}")"
while IFS= read -r zeile; do
	if [ -z "${zeile}" ]; then
		continue
	fi
	quelle_ist_nodepilot="nein"
	if line_ist_nodepilot_bezug "${zeile}"; then
		quelle_ist_nodepilot="ja"
	fi

	if printf '%s' "${zeile}" | grep -Eq '^W:'; then
		if [ "${quelle_ist_nodepilot}" = "nein" ]; then
			fremdrepo_warnungen+=("${zeile}")
		fi
	fi

	if printf '%s' "${zeile}" | grep -Eq '^(E:|Err:|W: GPG error:|W: Failed to fetch|W: The repository .* does not have a Release file)'; then
		if [ "${quelle_ist_nodepilot}" = "ja" ]; then
			nodepilot_repo_fehler+=("${zeile}")
		else
			fremdrepo_blocker+=("${zeile}")
		fi
	fi
done <<< "${apt_zeilen}"

if [ "${apt_update_exit_code}" -ne 0 ]; then
	if [ "${#nodepilot_repo_fehler[@]}" -eq 0 ] && { [ "${kandidatenversion_vorher}" = "(none)" ] || [ "${nodepilot_quelle}" = "unbekannt" ]; }; then
		nodepilot_repo_fehler+=("apt-get update fehlgeschlagen (${apt_update_exit_code}) und NodePilot-Quelle/Kandidat ist nicht sauber lesbar.")
	fi
	blockierende_fehler+=("apt-get update fehlgeschlagen (Exit-Code ${apt_update_exit_code}).")
fi

if [ "${#nodepilot_repo_fehler[@]}" -gt 0 ]; then
	nodepilot_repo_ok="nein"
	blockierende_fehler+=("${nodepilot_repo_fehler[@]}")
fi

if [ "${dpkg_inkonsistent}" = "nein" ] && [ "${apt_update_exit_code}" -eq 0 ]; then
	set +e
	{
		export DEBIAN_FRONTEND=noninteractive
		apt-get install -y --only-upgrade nodepilot
	} >"${apt_install_stdout_datei}" 2>"${apt_install_stderr_datei}"
	apt_install_exit_code="$?"
	set -e
else
	printf '%s\n' "apt-get install wurde wegen Vorfehlern nicht gestartet." > "${apt_install_stderr_datei}"
fi

apt_install_stdout="$(cat "${apt_install_stdout_datei}" 2>/dev/null || true)"
apt_install_stderr="$(cat "${apt_install_stderr_datei}" 2>/dev/null || true)"
version_nachher="$(erste_zeile_saeubern "$(dpkg-query -W -f='${Version}' nodepilot 2>/dev/null || true)")"
if [ -z "${version_nachher}" ]; then
	version_nachher="(none)"
fi

upgrade_verfuegbar="nein"
if versionsvergleich_gt "${kandidatenversion_vorher}" "${version_vorher}"; then
	upgrade_verfuegbar="ja"
fi

upgrade_durchgefuehrt="nein"
if [ "${version_nachher}" != "${version_vorher}" ] && versionsvergleich_gt "${version_nachher}" "${version_vorher}"; then
	upgrade_durchgefuehrt="ja"
fi

zielversion_erreicht="nein"
if [ "${kandidatenversion_vorher}" != "(none)" ] && [ "${version_nachher}" = "${kandidatenversion_vorher}" ]; then
	zielversion_erreicht="ja"
fi

status="ok"
exit_code=0
markerdatei_geschrieben="nein"
agent_neustart_angefordert="nein"

if [ "${dpkg_inkonsistent}" = "ja" ]; then
	status="fehler"
	exit_code=100
	blockierende_fehler+=("dpkg meldet einen inkonsistenten Zustand. Bitte zuerst 'dpkg --configure -a' ausfuehren.")
fi

if [ "${nodepilot_repo_ok}" != "ja" ]; then
	status="fehler"
	if [ "${exit_code}" -eq 0 ]; then
		exit_code=3
	fi
	blockierende_fehler+=("NodePilot-APT-Repository ist nicht sauber lesbar; fachlich kein valider Upgrade-Check moeglich.")
fi

if [ "${apt_install_exit_code}" -ne 0 ]; then
	status="fehler"
	exit_code="${apt_install_exit_code}"
	blockierende_fehler+=("apt-get install --only-upgrade nodepilot fehlgeschlagen (Exit-Code ${apt_install_exit_code}).")
fi

if printf '%s\n%s\n' "${apt_install_stdout}" "${apt_install_stderr}" | grep -Fq "dpkg was interrupted"; then
	status="fehler"
	if [ "${exit_code}" -eq 0 ]; then
		exit_code=100
	fi
	blockierende_fehler+=("apt/dpkg meldet Unterbrechung: bitte 'dpkg --configure -a' ausfuehren.")
fi

if [ "${upgrade_verfuegbar}" = "ja" ] && [ "${zielversion_erreicht}" != "ja" ]; then
	status="fehler"
	if [ "${exit_code}" -eq 0 ]; then
		exit_code=2
	fi
	blockierende_fehler+=("Upgrade war verfuegbar (${version_vorher} -> ${kandidatenversion_vorher}), aber Zielversion wurde nicht erreicht (Ist nach Lauf: ${version_nachher}).")
fi

if [ "${status}" = "ok" ] && { [ "${#fremdrepo_warnungen[@]}" -gt 0 ] || [ "${#fremdrepo_blocker[@]}" -gt 0 ]; }; then
	status="warnung"
fi

if [ "${status}" != "fehler" ] && [ "${upgrade_durchgefuehrt}" = "ja" ]; then
	install -d -m 0750 "${RESTART_MARKER_DIR}"
	printf 'grund=nodepilot_install_update\nzeitstempel=%s\nversion_vorher=%s\nversion_nachher=%s\n' "$(date -Iseconds)" "${version_vorher}" "${version_nachher}" > "${RESTART_MARKER_DATEI}"
	chmod 0640 "${RESTART_MARKER_DATEI}" 2>/dev/null || true
	markerdatei_geschrieben="ja"
	agent_neustart_angefordert="ja"
fi

if [ "${status}" = "fehler" ]; then
	agent_neustart_angefordert="nein"
fi

diagnose_zeilen+=("Version nachher=${version_nachher}")
diagnose_zeilen+=("Markerdatei geschrieben=${markerdatei_geschrieben}")
diagnose_zeilen+=("Agent-Neustart angefordert=${agent_neustart_angefordert}")

stdout_text="$(printf '%s\n%s\n\n%s\n' "${apt_update_stdout}" "${apt_install_stdout}" "$(printf '%s\n' "${diagnose_zeilen[@]}")")"
stderr_text="${apt_update_stderr}
${apt_install_stderr}"

if [ "${#blockierende_fehler[@]}" -gt 0 ]; then
	stderr_text="${stderr_text}
$(printf '%s\n' "${blockierende_fehler[@]}")"
fi

ende_ms="$(date +%s%3N)"
fremdrepo_warnungen_json="$(json_array_aus_liste fremdrepo_warnungen)"
fremdrepo_blocker_json="$(json_array_aus_liste fremdrepo_blocker)"
blockierende_fehler_json="$(json_array_aus_liste blockierende_fehler)"
diagnose_zeilen_json="$(json_array_aus_liste diagnose_zeilen)"

jq -n \
	--arg status "${status}" \
	--arg stdout_text "${stdout_text}" \
	--arg stderr_text "${stderr_text}" \
	--argjson exit_code "${exit_code}" \
	--arg command "${ausfuehrung_befehl}" \
	--arg hostname "${hostname}" \
	--arg version_vorher "${version_vorher}" \
	--arg installierte_version_policy "${installierte_version_policy}" \
	--arg kandidatenversion_vorher "${kandidatenversion_vorher}" \
	--arg version_nachher "${version_nachher}" \
	--arg upgrade_verfuegbar "${upgrade_verfuegbar}" \
	--arg upgrade_durchgefuehrt "${upgrade_durchgefuehrt}" \
	--arg zielversion_erreicht "${zielversion_erreicht}" \
	--arg nodepilot_repo_ok "${nodepilot_repo_ok}" \
	--arg nodepilot_quelle "${nodepilot_quelle}" \
	--arg dpkg_inkonsistent "${dpkg_inkonsistent}" \
	--arg markerdatei_geschrieben "${markerdatei_geschrieben}" \
	--arg agent_neustart_angefordert "${agent_neustart_angefordert}" \
	--arg restart_marker_datei "${RESTART_MARKER_DATEI}" \
	--arg apt_update_stdout "${apt_update_stdout}" \
	--arg apt_update_stderr "${apt_update_stderr}" \
	--arg apt_install_stdout "${apt_install_stdout}" \
	--arg apt_install_stderr "${apt_install_stderr}" \
	--argjson fremdrepo_warnungen "${fremdrepo_warnungen_json}" \
	--argjson fremdrepo_blocker "${fremdrepo_blocker_json}" \
	--argjson blockierende_fehler "${blockierende_fehler_json}" \
	--argjson diagnose_zeilen "${diagnose_zeilen_json}" \
	--argjson apt_update_exit_code "${apt_update_exit_code}" \
	--argjson apt_install_exit_code "${apt_install_exit_code}" \
	--argjson dauer_ms "$((ende_ms-start_ms))" \
	'{
		status:$status,
		stdout_text:$stdout_text,
		stderr_text:$stderr_text,
		exit_code:$exit_code,
		hostname:$hostname,
		version_vorher:$version_vorher,
		installierte_version_policy:$installierte_version_policy,
		kandidatenversion_vorher:$kandidatenversion_vorher,
		version_nachher:$version_nachher,
		upgrade_verfuegbar:($upgrade_verfuegbar=="ja"),
		upgrade_durchgefuehrt:($upgrade_durchgefuehrt=="ja"),
		zielversion_erreicht:($zielversion_erreicht=="ja"),
		nodepilot_repo_ok:($nodepilot_repo_ok=="ja"),
		nodepilot_quelle:$nodepilot_quelle,
		dpkg_inkonsistent:($dpkg_inkonsistent=="ja"),
		markerdatei_geschrieben:($markerdatei_geschrieben=="ja"),
		agent_neustart_angefordert:($agent_neustart_angefordert=="ja"),
		restart_marker_datei:$restart_marker_datei,
		diagnose_zeilen:$diagnose_zeilen,
		fremdrepo_warnungen:$fremdrepo_warnungen,
		fremdrepo_blocker:$fremdrepo_blocker,
		blockierende_fehler:$blockierende_fehler,
		apt_update_exit_code:$apt_update_exit_code,
		apt_install_exit_code:$apt_install_exit_code,
		apt_update_stdout:$apt_update_stdout,
		apt_update_stderr:$apt_update_stderr,
		apt_install_stdout:$apt_install_stdout,
		apt_install_stderr:$apt_install_stderr,
		meta:{command:$command,dauer_ms:$dauer_ms}
	}'

exit "${exit_code}"
