#!/bin/sh

# ota-upgrade 1.0.0
# Copyright (c)2026 John Lawson & Sons
# All Rights Reserved

set -eu

OS_RELEASE="/etc/os-release"
BOOT_MARKER="isignage-boot.marker"
OTA_CHANNEL="${OTA_CHANNEL:-latest}"

[ -f "$OS_RELEASE" ] || {
	echo "[OTA] ERROR: $OS_RELEASE missing"
	exit 1
}

CURRENT_VERSION="$(awk -F= '$1=="VERSION"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE")"
MACHINE="$(awk -F= '$1=="MACHINE"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE")"

[ -n "$CURRENT_VERSION" ] || {
	echo "[OTA] ERROR: VERSION missing from $OS_RELEASE"
	exit 1
}

[ -n "$MACHINE" ] || {
	echo "[OTA] ERROR: MACHINE missing from $OS_RELEASE"
	exit 1
}

DO_REBOOT=""
LOCAL_DIR=""

while [ "$#" -gt 0 ]; do
	case "$1" in
		--reboot)
			DO_REBOOT="--reboot"
			;;
		--local-dir)
			shift
			[ "$#" -gt 0 ] || {
				echo "Usage: $0 [--local-dir DIR] [--reboot]"
				exit 1
			}
			LOCAL_DIR="$1"
			;;
		*)
			echo "Usage: $0 [--local-dir DIR] [--reboot]"
			exit 1
			;;
	esac
	shift
done

[ "$(id -u)" = "0" ] || {
	echo "[OTA] ERROR: must run as root"
	exit 1
}

command -v curl >/dev/null || { echo "[OTA] ERROR: curl missing"; exit 1; }
command -v findmnt >/dev/null || { echo "[OTA] ERROR: findmnt missing"; exit 1; }
command -v mkfs.btrfs >/dev/null || { echo "[OTA] ERROR: mkfs.btrfs missing"; exit 1; }
command -v mountpoint >/dev/null || { echo "[OTA] ERROR: mountpoint missing"; exit 1; }
command -v sha256sum >/dev/null || { echo "[OTA] ERROR: sha256sum missing"; exit 1; }
command -v tar >/dev/null || { echo "[OTA] ERROR: tar missing"; exit 1; }

BASE_URL="https://upgrades.isignage.app/${MACHINE}/${OTA_CHANNEL}"

partition_path() {
	disk="$1"
	index="$2"

	case "$disk" in
		/dev/mmcblk*|/dev/nvme*)
			echo "${disk}p${index}"
			;;
		*)
			echo "${disk}${index}"
			;;
	esac
}

preferred_boot_disks() {
	seen=""

	add_disk() {
		disk="$1"

		[ -n "$disk" ] || return 0
		[ -b "$disk" ] || return 0

		case " $seen " in
			*" $disk "*)
				return 0
				;;
		esac

		printf '%s\n' "$disk"
		seen="$seen $disk"
	}

	for disk in /dev/nvme0n1 /dev/mmcblk1 /dev/mmcblk0 /dev/mmcblk2; do
		add_disk "$disk"
	done

	for sysdisk in /sys/class/block/*; do
		name="${sysdisk##*/}"

		[ -e "$sysdisk" ] || continue
		[ ! -e "$sysdisk/partition" ] || continue

		case "$name" in
			loop*|ram*|zram*|dm-*|md*|sr*|mtdblock*|nbd*)
				continue
				;;
		esac

		add_disk "/dev/$name"
	done
}

partition_disk() {
	part="$1"

	if command -v readlink >/dev/null 2>&1; then
		part="$(readlink -f "$part" 2>/dev/null || echo "$part")"
	fi

	case "$part" in
		/dev/mmcblk*p[0-9]*|/dev/nvme*n*p[0-9]*)
			echo "${part%p*}"
			;;
		/dev/*[0-9])
			echo "${part%[0-9]}"
			;;
		*)
			return 1
			;;
	esac
}

partition_mount_source() {
	part="$1"
	echo "$part"
}

disk_removable() {
	disk="$1"
	name="${disk##*/}"
	value="$(cat "/sys/class/block/$name/removable" 2>/dev/null || true)"

	case "$value" in
		0)
			return 1
			;;
		*)
			return 0
			;;
	esac
}

