#!/bin/sh
#
# git-issue-lib - Shared functions for git-issue commands
#
# Source this file from git-issue commands:
#   . "$(dirname "$0")/git-issue-lib"
#

# Resolve a short issue ID to full ref(s)
# Uses git's own ref glob matching for O(1) lookup
resolve_issue() {
	_ri_prefix="$1"
	git for-each-ref --format='%(refname)' "refs/issues/$_ri_prefix*"
}

# Validate that a string contains no newlines
validate_no_newlines() {
	case "$1" in
		*"
"*)
			echo "error: $2 must not contain newlines" >&2
			exit 1
			;;
	esac
}

# Normalize and validate a comma-separated label string
# Args: $1 = raw label string (e.g., "bug, feature, bug")
# Output: normalized string (e.g., "bug, feature") to stdout
# Exits with error if any label contains a comma
normalize_labels() {
	_raw="$1"

	# Empty input is valid (no labels)
	test -z "$_raw" && return 0

	# Save/restore IFS for comma splitting
	_old_ifs="$IFS"
	IFS=','

	_normalized=""
	_seen="" # Track seen labels for deduplication

	for _label in $_raw
	do
		# Trim leading/trailing whitespace
		_label="$(printf '%s' "$_label" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"

		# Skip empty labels
		test -z "$_label" && continue

		# Validate: label must not contain comma
		# (This check is on trimmed individual label, not the original input)
		case "$_label" in
			*,*)
				echo "error: label '$_label' contains a comma (use multiple -l flags instead)" >&2
				IFS="$_old_ifs"
				exit 1
				;;
		esac

		# Check if already seen (case-sensitive deduplication per spec 4.7)
		_duplicate=0
		_check_ifs="$IFS"
		IFS=','
		for _seen_label in $_seen
		do
			if test "$_seen_label" = "$_label"
			then
				_duplicate=1
				break
			fi
		done
		IFS="$_check_ifs"

		# Add to result if not duplicate
		if test "$_duplicate" -eq 0
		then
			if test -n "$_normalized"
			then
				_normalized="$_normalized, $_label"
			else
				_normalized="$_label"
			fi

			# Track as seen
			if test -n "$_seen"
			then
				_seen="$_seen,$_label"
			else
				_seen="$_label"
			fi
		fi
	done

	IFS="$_old_ifs"
	printf '%s' "$_normalized"
}

# Validate git user configuration before creating commits
# Exits with code 128 if user.name or user.email is not set
validate_git_identity() {
	_name="${GIT_AUTHOR_NAME:-${GIT_COMMITTER_NAME:-$(git config user.name 2>/dev/null || true)}}"
	_email="${GIT_AUTHOR_EMAIL:-${GIT_COMMITTER_EMAIL:-$(git config user.email 2>/dev/null || true)}}"

	if test -z "$_name"
	then
		printf 'fatal: user.name not set\n' >&2
		printf '       run: git config user.name "Your Name"\n' >&2
		exit 128
	fi

	if test -z "$_email"
	then
		printf 'fatal: user.email not set\n' >&2
		printf '       run: git config user.email "you@example.com"\n' >&2
		exit 128
	fi
}

# Validate email format for assignee
# Args: $1 = email string, $2 = field name (for error messages)
# Returns: 0 if valid, exits with error if invalid
validate_email() {
	_email="$1"
	_field="${2:-assignee}"

	# Allow empty (unassigning)
	test -z "$_email" && return 0

	# Basic email format: something@something.something
	# This is permissive - accepts most common email formats
	case "$_email" in
		*@*.*)
			# Has @ and at least one dot after @
			# Check there's something before @
			_local="${_email%%@*}"
			if test -z "$_local"
			then
				printf 'error: %s email missing local part (before @)\n' "$_field" >&2
				exit 1
			fi
			# Check there's something between @ and .
			_rest="${_email#*@}"
			_domain="${_rest%%.*}"
			if test -z "$_domain"
			then
				printf 'error: %s email missing domain (between @ and .)\n' "$_field" >&2
				exit 1
			fi
			# Check there's something after final .
			_tld="${_email##*.}"
			if test -z "$_tld" || test "$_tld" = "$_email"
			then
				printf 'error: %s email missing TLD (after final .)\n' "$_field" >&2
				exit 1
			fi
			return 0
			;;
		*)
			printf 'error: %s must be a valid email address (format: user@domain.com)\n' "$_field" >&2
			exit 1
			;;
	esac
}

