#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR="$(git rev-parse --show-toplevel)"
GITLINT_FILE="$ROOT_DIR/.gitlint"
START_HEAD=""
CREATED_TAG=""
VERSION_META_LINES=()
VERSION_META_EXPORT_NAMES=()

# ── logging ────────────────────────────────────────────────────────────────

log() { echo "[release] $*" >&2; }

usage() {
	local cmd
	cmd="$(basename "$0")"
	printf '%s\n' \
		"Usage:" \
		"  ${cmd} [major|minor|patch] [stable|__CHANNEL_LIST__]" \
		"  ${cmd} set <version>" \
		"" \
		"Bump types:" \
		"  (none)            bump patch, keep current channel" \
		"  major/minor/patch bump the given part, keep current channel" \
		"  stable / full     remove prerelease suffix (only opt-in path to promote prerelease -> stable)" \
		"  __CHANNEL_LIST__    switch channel (from stable, auto-bumps patch unless bump is specified)" \
		"" \
		"Safety rule:" \
		"  If current version is prerelease (e.g. x.y.z-beta.N), promotion to stable is allowed only via 'stable' or 'full'." \
		"  Commands like '${cmd} set x.y.z' or '${cmd} patch stable' are blocked from prerelease channels." \
		"" \
		"Examples:" \
		"  ${cmd}                 # patch bump on current channel" \
		"  ${cmd} minor           # minor bump on current channel" \
		"  ${cmd} beta            # from stable: patch bump + beta.1" \
		"  ${cmd} patch beta      # patch bump, switch to beta channel" \
		"  ${cmd} rc              # switch to rc channel" \
		"  ${cmd} stable          # promote prerelease to stable (opt-in)" \
		"  ${cmd} set 1.2.3" \
		"  ${cmd} set 1.2.3-beta.1"
}

# ── git ────────────────────────────────────────────────────────────────────

require_clean_git() {
	if ! git diff --quiet || ! git diff --cached --quiet; then
		echo "Error: git working tree is not clean. Commit or stash changes first." >&2
		exit 1
	fi
}

revert_on_failure() {
	local status=$?
	if [[ -n $START_HEAD ]]; then
		log "Release failed — reverting to $START_HEAD"
		git reset --hard "$START_HEAD"
	fi
	if [[ -n $CREATED_TAG ]]; then
		git tag -d "$CREATED_TAG" >/dev/null 2>&1 || true
	fi
	exit $status
}

# ── version parsing ────────────────────────────────────────────────────────