find_boot_config_relpath() {
	for rel in cmdline.txt extlinux/extlinux.conf; do
		if [ -f "/boot/$rel" ]; then
			echo "$rel"
			return 0
		fi
	done

	return 1
}

boot_disk_score() {
	disk="$1"
	score=0
	part1="$(partition_path "$disk" 1)"
	part2="$(partition_path "$disk" 2)"
	part3="$(partition_path "$disk" 3)"
	part4="$(partition_path "$disk" 4)"

	[ -b "$part1" ] || {
		echo 0
		return 0
	}

	mkdir -p /tmp/ota-boot-test

	if ! mount -t vfat "$part1" /tmp/ota-boot-test 2>/dev/null; then
		echo 0
		return 0
	fi

	if [ ! -f "/tmp/ota-boot-test/$BOOT_MARKER" ]; then
		umount /tmp/ota-boot-test 2>/dev/null || true
		echo 0
		return 0
	fi

	if ! disk_removable "$disk"; then
		score=$((score + 1000))
	fi

	if [ -f /tmp/ota-boot-test/cmdline.txt ] || [ -f /tmp/ota-boot-test/extlinux/extlinux.conf ]; then
		score=$((score + 50))
	fi

	if [ -f /tmp/ota-boot-test/config.txt ]; then
		score=$((score + 10))
	fi

	if [ -f /tmp/ota-boot-test/fitImage ] || [ -f /tmp/ota-boot-test/uImage ] || [ -f /tmp/ota-boot-test/Image ]; then
		score=$((score + 25))
	fi

	if [ -f /tmp/ota-boot-test/initramfs-rollback.cpio ] || [ -f /tmp/ota-boot-test/initramfs-rollback.cpio.gz ]; then
		score=$((score + 20))
	fi

	if [ -d /tmp/ota-boot-test/ota ]; then
		score=$((score + 10))
	fi

	if [ -d /tmp/ota-boot-test/recovery ]; then
		score=$((score + 10))
	fi

	umount /tmp/ota-boot-test 2>/dev/null || true

	[ -b "$part2" ] && score=$((score + 15))
	[ -b "$part3" ] && score=$((score + 15))
	[ -b "$part4" ] && score=$((score + 10))

	echo "$score"
}

find_boot_disk() {
	boot_source="$(findmnt -no SOURCE /boot 2>/dev/null || true)"
	if [ -n "$boot_source" ]; then
		disk="$(partition_disk "$boot_source" 2>/dev/null || true)"
		if [ -n "$disk" ] && [ -b "$(partition_path "$disk" 1)" ]; then
			echo "$disk"
			return 0
		fi
	fi

	root_source="$(findmnt -no SOURCE / 2>/dev/null || true)"
	case "$root_source" in
		/dev/root)
			;;
		*)
			if [ -n "$root_source" ]; then
				disk="$(partition_disk "$root_source" 2>/dev/null || true)"
				if [ -n "$disk" ] && [ -b "$(partition_path "$disk" 1)" ]; then
					echo "$disk"
					return 0
				fi
			fi
			;;
	esac

	best_disk=""
	best_score=0

	for d in $(preferred_boot_disks); do
		score="$(boot_disk_score "$d")"

		case "$score" in
			''|*[!0-9]*)
				score=0
				;;
		esac

		if [ "$score" -gt "$best_score" ]; then
			best_disk="$d"
			best_score="$score"
		fi
	done

	if [ -n "$best_disk" ]; then
		echo "$best_disk"
		return 0
	fi

	return 1
}

DISK="$(find_boot_disk || true)"

[ -n "$DISK" ] || {
	echo "[OTA] ERROR: could not detect boot disk"
	exit 1
}

BOOT_PART="$(partition_path "$DISK" 1)"
ROOT_A_PART="$(partition_path "$DISK" 2)"
ROOT_B_PART="$(partition_path "$DISK" 3)"
HOME_PART="$(partition_path "$DISK" 4)"

