#!/bin/sh
# pkgos-update-release: update every package of an OpenStack release that is
# LOWER than its upstream version on the osbpo deb-status page. The page is
# fetched once; for each package needing an update, the script asks for a
# y/n/q keypress, locates the checkout through /etc/pkgos/all-git, then runs
# the whole update flow: fetch upstream tags, merge, changelog bump, orig
# tarball, gbp buildpackage, signed tag + push, debsign + dput.
# Usage: pkgos-update-release <release> [dist]
#        pkgos-update-release hibiscus          -> trixie-hibiscus.html
# Any failure stops the batch; already-updated packages are skipped, so the
# batch can simply be re-run after fixing an issue.

set -e

RELEASE=${1:-}
DIST=${2:-trixie}
ALL_GIT=${PKGOS_ALL_GIT:-/etc/pkgos/all-git}
SALSA_ROOT=${PKGOS_SALSA_ROOT:-/home/zigo/salsa/openstack}
STATUS_URL=${OSBPO_STATUS_URL:-https://osbpo.debian.net/deb-status/${DIST}-${RELEASE}.html}
DEBSIGN_KEY='A0B1A9F3508956130E7A425CD416AD15AC6B43FE!'

SAVED_STTY=
WORKDIR=
COUNTER=0
N_TOTAL=0
OK_COUNT=0
DONE_COUNT=0
SKIP_DONE_COUNT=0
SKIP_USER_COUNT=0
WARN_COUNT=0

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

usage () {
	echo "Usage: pkgos-update-release <release> [dist]" >&2
	echo "       e.g.: pkgos-update-release hibiscus" >&2
	exit 1
}

cleanup () {
	if [ -n "$SAVED_STTY" ]; then
		stty "$SAVED_STTY" 2>/dev/null || true
		SAVED_STTY=
	fi
	if [ -n "$WORKDIR" ]; then
		rm -rf "$WORKDIR"
	fi
}

# Read a single keypress without requiring Enter. Restores the terminal
# settings before returning; the cleanup() trap covers abnormal exits.
askkey () {
	SAVED_STTY=$(stty -g 2>/dev/null) || return 1
	stty raw -echo min 1 time 0
	KEY=$(dd bs=1 count=1 2>/dev/null) || true
	stty "$SAVED_STTY" 2>/dev/null || true
	SAVED_STTY=
	printf '%s' "$KEY"
}

print_summary () {
	echo
	echo "=== Summary for $RELEASE (page: $DIST-$RELEASE) ==="
	echo "   up to date:       $OK_COUNT"
	echo "   processed:        $DONE_COUNT"
	echo "   skipped by you:   $SKIP_USER_COUNT"
	echo "   already done:     $SKIP_DONE_COUNT"
	echo "   warnings/skipped: $WARN_COUNT"
}

[ -n "$RELEASE" ] || usage
[ -f "$ALL_GIT" ] || die "$ALL_GIT not found."
[ -d "$SALSA_ROOT" ] || die "$SALSA_ROOT not found."

echo "==> Fetching $STATUS_URL"
WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/pkgos-update-release.XXXXXX")
trap cleanup EXIT
curl -fsSL "$STATUS_URL" >"$WORKDIR/page.html" || die "could not fetch $STATUS_URL"

# Extract every table row: "<pkg> <debver-page> <upstream> <status>".
awk '
	/<tr class=/ { n = 0 }
	/<td>/ {
		line = $0
		gsub(/<[^>]*>/, "", line)
		sub(/^[ \t]+/, "", line)
		sub(/[ \t]+$/, "", line)
		v[++n] = line
	}
	/<\/tr>/ {
		if (n >= 6) {
			print v[2] " " v[3] " " v[5] " " v[6]
		}
	}
' "$WORKDIR/page.html" >"$WORKDIR/rows.txt"
[ -s "$WORKDIR/rows.txt" ] || die "no package rows found on $STATUS_URL"
N_TOTAL=$(wc -l <"$WORKDIR/rows.txt")
echo "==> $N_TOTAL packages on the page, iterating (fd 3 holds the list)"

# NOTE: the loop input is on fd 3 so that stdin stays attached to the
# terminal for the keypress prompts below.
while read -r PKG DEBVER_PAGE UPSTREAM STATUS <&3; do
	COUNTER=$((COUNTER + 1))
	case "$STATUS" in
	OK)
		echo "==> [$COUNTER/$N_TOTAL] $PKG: already up to date."
		OK_COUNT=$((OK_COUNT + 1))
		continue
		;;
	MISSING)
		echo "W: [$COUNTER/$N_TOTAL] $PKG: missing from Debian, initial packaging needed: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
		;;
	esac

	# Map the Debian source package to its salsa checkout via all-git.
	REPO=$(awk -F, -v p="$PKG" '{
		n = split($1, a, "/")
		if (a[n] == p ".git") { print $1; exit }
	}' "$ALL_GIT")
	if [ -z "$REPO" ]; then
		echo "W: [$COUNTER/$N_TOTAL] $PKG: not found in $ALL_GIT: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi
	RELPATH=${REPO#openstack-team/}
	RELPATH=${RELPATH%.git}
	PKGPATH=$SALSA_ROOT/$RELPATH/$PKG
	if [ ! -d "$PKGPATH" ]; then
		echo "W: [$COUNTER/$N_TOTAL] $PKG: $PKGPATH does not exist (not checked out?): skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi

	CURVER=$(dpkg-parsechangelog -l"$PKGPATH/debian/changelog" -SVersion 2>/dev/null) || {
		echo "W: [$COUNTER/$N_TOTAL] $PKG: cannot parse $PKGPATH/debian/changelog: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	}
	case "$CURVER" in
	*:*)
		EPOCH=${CURVER%%:*}:
		;;
	*)
		EPOCH=
		;;
	esac
	TARGET=${EPOCH}${UPSTREAM}-1
	if dpkg --compare-versions "$CURVER" ge "$TARGET"; then
		echo "==> [$COUNTER/$N_TOTAL] $PKG: already at $CURVER locally (>= $TARGET): skipping."
		SKIP_DONE_COUNT=$((SKIP_DONE_COUNT + 1))
		continue
	fi

	BRANCH=$(git -C "$PKGPATH" rev-parse --abbrev-ref HEAD 2>/dev/null) || {
		echo "W: [$COUNTER/$N_TOTAL] $PKG: not a git checkout: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	}
	if [ "$BRANCH" != "debian/$RELEASE" ]; then
		echo "W: [$COUNTER/$N_TOTAL] $PKG: on branch $BRANCH, not debian/$RELEASE: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi
	if [ -n "$(git -C "$PKGPATH" status --porcelain)" ]; then
		echo "W: [$COUNTER/$N_TOTAL] $PKG: work tree not clean: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi

	SELECTION=
	while [ -z "$SELECTION" ]; do
		printf '==> [%s/%s] Process %s (%s -> %s)? Type y to process, n to skip, q to quit: ' "$COUNTER" "$N_TOTAL" "$PKG" "$CURVER" "$TARGET"
		KEY=$(askkey) || die "cannot prompt for confirmation without a tty."
		echo "$KEY"
		case "$KEY" in
		y) SELECTION=y ;;
		n) SELECTION=n ;;
		q) print_summary; exit 0 ;;
		esac
	done
	if [ "$SELECTION" = "n" ]; then
		echo "==> [$COUNTER/$N_TOTAL] $PKG: skipped by you."
		SKIP_USER_COUNT=$((SKIP_USER_COUNT + 1))
		continue
	fi

	# ---- Process the package (same flow as the "n" script) ----
	cd "$PKGPATH" || die "$PKG: cannot cd to $PKGPATH."
	[ "$(git rev-parse --abbrev-ref HEAD)" = "debian/$RELEASE" ] || die "$PKG: not on branch debian/$RELEASE."
	[ -z "$(git status --porcelain)" ] || die "$PKG: work tree not clean."

	echo "==> [$COUNTER/$N_TOTAL] $PKG: fetching upstream tags"
	./debian/rules fetch-upstream-remote || die "$PKG: fetch-upstream-remote failed."
	if ! git rev-parse -q --verify "refs/tags/$UPSTREAM" >/dev/null; then
		die "$PKG: tag $UPSTREAM not found after fetch (check: git tag -l | grep -F $UPSTREAM)."
	fi

	echo "==> [$COUNTER/$N_TOTAL] $PKG: merging tag $UPSTREAM"
	if ! git merge --no-edit -X theirs "$UPSTREAM"; then
		git merge --abort 2>/dev/null || true
		die "$PKG: merge of $UPSTREAM failed: resolve manually."
	fi

	echo "==> [$COUNTER/$N_TOTAL] $PKG: bumping changelog to $TARGET"
	dch --newversion "$TARGET" -m "New upstream release." --distribution unstable || die "$PKG: dch failed."

	git --no-pager diff -u

	printf '\n==> Review the debian/changelog diff above for %s. Type y to commit it, build, tag/push and upload (any other key aborts): ' "$PKG"
	SAVED_STTY=$(stty -g 2>/dev/null) || die "cannot prompt for confirmation without a tty."
	stty raw -echo min 1 time 0
	KEY=$(dd bs=1 count=1 2>/dev/null) || true
	stty "$SAVED_STTY"
	SAVED_STTY=
	echo "$KEY"
	if [ "$KEY" != "y" ]; then
		die "aborted at review of $PKG: the $TARGET changelog entry is left uncommitted (revert with: git checkout debian/changelog)."
	fi

	echo "==> [$COUNTER/$N_TOTAL] $PKG: committing changelog"
	git commit -m "New upstream release." debian/changelog || die "$PKG: commit failed."

	echo "==> [$COUNTER/$N_TOTAL] $PKG: generating orig tarball for $UPSTREAM"
	./debian/rules gen-orig-xz || die "$PKG: gen-orig-xz failed."

	echo "==> [$COUNTER/$N_TOTAL] $PKG: building with gbp buildpackage"
	if ! gbp buildpackage; then
		die "$PKG: build failed: fix, then continue by hand with gbp buildpackage, then tag/push and upload."
	fi

	echo "==> [$COUNTER/$N_TOTAL] $PKG: tagging and pushing"
	DEBVER=$(dpkg-parsechangelog -SVersion)
	DEBVER_NO_EPOCH=$(echo "$DEBVER" | sed -e 's/^[[:digit:]]*://' -e 's/~/_/g')
	git tag -s "debian/${DEBVER_NO_EPOCH}" -m "Debian release ${DEBVER}" || die "$PKG: tagging failed."
	git push || die "$PKG: git push failed."
	git push --tags || die "$PKG: git push --tags failed."

	echo "==> [$COUNTER/$N_TOTAL] $PKG: uploading"
	cd ../build-area || die "$PKG: cannot cd to ../build-area."
	CHANGES_FILE=$(ls -tr *.changes | tail -n 1)
	[ -n "$CHANGES_FILE" ] || die "$PKG: no .changes file found in $(pwd)."
	debsign --re-sign -k "$DEBSIGN_KEY" "$CHANGES_FILE" || die "$PKG: debsign failed."
	dput "$CHANGES_FILE" || die "$PKG: dput failed."
	echo "I: $PKG updated to $TARGET."
	DONE_COUNT=$((DONE_COUNT + 1))
done 3<"$WORKDIR/rows.txt"

print_summary
exit 0
