#!/usr/bin/env sh

SCRIPT=$(readlink -f "${0}")
RHPKG=${RHPKG:-rhpkg}
PATCHPAL=${PATCHPAL:-patchpal}

init_vars() {
	SPEC_FILE=
	DIST_GIT_HASH=
	DIST_GIT_HASH_REPO=
	DIST_GIT_SOURCE_REPO=
	PATCH_LIST=
	PATCH_LIST_START_NUMBER=
	COMMIT_CHANGES=
}

die() {
	for ERR in "$@"; do
		printf "Error: %s\n" "${ERR}" >&2
	done
	exit 1
}

status() {
	printf "\n"
	for LINE in "$@"; do
		printf ">>> %s\n" "${LINE}"
	done
}

help() {
	printf -- "Description:\n"
	printf -- "%s is a tool for backporting dist-git commits to older releases." "${SCRIPT##*/}\n"
	printf -- "It is a wrapper around patchpal.\n"
	printf -- "It MUST be called from a dist-git checkout\n"
	printf -- "branch to which a given git hash is to be backported.\n"
}

usage() {
	printf -- "Usage: %s [<options>] -s <spec file> <commit hash>\n\n" "${SCRIPT##*/}"
	printf -- "<commit hash>\tThe dist-git hash to backport to the currently checkout branch.\n\n"
	printf -- "Options:\n"
	printf -- "-h\tHelp output, i.e. what you're reading now.\n"
	printf -- "-c\tCommit changes.\n"
	printf -- "-s\tRPM spec file. Required. Path is relative to CWD (dist-get checkout).\n"
}

parse_args() {
	while test $# -ge 0; do
		case $1 in
			-h) help; usage; exit 0 ;;
			-c) COMMIT_CHANGES=1 ;;
			-s) shift; SPEC_FILE="$1" ;;
			-*) usage; exit 1 ;;
			*) break ;;
		esac
		shift
	done

	test $# -eq 0 && { usage; exit 1; }
	test -n "${SPEC_FILE}" || { usage; exit 1; }
	DIST_GIT_HASH="$1"
	TEMP_DIR="$(mktemp -d)"
	DIST_GIT_HASH_REPO="${TEMP_DIR}/${DIST_GIT_HASH}"
	DIST_GIT_SOURCE_REPO="${TEMP_DIR}/source"
	PATCH_LIST_START_NUMBER=$(spectool -P "${SPEC_FILE}" |sed -e 's/^Patch\([0-9]\+\)[:].*/\1/' |sort -n |tail -n1)
	# shellcheck disable=SC2003
	PATCH_LIST_START_NUMBER=$(expr "${PATCH_LIST_START_NUMBER}" \+ 1)
}

init_dist_git_source_repo() {
	status "Init dist-git source repo"

	git init "${DIST_GIT_SOURCE_REPO}" || die "Failed to init dist-git source tree"
	git -C "${DIST_GIT_SOURCE_REPO}" checkout -b main
	git -C "${DIST_GIT_SOURCE_REPO}" commit --allow-empty -m "empty" || die "Failed to init dist-git source tree"
}

build_dist_git_hash_prev_branch() {
	status "Building git branch dist-git hash ${DIST_GIT_HASH}^"

	git -C "${DIST_GIT_HASH_REPO}" checkout -f "${DIST_GIT_HASH}^" ||
		die "failed to checkout dist-git commit ${DIST_GIT_HASH}^"

	git -C "${DIST_GIT_SOURCE_REPO}" checkout -b dist-git-hash-prev main ||
		die "failed to create dist-git-hash-prev branch"
	tar -C "${DIST_GIT_SOURCE_REPO}" \
	    --strip-components 1 \
	    -xf "${DIST_GIT_HASH_REPO}/$(basename "$(spectool -S "${DIST_GIT_HASH_REPO}/${SPEC_FILE}" |head -n 1 |awk '{print $2}')")" ||
		die "failed to unpack dist-git-hash-prev source"
	git -C "${DIST_GIT_SOURCE_REPO}" add . ||
		die "failied to add dist-git-hash-prev source"
	git -C "${DIST_GIT_SOURCE_REPO}" commit -m "source unpack" ||
		die "failed to commit dist-git-hash-prev source"
	for PATCH in $(spectool -P "${DIST_GIT_HASH_REPO}/${SPEC_FILE}" |awk '{print $2}'); do
		# FIXME: no guarantee that these are git format-patch patches
		# would have to apply with "patch" and git add/commit.
		git -C "${DIST_GIT_SOURCE_REPO}" am "${DIST_GIT_HASH_REPO}/${PATCH}" ||
			die "failed add dist-git-hash-prev patch: ${PATCH}"
	done
}

