#!/bin/sh
set -eu

vm="/mnt/internal/linux"
marker="alpine-avf-install"
extras="vmlinuz initrd.img build_id vm_config.json"	# everything but root_part

die() { echo "$*" >&2; exit 1; }

[ "$(id -u)" = 0 ] || die "run as root"
[ -e /sdcard ] && die "run this INSIDE the Terminal VM, not Android"
[ -d "$vm" ] || die "$vm not found; is this the Terminal VM?"
for c in python3 truncate stat blockdev findmnt e2fsck cmp dd sync reboot; do
	command -v "$c" >/dev/null 2>&1 || die "need $c"
done

if [ -f "$marker" ]; then
	stage=2; img="$(cat "$marker")"
else
	stage=1; img="$(CDPATH='' cd -- alpine-avf && pwd)"
fi

for f in root_part $extras; do
	[ -f "$img/$f" ] || die "missing $img/$f"
done

if [ "$stage" = 1 ]; then
	echo "stage 1: staging $img"
	# Size the new partition to the larger of our image and the outgoing Debian
	# root_part, so Alpine inherits the full disk. The file is sparse, so the
	# larger apparent size costs no real space; avf-resize grows ext4 into it.
	size="$(stat -c %s "$img/root_part")"
	cur="$(stat -c %s "$vm/root_part" 2>/dev/null || echo 0)"
	[ "$cur" -gt "$size" ] && size="$cur"
	guid="$(cat /proc/sys/kernel/random/uuid)"
	# Hide the stock backup while the install is in flight, so the app cannot
	# restore it over the staged root; stage 2 puts it back after the swap.
	[ -e "$vm/root_part_backup" ] && mv -f "$vm/root_part_backup" "$vm/root_part_backup.aside"
	truncate -s "$size" "$vm/alpine_root"
	# Append a temporary ALPINE partition; crosvm exposes it as a new /dev/vdaN.
	python3 - "$vm/vm_config.json" "$guid" <<'PY'
import json, os, sys
p, guid = sys.argv[1], sys.argv[2]
c = json.load(open(p)); parts = c["disks"][0]["partitions"]
if not any(str(x.get("path", "")).endswith("/alpine_root") for x in parts):
    parts.append({"label": "ALPINE", "path": "$PAYLOAD_DIR/alpine_root", "writable": True, "guid": guid})
t = p + ".new"
with open(t, "w") as f:
    json.dump(c, f, indent=4)
    f.write("\n")
os.replace(t, p)
PY
	printf '%s\n' "$img" > "$marker"
	echo "staged; rebooting. Reopen Terminal, then run this again."
	sync; reboot
	exit 0
fi

echo "stage 2: installing $img"
target="$(python3 - "$vm/vm_config.json" <<'PY'
import json, sys
parts = json.load(open(sys.argv[1]))["disks"][0]["partitions"]
i = next((n for n, x in enumerate(parts, 1) if str(x.get("path", "")).endswith("/alpine_root")), None)
if i is None:
    sys.exit("no ALPINE partition in vm_config.json; rm ./alpine-avf-install and rerun stage 1")
print("/dev/vda%d" % i)
PY
)"
echo "target: $target"
n=0; while [ ! -b "$target" ] && [ "$n" -lt 20 ]; do sleep 1; n=$((n + 1)); done
[ -b "$target" ] || die "$target absent; reboot and rerun, or rm ./$marker and rerun stage 1"
[ "$target" != "$(findmnt -no SOURCE /)" ] || die "$target is the live root; refusing"
want="$(stat -c %s "$img/root_part")"
[ "$(blockdev --getsize64 "$target")" -ge "$want" ] || die "target smaller than source"

# dd through the block device (writing the file over virtiofs corrupts).
# Chunked with a sync per chunk to bound dirty page cache in the VM's RAM.
echo "copying root_part -> $target"
chunk=$((250 * 1024 * 1024)); n=0
while [ $((n * chunk)) -lt "$want" ]; do
	dd if="$img/root_part" of="$target" bs="$chunk" count=1 skip="$n" seek="$n" conv=notrunc iflag=fullblock status=none
	sync; n=$((n + 1))
done
# The copy of a freshly built image must check out clean; even "errors
# corrected" (rc 1/2) means the copy diverged from the verified source.
e2fsck -fy "$target" || die "e2fsck rc $? on $target: copy diverged; rerun stage 2"

# Stage kernel/initrd/config over virtiofs, verified; fail before the swap.
for f in $extras; do
	rm -f "$vm/$f.new"
	n=0
	until cmp -s "$img/$f" "$vm/$f.new"; do
		[ "$n" -lt 3 ] || die "could not write $vm/$f.new reliably; aborted before swap"
		dd if="$img/$f" of="$vm/$f.new" bs=1M conv=fsync status=none
		sync
		n=$((n + 1))
	done
done

echo "swapping root"
rm -f "$vm/root_part.previous"
mv -f "$vm/root_part" "$vm/root_part.previous"
mv -f "$vm/alpine_root" "$vm/root_part" || {
	mv -f "$vm/root_part.previous" "$vm/root_part"
	die "could not replace root_part"
}
for f in $extras; do
	mv -f "$vm/$f.new" "$vm/$f"
done
rm -f "$vm/root_part.previous"
# stock Debian's extra-modules image; the new vm_config.json does not use it
rm -f "$vm/kernel_extras_part"
[ -e "$vm/root_part_backup.aside" ] && mv -f "$vm/root_part_backup.aside" "$vm/root_part_backup"
rm -f "$marker"
echo "installed; rebooting into Alpine. Reopen the Terminal app."
# the pause lets virtiofs settle the renames host-side before the VM dies
sync; sleep 2; reboot