WORKDIR="/home/update"
TARGET_MNT="/mnt/ota-target"
OTA_BOOT_DIR="/boot/ota"
RECOVERY_DIR="/boot/recovery"

current_root_from_metadata() {
	for file in "$OTA_BOOT_DIR/current-root" "$OTA_BOOT_DIR/last-booted-root"; do
		if [ -f "$file" ]; then
			root="$(cat "$file" 2>/dev/null || true)"
			case "$root" in
				"$ROOT_A_PART"|"$ROOT_B_PART")
					echo "$root"
					return 0
					;;
			esac
		fi
	done

	root="$(awk -F= '$1=="INSTALLED_ROOT"{gsub(/"/,"",$2); print $2}' "$OS_RELEASE" 2>/dev/null || true)"
	case "$root" in
		"$ROOT_A_PART"|"$ROOT_B_PART")
			echo "$root"
			return 0
			;;
	esac

	return 1
}

stage_recovery_image() {
	echo "[OTA] Staging recovery rootfs..."

	mkdir -p "$RECOVERY_DIR"

	rm -f "$RECOVERY_DIR/rootfs.tar.xz.new"
	rm -f "$RECOVERY_DIR/rootfs.tar.xz.sha256.new"
	rm -f "$RECOVERY_DIR/version.new"

	cp "$WORKDIR/rootfs.tar.xz" "$RECOVERY_DIR/rootfs.tar.xz.new"

	cd "$RECOVERY_DIR"

	sha256sum rootfs.tar.xz.new > rootfs.tar.xz.sha256.new
	sha256sum -c rootfs.tar.xz.sha256.new

	cat > version.new <<EOF
VERSION=$BUNDLE_VERSION
MACHINE=$BUNDLE_MACHINE
STAGED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
ROOT=$NEW_ROOT
EOF

	cd "$WORKDIR"

	sync

	echo "[OTA] Recovery rootfs staged"
}

cleanup() {
	if mountpoint -q "$TARGET_MNT"; then
		echo "[OTA] Cleanup: unmounting $TARGET_MNT"
		umount "$TARGET_MNT" || true
	fi
}

trap cleanup EXIT

echo "[OTA] Machine: $MACHINE"
echo "[OTA] Current version: $CURRENT_VERSION"
echo "[OTA] Update channel: $OTA_CHANNEL"
echo "[OTA] Update URL: $BASE_URL"

mkdir -p "$WORKDIR" "$TARGET_MNT"

echo "[OTA] Ensuring /boot is mounted..."
mkdir -p /boot

if ! mountpoint -q /boot; then
	mount "$BOOT_PART" /boot
fi

mkdir -p "$OTA_BOOT_DIR"
mkdir -p "$RECOVERY_DIR"

cd "$WORKDIR"

echo "[OTA] Cleaning old downloads..."
rm -f rootfs.tar.xz boot.tar.xz SHA256SUMS os-release

if [ -n "$LOCAL_DIR" ]; then
	echo "[OTA] Loading local update bundle from $LOCAL_DIR"

	[ -d "$LOCAL_DIR" ] || {
		echo "[OTA] ERROR: local bundle directory missing: $LOCAL_DIR"
		exit 1
	}

	for f in rootfs.tar.xz boot.tar.xz SHA256SUMS os-release; do
		[ -f "$LOCAL_DIR/$f" ] || {
			echo "[OTA] ERROR: missing local bundle file: $LOCAL_DIR/$f"
			exit 1
		}
		cp -f "$LOCAL_DIR/$f" "./$f"
	done
else
	echo "[OTA] Downloading update bundle..."
	curl -fL -o rootfs.tar.xz "$BASE_URL/rootfs.tar.xz"
	curl -fL -o boot.tar.xz "$BASE_URL/boot.tar.xz"
	curl -fL -o SHA256SUMS "$BASE_URL/SHA256SUMS"
	curl -fL -o os-release "$BASE_URL/os-release"