build_dist_git_hash_branch() {
	status "Building git branch dist-git hash ${DIST_GIT_HASH}"

	DIST_GIT_HASH_PREV_PATCHES=$(mktemp)
	git -C "${DIST_GIT_HASH_REPO}" checkout -f "${DIST_GIT_HASH}^"
	spectool -P "${DIST_GIT_HASH_REPO}/${SPEC_FILE}" > "${DIST_GIT_HASH_PREV_PATCHES}"

	DIST_GIT_HASH_PATCHES=$(mktemp)
	git -C "${DIST_GIT_HASH_REPO}" checkout -f "${DIST_GIT_HASH}"
	spectool -P "${DIST_GIT_HASH_REPO}/${SPEC_FILE}" > "${DIST_GIT_HASH_PATCHES}"

	git -C "${DIST_GIT_SOURCE_REPO}" checkout -b dist-git-hash dist-git-hash-prev ||
		"failed to create branch dist-git-hash"
	NEW_PATCHES=$(diff -u "${DIST_GIT_HASH_PREV_PATCHES}" "${DIST_GIT_HASH_PATCHES}" |grep "^[+]Patch" |awk '{print $2}')
	for PATCH in ${NEW_PATCHES}; do
		# FIXME: no guarantee that these are git format-patch patches
		# would have to apply with "patch" and git add/commit.
		git -C "${DIST_GIT_SOURCE_REPO}" am "${DIST_GIT_HASH_REPO}/${PATCH}" ||
			die "failed add dist-git-hash patch: ${PATCH}"
	done
}

build_working_tree_branch() {
	status "Building git branch for working tree"

	git -C "${DIST_GIT_SOURCE_REPO}" checkout -b working-tree main ||
		die "failed to create working-tree branch"
	tar -C "${DIST_GIT_SOURCE_REPO}" \
	    --strip-components 1 \
	    -xf "$(basename "$(spectool -S "${SPEC_FILE}" |head -n 1 |awk '{print $2}')")" ||
		die "failed to unpack working-tree source tarball"
	git -C "${DIST_GIT_SOURCE_REPO}" add . ||
		die "failed to git-add working-tree sources"
	git -C "${DIST_GIT_SOURCE_REPO}" commit -m "source unpack" ||
		die "failed to commit working-tree sources"
	for PATCH in $(spectool -P "${SPEC_FILE}" |awk '{print $2}'); do
		# FIXME: no guarantee that these are git format-patch patches
		# would have to apply with "patch" and git add/commit.
		git -C "${DIST_GIT_SOURCE_REPO}" am "${PWD}/${PATCH}" ||
			die "failed add working-tree patch: ${PATCH}"
	done
}

download_dist_git_sources() {
	status "Fetching dist-git sources"
	spectool -g -S "${SPEC_FILE}" || die "Failed to download dist-git sources"
}

checkout_dist_git_hash() {
	status "Checking out the dist-git tree at hash ${DIST_GIT_HASH}"
	git worktree add "${DIST_GIT_HASH_REPO}" "${DIST_GIT_HASH}" ||
		die "Failed to checkout dist-git commit \"${DIST_GIT_HASH}\"."
	# download the sources
	(
		cd "${DIST_GIT_HASH_REPO}" || die "failed to chdir"
		spectool -g -S "${DIST_GIT_HASH_REPO}/${SPEC_FILE}" || die "Failed to download dist-git sources"
	)
}

