#!/bin/sh
# pkgos-update-theme-packages: add a runtime dependency on the system
# Font Awesome fonts to every -doc package built with
# python3-openstackdocstheme. Since python-openstackdocstheme 3.2.0-4
# the theme ships no font files anymore: its font-awesome.css
# @font-face points at /usr/share/fonts-font-awesome (with Font
# Awesome 7's fa-solid-900 as fallback), so the -doc packages need
# fonts-font-awesome-legacy | fonts-font-awesome installed for their
# icons to render.
#
# For each checkout found under $PKGOS_SALSA_ROOT having a -doc binary
# and python3-openstackdocstheme in its control, the script:
#   - adds the dependency to the -doc stanza's Depends (sorted in),
#   - adds a changelog bullet (dch -i when the top entry is uploaded,
#     dch -a when the top entry is still pending),
#   - shows the resulting diff and waits for a y/n/q keypress,
#   - on y: commits, pushes to salsa, builds the source-only upload
#     (the arch:all -doc binaries get rebuilt on the archive side) and
#     dputs it.
# Packages that are not clean, have no -doc stanza or are already done
# are skipped, so the batch can simply be re-run after fixing an issue.
#
# Environment overrides:
#   PKGOS_SALSA_ROOT    checkout root (default: ~/salsa/openstack)
#   PKGOS_NO_PUSH=1     skip the git push step
#   PKGOS_NO_UPLOAD=1   skip the source build and dput step

set -e

DEP="fonts-font-awesome-legacy | fonts-font-awesome"
BULLET="Add a runtime dependency on the Font Awesome fonts to the -doc package, as needed by docs built with openstackdocstheme 3.2.0-4."
COMMIT_TITLE="Depend on system Font Awesome in -doc package."
COMMIT_BODY="The openstackdocstheme 3.2.0-4 based docs load the Font Awesome glyphs from the system files; add fonts-font-awesome-legacy | fonts-font-awesome to the -doc package's runtime depends so the icons render on any system."
SALSA_ROOT=${PKGOS_SALSA_ROOT:-${HOME}/salsa/openstack}
NO_PUSH=${PKGOS_NO_PUSH:-}
NO_UPLOAD=${PKGOS_NO_UPLOAD:-}

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

die () {
	echo "E: $*" >&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.
# Returns 1 on EOF or when no tty is available.
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=
	if [ -z "$KEY" ]; then
		return 1
	fi
	printf '%s' "$KEY"
}

revert_changes () {
	git checkout -- debian/control debian/changelog 2>/dev/null || true
}

print_summary () {
	echo
	echo "=== Summary ==="
	echo "   processed:        $DONE_COUNT"
	echo "   skipped by you:   $SKIP_USER_COUNT"
	echo "   already done:     $SKIP_DONE_COUNT"
	echo "   warnings/skipped: $WARN_COUNT"
}

[ -d "$SALSA_ROOT" ] || die "$SALSA_ROOT not found."

WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/pkgos-update-theme-packages.XXXXXX")
trap cleanup EXIT

echo "==> Looking for theme-using checkouts under $SALSA_ROOT"
# NOTE: the loop input is on fd 3 so that stdin stays attached to the
# terminal for the keypress prompts below.
find "$SALSA_ROOT" \
	\( -type d \( -name .git -o -name build-area -o -name obsolete -o \
		-name archived -o -name upstream -o -name .pc -o -name .stestr \
		-o -name ubuntu -o -name sid -o -name buster -o -name bullseye \
		-o -name bla -o -name old -o -name oldstable -o -name '[a-z]' \
		-o -name '[A-Z]' \) -prune \) \
	-o -type f -path '*/debian/control' -print | sort >"$WORKDIR/controls.txt"
N_TOTAL=$(wc -l <"$WORKDIR/controls.txt")
echo "==> $N_TOTAL debian/control files found, iterating"

