#!/usr/bin/env bash
# (c) 2026 Michał Górny
# SPDX-License-Identifier: GPL-2.0-or-later

VERSION=3

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

edo() {
	echo "+ ${@@Q}" >&2
	[[ ${DRY_RUN} ]] && return
	"${@}"
	local ret=${?}
	[[ ${ret} -ne 0 ]] && die "Command failed with exit status ${?}"
}

print_usage() {
	echo "Usage: ${0} [<options>]"
}

print_help() {
	print_usage
	echo
	echo "Create a Forgejo pull request for the current branch via AGit."
	echo
	echo "Options:"
	echo "  -d DESC, --description DESC"
	echo "                   Set the pull request description"
	echo "  -e EDITOR, --editor EDITOR"
	echo "                   Override the editor used (default: \${EDITOR})"
	echo "  -o TOPIC, --topic TOPIC"
	echo "                   Specify the topic to use (default: branch name)"
	echo "  -n, --dry-run    Print the git commands without executing them"
	echo "  -r REMOTE, --remote REMOTE"
	echo "                   Specify the Codeberg remote name"
	echo "                   (default: search for codeberg.org SSH URL)"
	echo "  -T TARGET, --target TARGET"
	echo "                   Specify the target branch"
	echo "                   (default: first of dev, main, master on remote)"
	echo "  -t TITLE, --title TITLE"
	echo "                   Set the pull request title"
	echo "  -h, --help       Print this help message"
	echo "  -V, --version    Print the version string"
}

find_remote() {
	local remotes=() r url
	mapfile -t remotes < <(git remote)
	# prefer [codeberg, upstream, origin], fall back to all branches
	for r in codeberg upstream origin "${remotes[@]}"; do
		url=$(git remote get-url "${r}" 2>/dev/null)
		case ${url} in
			*ssh://git@codeberg.org/*|git@codeberg.org:*)
				echo "${r}"
				break
		esac
	done
}

find_target_branch() {
	local remote=${1}
	local candidate
	for candidate in dev main master; do
		if git rev-parse "${remote}/${candidate}" &>/dev/null; then
			echo "${candidate}"
			return
		fi
	done
}

get_template() {
	local target=${1}
	local path

	# matches forgejo behavior
	for path in .{forgejo,github}/pull_request_template.md; do
		git cat-file -p "${target}:${path}" 2>/dev/null && return
	done
}

format_commits() {
	local IFS=
	echo "${*}"
}

fj_b64() {
	local data=${1}

	printf '{base64}'
	base64 -w 0 <<<"${data}"
}