fi

BUNDLE_VERSION="$(awk -F= '$1=="VERSION"{gsub(/"/,"",$2); print $2}' os-release)"
BUNDLE_MACHINE="$(awk -F= '$1=="MACHINE"{gsub(/"/,"",$2); print $2}' os-release)"

[ -n "$BUNDLE_VERSION" ] || {
	echo "[OTA] ERROR: VERSION missing from downloaded os-release"
	exit 1
}

[ -n "$BUNDLE_MACHINE" ] || {
	echo "[OTA] ERROR: MACHINE missing from downloaded os-release"
	exit 1
}

[ "$BUNDLE_MACHINE" = "$MACHINE" ] || {
	echo "[OTA] ERROR: bundle machine mismatch: expected $MACHINE got $BUNDLE_MACHINE"
	exit 1
}

echo "[OTA] Bundle version: $BUNDLE_VERSION"

echo "[OTA] Verifying checksums..."
sha256sum -c SHA256SUMS

ACTIVE_SOURCE="$(findmnt -no SOURCE / || true)"
if [ -n "$ACTIVE_SOURCE" ] && [ "$ACTIVE_SOURCE" != "/dev/root" ] && command -v readlink >/dev/null 2>&1; then
	ACTIVE_SOURCE="$(readlink -f "$ACTIVE_SOURCE" 2>/dev/null || echo "$ACTIVE_SOURCE")"
fi

case "$ACTIVE_SOURCE" in
	"$ROOT_A_PART")
		ACTIVE_PART="$ACTIVE_SOURCE"
		TARGET_LABEL="rootB"
		TARGET_PART="$ROOT_B_PART"
		PREV_ROOT="$ROOT_A_PART"
		NEW_ROOT="$ROOT_B_PART"
		;;
	"$ROOT_B_PART")
		ACTIVE_PART="$ROOT_B_PART"
		TARGET_LABEL="rootA"
		TARGET_PART="$ROOT_A_PART"
		PREV_ROOT="$ROOT_B_PART"
		NEW_ROOT="$ROOT_A_PART"
		;;
	"/dev/root")
		ACTIVE_PART="$(current_root_from_metadata || true)"

		case "$ACTIVE_PART" in
			"$ROOT_B_PART")
				TARGET_LABEL="rootA"
				TARGET_PART="$ROOT_A_PART"
				PREV_ROOT="$ROOT_B_PART"
				NEW_ROOT="$ROOT_A_PART"
				;;
			"$ROOT_A_PART"|"")
				ACTIVE_PART="$ROOT_A_PART"
				TARGET_LABEL="rootB"
				TARGET_PART="$ROOT_B_PART"
				PREV_ROOT="$ROOT_A_PART"
				NEW_ROOT="$ROOT_B_PART"
				;;
			*)
				echo "[OTA] ERROR: active root metadata is not recognised"
				echo "[OTA] Active metadata: $ACTIVE_PART"
				exit 1
				;;
		esac
		;;
	*)
		echo "[OTA] ERROR: active root is not recognised"
		echo "[OTA] Active source: $ACTIVE_SOURCE"
		exit 1
		;;
esac

echo "[OTA] Active root: $ACTIVE_PART"
echo "[OTA] Target partition: $TARGET_PART"
echo "[OTA] Previous root: $PREV_ROOT"
echo "[OTA] New root: $NEW_ROOT"

if findmnt -rn "$TARGET_PART" >/dev/null 2>&1; then
	echo "[OTA] ERROR: $TARGET_PART is currently mounted"
	findmnt "$TARGET_PART" || true
	exit 1
fi

BOOT_CONFIG_REL="$(find_boot_config_relpath || true)"

[ -n "$BOOT_CONFIG_REL" ] || {
	echo "[OTA] ERROR: could not detect boot config in /boot"
	exit 1
}

echo "[OTA] Preserving boot config: /boot/$BOOT_CONFIG_REL"
mkdir -p "$WORKDIR/$(dirname "$BOOT_CONFIG_REL")"
cp -a "/boot/$BOOT_CONFIG_REL" "$WORKDIR/$BOOT_CONFIG_REL.before"