backport_patches() {
	HASHES=$(git -C "${DIST_GIT_SOURCE_REPO}" log --pretty="format:%H" --reverse dist-git-hash-prev..dist-git-hash)

	status "Begin backporting patches to working tree"
	git -C "${DIST_GIT_SOURCE_REPO}" checkout -b working-tree-next working-tree
	for HASH in ${HASHES}; do
		status "Calling patchpal to backport commit ${HASH}"
		( cd "${DIST_GIT_SOURCE_REPO}" && patchpal "${HASH}"; )
	done
}

generate_patches_for_dist_git() {
	git -C "${DIST_GIT_SOURCE_REPO}" format-patch --no-cover-letter \
	                                              --start-number "${PATCH_LIST_START_NUMBER}" \
	                                              working-tree..working-tree-next
	PATCH_LIST=$(ls "${DIST_GIT_SOURCE_REPO}"/*.patch)

	for PATCH in ${PATCH_LIST}; do
		cp "${PATCH}" .

		if test -n "${COMMIT_CHANGES}"; then
			git add "$(basename "${PATCH}")"
		fi
	done
}

update_spec_file() {
	# add new patches to spec
	sed -e '1,/^Patch[0-9]\+[:]/p;d' "${SPEC_FILE}" | sed '$d' > "${SPEC_FILE}.new"
	grep '^Patch[0-9]\+[:]' "${SPEC_FILE}" >> "${SPEC_FILE}.new"
	NUM="${PATCH_LIST_START_NUMBER}"
	for PATCH in ${PATCH_LIST}; do
		printf "Patch%u: %s" >> "${SPEC_FILE}.new" "${NUM}" "$(basename "${PATCH}")"
		# shellcheck disable=SC2003
		NUM=$(expr "${NUM}" \+ 1)
	done
	sed -e '/^Patch[0-9]\+[:]/,$p;d' "${SPEC_FILE}" | grep -v '^Patch[0-9]\+[:]' >> "${SPEC_FILE}.new"
	mv "${SPEC_FILE}.new" "${SPEC_FILE}"

	# bump version and add changelog
	CHANGELOG=$(git -C "${DIST_GIT_SOURCE_REPO}" log --pretty='format:%s' working-tree..working-tree-next |sed 's/^/- /')
	rpmdev-bumpspec --comment="${CHANGELOG}" "${SPEC_FILE}"

	if test -n "${COMMIT_CHANGES}"; then
		git add "${SPEC_FILE}"
	fi
}

sanity() {
	test -n "${DISPLAY}" || die "Must run from a X/Wayland or have DISPLAY set."
	command -v spectool >/dev/null 2>&1 || die "spectool must be installed with \`dnf install rpmspectool\`."
	command -v patchpal >/dev/null 2>&1 || die "patchpal must be in your PATH."
	command -v rpmdev-bumpspec >/dev/null 2>&1 || die "rpmdev-bumpspec must be installed with \`dnf install rpmdevtools\`."
}

unit_test() {
	# FIXME: Add some tests. Can set SPEC_FILE, DIST_GIT_*, etc. since
	# we're called before parse_args and main
	:
}

main() {
	init_vars
	parse_args "$@"
	sanity

	download_dist_git_sources
	checkout_dist_git_hash
	init_dist_git_source_repo
	build_working_tree_branch
	build_dist_git_hash_prev_branch
	build_dist_git_hash_branch
	backport_patches
	generate_patches_for_dist_git
	update_spec_file
	if test -n "${COMMIT_CHANGES}"; then
		git commit -m "$(git -C "${DIST_GIT_HASH_REPO}" log -n 1 --pretty=format:%s "${DIST_GIT_HASH}")"

		status "Complete. Changes have been committed" \
		       "You should 'git commit --amend' to add the Jira Issue."
	else
		status "Complete. Changes have NOT been committed or staged."
	fi
}

unit_test
main "$@"