main() {
	local description=
	local DRY_RUN=
	local editor=
	local remote=
	local target=
	local title=
	local topic=

	while [[ ${#} -gt 0 ]]; do
		case ${1} in
			-h|--help)
				print_help
				exit 0
				;;
			-V|--version)
				echo "gagit ${VERSION}"
				exit 0
				;;
			-d|--description)
				[[ ${#} -gt 1 ]] || die "${0}: missing argument to ${1}"
				description=${2}
				shift
				;;
			-d*)
				description=${1#-d}
				;;
			--description=*)
				description=${1#*=}
				;;
			-e|--editor)
				[[ ${#} -gt 1 ]] || die "${0}: missing argument to ${1}"
				editor=${2}
				shift
				;;
			-e*)
				editor=${1#-e}
				;;
			--editor=*)
				editor=${1#*=}
				;;
			-n|--dry-run)
				DRY_RUN=1
				;;
			-r|--remote)
				[[ ${#} -gt 1 ]] || die "${0}: missing argument to ${1}"
				remote=${2}
				shift
				;;
			-r*)
				remote=${1#-r}
				;;
			--remote=*)
				remote=${1#*=}
				;;
			-T|--target)
				[[ ${#} -gt 1 ]] || die "${0}: missing argument to ${1}"
				target=${2}
				shift
				;;
			-T*)
				target=${1#-T}
				;;
			--target=*)
				target=${1#*=}
				;;
			-t|--title)
				[[ ${#} -gt 1 ]] || die "${0}: missing argument to ${1}"
				title=${2}
				shift
				;;
			-t*)
				title=${1#-t}
				;;
			--title=*)
				title=${1#*=}
				;;
			-o|--topic)
				[[ ${#} -gt 1 ]] || die "${0}: missing argument to ${1}"
				topic=${2}
				shift
				;;
			-o*)
				topic=${1#-o}
				;;
			--topic=*)
				topic=${1#*=}
				;;
			-*)
				print_usage >&2
				exit 1
				;;
			*)
				echo "${0}: no positional arguments expected" >&2
				print_usage
				exit 1
				;;
		esac
		shift
	done

	# set defaults
	: "${editor:=${EDITOR:-vim}}"

	if [[ ${DRY_RUN} ]]; then
		echo "Running in dry-run mode, no modifying git calls will be executed" >&2
	fi

	if [[ $(git rev-parse --is-inside-work-tree 2>/dev/null) != true ]]; then
		die "gagit needs to be run inside a git checkout"
	fi

	if [[ -z ${remote} ]]; then
		remote=$(find_remote)
	fi
	[[ -z ${remote} ]] && die "Codeberg remote not found"
	echo "Codeberg remote: ${remote}" >&2

	local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
	echo "Current branch: ${branch}" >&2
	case ${branch} in
		HEAD|dev|main|master)
			die "Please run gagit on a new feature branch"
			;;
	esac

	: "${topic:=${branch}}"

	if [[ -z ${target} ]]; then
		local target=$(find_target_branch "${remote}")
		[[ -z ${target} ]] && die "Unable to find a target branch"
	fi
	echo "Target branch: ${target}" >&2

	local template=$(get_template "${target}")

	local commits=()
	mapfile commits < <(git log --format='# %h %s' "${target}..${branch}")
	[[ ${#commits[@]} -eq 0 ]] &&
		die "No changes found in ${target}..${branch} range"

	: "${title:=$(git log --format="WIP: %s${commits[1]+ (and more)}" -1)}"
	: "${description:=$(git log --format='%b' -1)}"

	tempdir=$(mktemp -d) || die "Unable to create a temporary directory"
	trap 'rm -r -f "${tempdir}"' EXIT
	cat > "${tempdir}/message" <<-EOF || die "Unable to write template"
		# Please edit this file to set the new pull request title and description.
		# All lines starting with '#' will be discarded.
		# An empty description will abort creating the PR.
		# ${branch} -> ${remote}/${target}${DRY_RUN:+ (dry run)}

		# Put a single line title below.
		# "WIP:" creates a draft.  Remove it to skip draft stage.
		${title}

		# Put the pull request description below.
		${description}

		# Commits are listed here for your convenience.
		$(format_commits "${commits[@]}")

		# Please leave the template below.
		# Either fill the boxes here or via the website after pushing.
		${template}
	EOF

	${editor} "${tempdir}/message" || die "Editor failed"
	title=
	description=
	local line nl=$'\n'
	{
		# find the title
		while read -r line; do
			[[ ${line} == '#'* || -z ${line## } ]] && continue
			title=${line}
			break
		done
		# no title? sounds like we got an empty file (whitespace only)
		[[ -z ${title} ]] && die "No pull request description provided."

		# find the initial description line
		while read -r line; do
			[[ ${line} == '#'* || -z ${line## } ]] && continue
			description="${line}${nl}"
			break
		done

		# copy the remaining description lines
		while read -r line; do
			[[ ${line} == '#'* ]] && continue
			description+="${line}${nl}"
		done
	} < "${tempdir}/message"

	echo "Title: ${title}"
	edo git fetch "${remote}"
	edo git config "branch.${branch}.remote" "${remote}"
	edo git config "branch.${branch}.merge" "refs/for/${target}/${topic}"
	edo git push \
		-o "title=$(fj_b64 "${title}")" \
		-o "description=$(fj_b64 "${description}")"

	echo
	echo "The branch has been set to push to the PR.  To update it:"
	echo "  git push"
	echo
	echo "If you needed to force-push:"
	echo "  git push -o force-push=true"
	echo
}

main "${@}"