echo "[OTA] Formatting inactive rootfs partition..."
mkfs.btrfs -f -L "$TARGET_LABEL" "$TARGET_PART"

echo "[OTA] Mounting target rootfs..."
mount "$TARGET_PART" "$TARGET_MNT"

echo "[OTA] Extracting new rootfs..."
tar --numeric-owner -xpf rootfs.tar.xz -C "$TARGET_MNT"

echo "[OTA] Writing target fstab..."
ROOT_MOUNT_SOURCE="$(partition_mount_source "$NEW_ROOT")"
BOOT_MOUNT_SOURCE="$(partition_mount_source "$BOOT_PART")"
HOME_MOUNT_SOURCE="$(partition_mount_source "$HOME_PART")"
cat > "$TARGET_MNT/etc/fstab" <<EOF
$ROOT_MOUNT_SOURCE /          btrfs  defaults                           1 1
proc           /proc         proc   defaults                           0 0
devpts         /dev/pts      devpts mode=0620,ptmxmode=0666,gid=5      0 0
tmpfs          /run          tmpfs  mode=0755,nodev,nosuid,strictatime 0 0
tmpfs          /var/volatile tmpfs  defaults                           0 0
$BOOT_MOUNT_SOURCE /boot     vfat   defaults                           0 0
$HOME_MOUNT_SOURCE /home     btrfs  defaults                           0 0
EOF

echo "[OTA] Installing version metadata..."
mkdir -p "$TARGET_MNT/etc"

cat os-release > "$TARGET_MNT/etc/os-release"

cat >> "$TARGET_MNT/etc/os-release" <<EOF
INSTALLED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
INSTALLED_ROOT=$NEW_ROOT
PREVIOUS_ROOT=$PREV_ROOT
EOF

echo "[OTA] Installing safe /boot files..."
tar --no-same-owner \
	--exclude='./cmdline.txt' \
	--exclude='cmdline.txt' \
	--exclude='./start*.elf' \
	--exclude='start*.elf' \
	--exclude='./fixup*.dat' \
	--exclude='fixup*.dat' \
	--exclude='./bootcode.bin' \
	--exclude='bootcode.bin' \
	-xpf boot.tar.xz -C /boot

if [ "$BOOT_CONFIG_REL" = "cmdline.txt" ]; then
	echo "[OTA] Restoring known-good boot config..."
	mkdir -p "/boot/$(dirname "$BOOT_CONFIG_REL")"
	cp -a "$WORKDIR/$BOOT_CONFIG_REL.before" "/boot/$BOOT_CONFIG_REL"
else
	echo "[OTA] Using boot config from OTA payload..."
fi

echo "[OTA] Writing boot status flags..."
echo "$NEW_ROOT" > "$OTA_BOOT_DIR/pending-root"
echo "$PREV_ROOT" > "$OTA_BOOT_DIR/previous-root"
echo "$TARGET_LABEL" > "$OTA_BOOT_DIR/pending-label"
echo "$TARGET_PART" > "$OTA_BOOT_DIR/pending-partition"
echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$OTA_BOOT_DIR/staged-at"

touch "$OTA_BOOT_DIR/rollback.flag"

stage_recovery_image

sync

echo
echo "[OTA] Final boot config (/boot/$BOOT_CONFIG_REL):"
cat "/boot/$BOOT_CONFIG_REL"

echo
echo "[OTA] /boot/ota:"
ls -la "$OTA_BOOT_DIR"

echo
echo "[OTA] /boot/recovery:"
ls -la "$RECOVERY_DIR"

echo
echo "[OTA] Target /etc/fstab:"
cat "$TARGET_MNT/etc/fstab"

echo
echo "[OTA] Upgrade staged successfully."

if [ "$DO_REBOOT" = "--reboot" ]; then
	echo "[OTA] Rebooting..."
	reboot
else
	echo "[OTA] Not rebooting automatically."
	echo "[OTA] Reboot manually when ready."
fi