parse_base_version() {
	local v="$1"
	if [[ ! $v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
		echo "Error: invalid base version '$v' (expected x.y.z)" >&2
		exit 1
	fi
	MAJOR="${BASH_REMATCH[1]}"
	MINOR="${BASH_REMATCH[2]}"
	PATCH="${BASH_REMATCH[3]}"
}

parse_full_version() {
	local v="$1"
	CHANNEL="stable"
	PRERELEASE_NUM=""

	if [[ $v =~ ^([0-9]+\.[0-9]+\.[0-9]+)-([a-zA-Z]+)\.([0-9]+)$ ]]; then
		BASE_VERSION="${BASH_REMATCH[1]}"
		CHANNEL="${BASH_REMATCH[2]}"
		PRERELEASE_NUM="${BASH_REMATCH[3]}"
	elif [[ $v =~ ^([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
		BASE_VERSION="${BASH_REMATCH[1]}"
	else
		echo "Error: invalid version '$v' (expected x.y.z or x.y.z-channel.N)" >&2
		exit 1
	fi
	parse_base_version "$BASE_VERSION"
}

validate_channel() {
	local ch="$1"
	[[ $ch == "stable" ]] && return 0
	local valid_channels="__CHANNEL_LIST__"
	for c in $valid_channels; do
		[[ $ch == "$c" ]] && return 0
	done
	echo "Error: unknown channel '$ch'. Valid channels: stable $valid_channels" >&2
	exit 1
}

version_cmp() {
	# Returns: 0 if equal, 1 if v1 > v2, 2 if v1 < v2
	# Stable > prerelease for same base version
	local v1="$1" v2="$2"
	[[ $v1 == "$v2" ]] && return 0

	local base1="" pre1="" base2="" pre2=""
	if [[ $v1 =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(.+)$ ]]; then
		base1="${BASH_REMATCH[1]}"
		pre1="${BASH_REMATCH[2]}"
	else
		base1="$v1"
	fi
	if [[ $v2 =~ ^([0-9]+\.[0-9]+\.[0-9]+)-(.+)$ ]]; then
		base2="${BASH_REMATCH[1]}"
		pre2="${BASH_REMATCH[2]}"
	else
		base2="$v2"
	fi

	if [[ $base1 != "$base2" ]]; then
		local highest_base
		highest_base=$(printf '%s\n%s\n' "$base1" "$base2" | sort -V | tail -n1)
		[[ $highest_base == "$base1" ]] && return 1 || return 2
	fi

	[[ -z $pre1 && -n $pre2 ]] && return 1 # stable > prerelease
	[[ -n $pre1 && -z $pre2 ]] && return 2 # prerelease < stable
	[[ -z $pre1 && -z $pre2 ]] && return 0 # both stable

	local highest_pre
	highest_pre=$(printf '%s\n%s\n' "$pre1" "$pre2" | sort -V | tail -n1)
	[[ $highest_pre == "$pre1" ]] && return 1 || return 2
}

bump_base_version() {
	case "$1" in
	major)
		MAJOR=$((MAJOR + 1))
		MINOR=0
		PATCH=0
		;;
	minor)
		MINOR=$((MINOR + 1))
		PATCH=0
		;;
	patch) PATCH=$((PATCH + 1)) ;;
	*)
		echo "Error: unknown bump part '$1'" >&2
		exit 1
		;;
	esac
	BASE_VERSION="${MAJOR}.${MINOR}.${PATCH}"
}

compute_full_version() {
	if [[ $CHANNEL == "stable" || -z $CHANNEL ]]; then
		FULL_VERSION="$BASE_VERSION"
	else
		FULL_VERSION="${BASE_VERSION}-${CHANNEL}.${PRERELEASE_NUM:-1}"
	fi
	FULL_TAG="v$FULL_VERSION"
	export BASE_VERSION CHANNEL PRERELEASE_NUM FULL_VERSION FULL_TAG
}

meta_env_name() {
	local key="$1"
	key="${key//[^[:alnum:]]/_}"
	key="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
	printf 'VERSION_META_%s\n' "$key"
}

clear_version_meta_exports() {
	local export_name
	for export_name in "${VERSION_META_EXPORT_NAMES[@]:-}"; do
		unset "$export_name"
	done
	VERSION_META_EXPORT_NAMES=()
}

load_version_metadata() {
	VERSION_META_LINES=()
	[[ ! -f "$ROOT_DIR/VERSION" ]] && return 0

	while IFS= read -r line || [[ -n $line ]]; do
		VERSION_META_LINES+=("$line")
	done < <(tail -n +4 "$ROOT_DIR/VERSION" 2>/dev/null || true)
}

export_version_metadata() {
	clear_version_meta_exports

	local line key value export_name
	for line in "${VERSION_META_LINES[@]:-}"; do
		[[ $line != *=* ]] && continue
		key="${line%%=*}"
		value="${line#*=}"
		[[ -z $key ]] && continue
		export_name="$(meta_env_name "$key")"
		printf -v "$export_name" '%s' "$value"
		export "${export_name?}=$value"
		VERSION_META_EXPORT_NAMES+=("$export_name")
	done
}

write_version_file() {
	local channel_to_write="$1"
	local n_to_write="$2"
	{
		printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write"
		local line
		for line in "${VERSION_META_LINES[@]:-}"; do
			printf '%s\n' "$line"
		done
	} >"$ROOT_DIR/VERSION"
}

version_meta_get() {
	local key="${1-}"
	local line
	for line in "${VERSION_META_LINES[@]:-}"; do
		if [[ $line == "$key="* ]]; then
			printf '%s\n' "${line#*=}"
			return 0
		fi
	done
	return 1
}

version_meta_set() {
	local key="${1-}"
	local value="${2-}"
	[[ -z $key ]] && echo "Error: version_meta_set requires a key" >&2 && exit 1

	local updated=0
	local index
	for index in "${!VERSION_META_LINES[@]}"; do
		if [[ ${VERSION_META_LINES[index]} == "$key="* ]]; then
			VERSION_META_LINES[index]="$key=$value"
			updated=1
			break
		fi
	done

	if [[ $updated -eq 0 ]]; then
		VERSION_META_LINES+=("$key=$value")
	fi

	export_version_metadata
	version_meta_write
}

version_meta_unset() {
	local key="${1-}"
	[[ -z $key ]] && echo "Error: version_meta_unset requires a key" >&2 && exit 1

	local filtered=()
	local line
	for line in "${VERSION_META_LINES[@]:-}"; do
		[[ $line == "$key="* ]] && continue
		filtered+=("$line")
	done
	VERSION_META_LINES=("${filtered[@]}")

	export_version_metadata
	version_meta_write
}

version_meta_write() {
	local channel_to_write="$CHANNEL"
	local n_to_write="${PRERELEASE_NUM:-1}"
	if [[ $channel_to_write == "stable" || -z $channel_to_write ]]; then
		channel_to_write="stable"
		n_to_write="0"
	fi
	write_version_file "$channel_to_write" "$n_to_write"
}

# ── gitlint ────────────────────────────────────────────────────────────────

get_gitlint_title_regex() {
	[[ ! -f $GITLINT_FILE ]] && return 0
	awk '
    /^\[title-match-regex\]$/ { in_section=1; next }
    /^\[/ { in_section=0 }
    in_section && /^regex=/ { sub(/^regex=/, ""); print; exit }
  ' "$GITLINT_FILE"
}

validate_commit_message() {
	local msg="$1"
	local regex
	regex="$(get_gitlint_title_regex)"
	if [[ -n $regex && ! $msg =~ $regex ]]; then
		echo "Error: commit message does not match .gitlint title-match-regex" >&2
		echo "Regex:   $regex" >&2
		echo "Message: $msg" >&2
		exit 1
	fi
}

# ── version file generation ────────────────────────────────────────────────

run_release_steps() {
	:
	__RELEASE_STEPS__
}

# ── version source (built-in) ──────────────────────────────────────────────

# Initializes $ROOT_DIR/VERSION from git tags if it doesn't exist.
# Must be called outside of any subshell so log output stays on stderr
# and never contaminates the stdout of do_read_version.
init_version_file() {
	if [[ -f "$ROOT_DIR/VERSION" ]]; then
		load_version_metadata
		export_version_metadata
		return 0
	fi

	local highest_tag=""
	while IFS= read -r raw_tag; do
		local tag="${raw_tag#v}"
		[[ $tag =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$ ]] || continue

		if [[ -z $highest_tag ]]; then
			highest_tag="$tag"
			continue
		fi

		local cmp_status=0
		version_cmp "$tag" "$highest_tag" || cmp_status=$?
		[[ $cmp_status -eq 1 ]] && highest_tag="$tag"
	done < <(git tag --list)

	[[ -z $highest_tag ]] && highest_tag="0.0.1"

	parse_full_version "$highest_tag"
	local channel_to_write="$CHANNEL"
	local n_to_write="${PRERELEASE_NUM:-1}"
	if [[ $channel_to_write == "stable" || -z $channel_to_write ]]; then
		channel_to_write="stable"
		n_to_write="0"
	fi

	VERSION_META_LINES=()
	write_version_file "$channel_to_write" "$n_to_write"
	export_version_metadata
	log "Initialized $ROOT_DIR/VERSION from highest tag: v$highest_tag"
}

do_read_version() {
	load_version_metadata
	export_version_metadata

	local base_line channel_line n_line
	base_line="$(sed -n '1p' "$ROOT_DIR/VERSION" | tr -d '\r')"
	channel_line="$(sed -n '2p' "$ROOT_DIR/VERSION" | tr -d '\r')"
	n_line="$(sed -n '3p' "$ROOT_DIR/VERSION" | tr -d '\r')"

	if [[ -z $channel_line || $channel_line == "stable" ]]; then
		printf '%s\n' "$base_line"
	else
		printf '%s-%s.%s\n' "$base_line" "$channel_line" "$n_line"
	fi
}

do_write_version() {
	local channel_to_write="$CHANNEL"
	local n_to_write="${PRERELEASE_NUM:-1}"
	if [[ $channel_to_write == "stable" || -z $channel_to_write ]]; then
		channel_to_write="stable"
		n_to_write="0"
	fi
	write_version_file "$channel_to_write" "$n_to_write"
	export_version_metadata
}

# ── user-provided hook ─────────────────────────────────────────────────────

do_post_version() {
	:
	__POST_VERSION__
}

# ── main ───────────────────────────────────────────────────────────────────

main() {
	[[ ${1-} == "-h" || ${1-} == "--help" ]] && usage && exit 0

	require_clean_git
	START_HEAD="$(git rev-parse HEAD)"
	trap revert_on_failure ERR

	# Initialize VERSION file outside any subshell so log lines never
	# bleed into the stdout capture below.
	init_version_file

	local raw_version
	raw_version="$(do_read_version | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' | tail -n1)"
	if [[ -z $raw_version ]]; then
		echo "Error: could not determine current version from VERSION source" >&2
		exit 1
	fi
	parse_full_version "$raw_version"
	compute_full_version
	local current_full="$FULL_VERSION"

	log "Current: base=$BASE_VERSION channel=$CHANNEL pre=${PRERELEASE_NUM:-}"

	local action="${1-}"
	shift || true

	if [[ $action == "set" ]]; then
		local newv="${1-}"
		local current_channel="$CHANNEL"
		[[ -z $newv ]] && echo "Error: 'set' requires a version argument" >&2 && exit 1
		parse_full_version "$newv"
		validate_channel "$CHANNEL"
		if [[ $current_channel != "stable" && $CHANNEL == "stable" ]]; then
			echo "Error: from prerelease channel '$current_channel', promote using 'stable' or 'full' only" >&2
			exit 1
		fi
		compute_full_version
		local cmp_status=0
		version_cmp "$FULL_VERSION" "$current_full" || cmp_status=$?
		case $cmp_status in
		0)
			echo "Version $FULL_VERSION is already current; nothing to do." >&2
			exit 1
			;;
		2)
			echo "Error: $FULL_VERSION is lower than current $current_full" >&2
			exit 1
			;;
		esac

	else
		local part="" target_channel="" was_channel_only=0

		case "$action" in
		"") part="patch" ;;
		major | minor | patch)
			part="$action"
			target_channel="${1-}"
			;;
		stable | full)
			[[ -n ${1-} ]] && echo "Error: '$action' takes no second argument" >&2 && usage && exit 1
			target_channel="stable"
			;;
		*)
			# check if action is a valid channel
			local is_channel=0
			for c in __CHANNEL_LIST__; do
				[[ $action == "$c" ]] && is_channel=1 && break
			done
			if [[ $is_channel == 1 ]]; then
				[[ -n ${1-} ]] && echo "Error: channel-only bump takes no second argument" >&2 && usage && exit 1
				target_channel="$action"
				was_channel_only=1
			else
				echo "Error: unknown argument '$action'" >&2
				usage
				exit 1
			fi
			;;
		esac

		[[ -z $target_channel ]] && target_channel="$CHANNEL"
		[[ $target_channel == "full" ]] && target_channel="stable"
		validate_channel "$target_channel"
		if [[ $CHANNEL != "stable" && $target_channel == "stable" && $action != "stable" && $action != "full" ]]; then
			echo "Error: from prerelease channel '$CHANNEL', promote using 'stable' or 'full' only" >&2
			exit 1
		fi

		if [[ -z $part && $was_channel_only -eq 1 && $CHANNEL == "stable" && $target_channel != "stable" ]]; then
			part="patch"
		fi

		local old_base="$BASE_VERSION" old_channel="$CHANNEL" old_pre="$PRERELEASE_NUM"
		[[ -n $part ]] && bump_base_version "$part"

		if [[ $target_channel == "stable" ]]; then
			CHANNEL="stable"
			PRERELEASE_NUM=""
		else
			if [[ $BASE_VERSION == "$old_base" && $target_channel == "$old_channel" && -n $old_pre ]]; then
				PRERELEASE_NUM=$((old_pre + 1))
			else
				PRERELEASE_NUM=1
			fi
			CHANNEL="$target_channel"
		fi
	fi

	compute_full_version
	if [[ $FULL_VERSION == "$current_full" ]]; then
		echo "Version $FULL_VERSION is already current; nothing to do." >&2
		exit 1
	fi
	log "Releasing $FULL_VERSION"

	do_write_version
	log "Updated version source"

	run_release_steps
	log "Release steps done"

	do_post_version
	log "Post-version hook done"

	(cd "$ROOT_DIR" && nix fmt)
	log "Formatted files"

	git add -A
	local commit_msg="chore(release): v$FULL_VERSION"
	validate_commit_message "$commit_msg"
	git commit -m "$commit_msg"
	log "Created commit"

	git tag "$FULL_TAG"
	CREATED_TAG="$FULL_TAG"
	log "Tagged $FULL_TAG"

	git push
	git push --tags
	log "Done — released $FULL_TAG"

	trap - ERR
}

main "$@"