# Update a ref with CAS retry on concurrent modification
# Usage: update_ref_with_retry <ref> <new_sha> <expected_old_sha>
# Returns: 0 on success, 1 on persistent conflict, 128 on error
update_ref_with_retry() {
	_ref="$1"
	_new_sha="$2"
	_expected_old="$3"

	_max_attempts=3
	_attempt=1

	while test "$_attempt" -le "$_max_attempts"
	do
		# Get current ref value (may have changed since expected)
		_current="$(git rev-parse "$_ref" 2>/dev/null || true)"

		if test -z "$_current"
		then
			printf 'error: ref %s disappeared during update\n' "$_ref" >&2
			return 128
		fi

		# On retry, use current value for CAS (not original expected)
		if test "$_attempt" -gt 1
		then
			_expected_old="$_current"
		fi

		# Attempt CAS update
		if git update-ref -- "$_ref" "$_new_sha" "$_expected_old" 2>/dev/null
		then
			# Success
			return 0
		fi

		# CAS failed - check if ref changed
		_after="$(git rev-parse "$_ref" 2>/dev/null || true)"

		if test "$_after" = "$_expected_old"
		then
			# Ref didn't change but update-ref failed (unexpected error)
			printf 'error: git update-ref failed unexpectedly\n' >&2
			return 128
		fi

		# Ref changed concurrently - retry if we have attempts left
		if test "$_attempt" -lt "$_max_attempts"
		then
			# Exponential backoff: 100ms, 200ms
			_sleep_ms=$((100 * (1 << (_attempt - 1))))

			# POSIX-portable sleep (use Perl if available for precision)
			if command -v perl >/dev/null 2>&1
			then
				perl -e "select(undef, undef, undef, $_sleep_ms / 1000)" 2>/dev/null || sleep 1
			else
				sleep 1
			fi

			_attempt=$((_attempt + 1))
		else
			# Out of retries
			_short_id="$(printf '%s' "$_ref" | sed 's|refs/issues/||' | cut -c1-7)"
			printf 'error: concurrent modification detected\n' >&2
			printf '       issue was modified by another process\n' >&2
			printf '       run git issue show %s to see current state\n' "$_short_id" >&2
			return 1
		fi
	done

	# Unreachable
	return 128
}

# Get the path to a platform-specific user cache file
# Stored inside .git/ so it travels with the repo's local state
# Args: $1 = platform name (e.g., "github", "gitlab", "gitea")
# Output: absolute path to cache file
get_user_cache_path() {
	_platform="$1"
	_git_dir="$(git rev-parse --git-dir 2>/dev/null)"
	printf '%s/issue-user-cache.%s' "$_git_dir" "$_platform"
}

# Cache a platform user mapping (email <-> platform login)
# Args: $1 = email, $2 = login, $3 = user_id, $4 = platform
cache_platform_user() {
	_cpu_email="$1"
	_cpu_login="$2"
	_cpu_id="$3"
	_cpu_platform="$4"

	_cpu_cache="$(get_user_cache_path "$_cpu_platform")"
	_cpu_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date +"%Y-%m-%dT%H:%M:%SZ")"

	# Remove stale entry for this email (if any)
	if test -f "$_cpu_cache"
	then
		_cpu_tmp="${_cpu_cache}.tmp"
		grep -v "^${_cpu_email}," "$_cpu_cache" > "$_cpu_tmp" 2>/dev/null || true
		mv "$_cpu_tmp" "$_cpu_cache"
	fi

	printf '%s,%s,%s,%s\n' "$_cpu_email" "$_cpu_login" "$_cpu_id" "$_cpu_ts" >> "$_cpu_cache"
}

# Lookup a platform login from a cached email
# Args: $1 = email, $2 = platform
# Output: login to stdout (empty if not found)
lookup_cached_login() {
	_lcl_email="$1"
	_lcl_platform="$2"
	_lcl_cache="$(get_user_cache_path "$_lcl_platform")"

	if test -f "$_lcl_cache"
	then
		_lcl_entry="$(grep "^${_lcl_email}," "$_lcl_cache" 2>/dev/null | tail -1)"
		if test -n "$_lcl_entry"
		then
			printf '%s' "$_lcl_entry" | cut -d, -f2
			return 0
		fi
	fi
	return 0
}

# Lookup a cached user ID from an email
# Args: $1 = email, $2 = platform
# Output: user_id to stdout (empty if not found)
lookup_cached_user_id() {
	_lci_email="$1"
	_lci_platform="$2"
	_lci_cache="$(get_user_cache_path "$_lci_platform")"

	if test -f "$_lci_cache"
	then
		_lci_entry="$(grep "^${_lci_email}," "$_lci_cache" 2>/dev/null | tail -1)"
		if test -n "$_lci_entry"
		then
			printf '%s' "$_lci_entry" | cut -d, -f3
			return 0
		fi
	fi
	return 0
}

# Get the effective title for an issue ref
# Checks for Title: trailer override, falls back to root commit subject
get_issue_title() {
	_git_ref="$1"
	_title_override="$(git log --format='%(trailers:key=Title,valueonly)' "$_git_ref" | \
		sed '/^$/d' | head -1)"
	_title_override="$(printf '%s' "$_title_override" | sed 's/^[[:space:]]*//')"

	if test -n "$_title_override"
	then
		printf '%s' "$_title_override"
	else
		_root="$(git rev-list --max-parents=0 "$_git_ref")"
		git log -1 --format='%s' "$_root"
	fi
}