while read -r CONTROL <&3; do
	COUNTER=$((COUNTER + 1))
	PKGPATH=${CONTROL%/debian/control}
	PKG=$(basename "$PKGPATH")

	grep -q python3-openstackdocstheme "$CONTROL" || continue

	# The -doc stanza must exist and must not carry the dependency yet.
	DEPSTATE=$(awk -v dep="$DEP" '
		/^Package:[ \t]*/ {
			if ($0 ~ /^Package: .*-doc[ \t]*$/) { in_doc = 1 } else { in_doc = 0 }
		}
		in_doc && index($0, dep) { found = 1 }
		END { if (found) { print "have" } else { print "missing" } }
	' "$CONTROL")
	HASDOC=$(grep -c '^Package: .*-doc[ \t]*$' "$CONTROL" || true)
	if [ "$HASDOC" = 0 ]; then
		echo "W: [$COUNTER/$N_TOTAL] $PKG: no -doc binary: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi
	if [ "$DEPSTATE" = "have" ]; then
		echo "==> [$COUNTER/$N_TOTAL] $PKG: already carries the dependency: 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 [ -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] Add Font Awesome dependency to %s (%s)? Type y to process, n to skip, q to quit: ' "$COUNTER" "$N_TOTAL" "$PKG" "$BRANCH"
		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 ----
	cd "$PKGPATH" || die "$PKG: cannot cd to $PKGPATH."
	[ -z "$(git status --porcelain)" ] || die "$PKG: work tree not clean."

	echo "==> [$COUNTER/$N_TOTAL] $PKG: adding the dependency to debian/control"
	# Insert the dependency into every -doc stanza's Depends, sorted in
	# among the real packages (the ${...} substvars stay at the end).
	awk -v dep="$DEP" '
		function baseof(line) {
			sub(/^[ \t]+/, "", line)
			sub(/,[ \t]*$/, "", line)
			sub(/[ \t(].*$/, "", line)
			return line
		}
		/^Package:[ \t]*/ {
			in_doc = ($0 ~ /^Package: .*-doc[ \t]*$/) ? 1 : 0
			in_dep = 0
			added = 0
			print
			next
		}
		in_doc && /^Depends:[ \t]*$/ {
			in_dep = 1
			print
			next
		}
		in_doc && /^Depends:[ \t]*[^ \t]/ {
			in_dep = 2
			print
			next
		}
		in_dep == 2 { print; next }
		in_dep == 1 && /^[ \t]/ {
			if (!added) {
				base = baseof($0)
				if (base ~ /^\$/ || base > dep) {
					indent = $0
					sub(/[^ \t].*$/, "", indent)
					print indent dep ","
					added = 1
				}
			}
			lastindent = $0
			sub(/[^ \t].*$/, "", lastindent)
			print
			next
		}
		in_dep == 1 {
			if (!added) {
				print lastindent dep ","
				added = 1
			}
			in_dep = 0
			print
			next
		}
		{ print }
	' debian/control > debian/control.new
	mv debian/control.new debian/control
	if ! grep -qF "$DEP" debian/control; then
		revert_changes
		echo "W: [$COUNTER/$N_TOTAL] $PKG: could not patch debian/control (single-line Depends?): skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi

	echo "==> [$COUNTER/$N_TOTAL] $PKG: updating debian/changelog"
	SRC=$(dpkg-parsechangelog -SSource) || {
		revert_changes
		echo "W: [$COUNTER/$N_TOTAL] $PKG: cannot parse changelog: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	}
	if ! RMAD_OUT=$(rmadison "$SRC" 2>/dev/null); then
		revert_changes
		echo "W: [$COUNTER/$N_TOTAL] $PKG: rmadison failed: skipping."
		WARN_COUNT=$((WARN_COUNT + 1))
		continue
	fi
	VER=$(dpkg-parsechangelog -SVersion)
	TOPDIST=$(dpkg-parsechangelog -SDistribution)
	if echo "$RMAD_OUT" | awk -F'|' -v v="$VER" '{
		gsub(/[ \t]/, "", $2)
		if ($2 == v) { found = 1 }
	} END { exit !found }'; then
		# Top entry already uploaded: open a new changelog entry.
		if [ "$TOPDIST" = UNRELEASED ]; then
			revert_changes
			echo "W: [$COUNTER/$N_TOTAL] $PKG: top entry uploaded but still UNRELEASED: skipping (handle by hand)."
			WARN_COUNT=$((WARN_COUNT + 1))
			continue
		fi
		dch -i "$BULLET" || { revert_changes; die "$PKG: dch failed."; }
		if [ "$(dpkg-parsechangelog -SDistribution)" = UNRELEASED ]; then
			sed -i "1s/UNRELEASED/$TOPDIST/" debian/changelog
		fi
	else
		# Top entry pending: append to it, one upload carries both.
		dch -a "$BULLET" || { revert_changes; die "$PKG: dch failed."; }
	fi

	git --no-pager diff -u debian/control debian/changelog

	printf '\n==> Review the diff above for %s. Type y to commit, push and upload (n reverts and skips, q quits, anything else 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"
	case "$KEY" in
	y)
		;;
	n)
		revert_changes
		echo "==> [$COUNTER/$N_TOTAL] $PKG: skipped by you."
		SKIP_USER_COUNT=$((SKIP_USER_COUNT + 1))
		continue
		;;
	q)
		echo "I: aborted at $PKG: the changes are left uncommitted (revert with: git checkout debian/control debian/changelog)."
		print_summary
		exit 0
		;;
	*)
		die "aborted at review of $PKG: the changes are left uncommitted (revert with: git checkout debian/control debian/changelog)."
		;;
	esac

	echo "==> [$COUNTER/$N_TOTAL] $PKG: committing"
	git commit -m "$COMMIT_TITLE" -m "$COMMIT_BODY" debian/control debian/changelog || {
		revert_changes
		die "$PKG: commit failed."
	}

	if [ -n "$NO_PUSH" ]; then
		echo "==> [$COUNTER/$N_TOTAL] $PKG: PKGOS_NO_PUSH set, not pushing."
	else
		echo "==> [$COUNTER/$N_TOTAL] $PKG: pushing"
		git push || die "$PKG: git push failed: fix, then push by hand and continue."
	fi

	if [ -n "$NO_UPLOAD" ]; then
		echo "==> [$COUNTER/$N_TOTAL] $PKG: PKGOS_NO_UPLOAD set, not uploading."
	else
		echo "==> [$COUNTER/$N_TOTAL] $PKG: building the source-only upload"
		if ! gbp buildpackage; then
			die "$PKG: source build failed: fix, then continue by hand with gbp buildpackage -S -sd -us -uc, debsign and dput."
		fi
		CHANGES_FILE=
		for d in ../build-area; do
			CHANGES_FILE=$(ls -tr "$d/${SRC}"_*.changes 2>/dev/null | tail -n 1)
			[ -n "$CHANGES_FILE" ] && break
		done
		[ -n "$CHANGES_FILE" ] || die "$PKG: no .changes file found in ../build-area or .."
		echo "==> [$COUNTER/$N_TOTAL] $PKG: uploading $CHANGES_FILE"
		debsign --re-sign -k 'A0B1A9F3508956130E7A425CD416AD15AC6B43FE!' "$CHANGES_FILE" || die "$PKG: debsign failed."
		dput "$CHANGES_FILE" || die "$PKG: dput failed."
	fi

	echo "I: $PKG updated."
	DONE_COUNT=$((DONE_COUNT + 1))
done 3<"$WORKDIR/controls.txt"

print_summary
exit 0
