#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
# shellcheck disable=SC2001,SC2034,SC2181,SC2317
# =============================================================================
# Version Manager — Changelog Functions
# =============================================================================
# Changelog reading, generation, and update functions extracted from
# version-manager.sh to reduce file size.
#
# Covers:
#   - Reading/checking CHANGELOG.md state
#   - Generating changelog entries from conventional commits
#   - Updating CHANGELOG.md during release
#
# Usage: source "${SCRIPT_DIR}/version-manager-changelog.sh"
#
# Dependencies:
#   - shared-constants.sh (print_error, print_info, print_success, print_warning)
#   - REPO_ROOT and VERSION_FILE must be set by the orchestrator
#
# Part of aidevops framework: https://aidevops.sh

# Apply strict mode only when executed directly (not when sourced)
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail

# Include guard
[[ -n "${_VERSION_MANAGER_CHANGELOG_LOADED:-}" ]] && return 0
_VERSION_MANAGER_CHANGELOG_LOADED=1

if [[ -z "${SCRIPT_DIR:-}" ]]; then
	_lib_path="${BASH_SOURCE[0]%/*}"
	[[ "$_lib_path" == "${BASH_SOURCE[0]}" ]] && _lib_path="."
	SCRIPT_DIR="$(cd "$_lib_path" && pwd)"
	unset _lib_path
fi

# --- Functions ---

# Function to check changelog has entry for version
check_changelog_version() {
	local version="$1"
	local changelog_file="$REPO_ROOT/CHANGELOG.md"

	if [[ ! -f "$changelog_file" ]]; then
		print_warning "CHANGELOG.md not found"
		return 1
	fi

	# Check if version entry exists
	if grep -q "^\## \[$version\]" "$changelog_file"; then
		print_success "CHANGELOG.md: $version ✓"
		return 0
	else
		print_error "CHANGELOG.md missing entry for version $version"
		return 1
	fi
}

# Function to check changelog has unreleased content
check_changelog_unreleased() {
	local changelog_file="$REPO_ROOT/CHANGELOG.md"

	if [[ ! -f "$changelog_file" ]]; then
		print_warning "CHANGELOG.md not found"
		return 1
	fi

	# Check if [Unreleased] section exists
	if ! grep -q "^\## \[Unreleased\]" "$changelog_file"; then
		print_error "CHANGELOG.md missing [Unreleased] section"
		return 1
	fi

	# Check if there's content under [Unreleased]
	local unreleased_content
	unreleased_content=$(sed -n '/^## \[Unreleased\]/,/^## \[/p' "$changelog_file" | grep -v "^##" | grep -v "^$" | head -5 || true)

	if [[ -z "$unreleased_content" ]]; then
		print_warning "CHANGELOG.md [Unreleased] section is empty"
		return 1
	fi

	print_success "CHANGELOG.md has unreleased content ✓"
	return 0
}

# Function to get releasable commit subjects since last tag
# Excludes release bump commits which are generated by this script.
get_releasable_commit_subjects() {
	local prev_tag
	prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

	local commits
	if [[ -n "$prev_tag" ]]; then
		commits=$(git log "$prev_tag"..HEAD --pretty=format:"%s" 2>/dev/null)
	else
		commits=$(git log --oneline -20 --pretty=format:"%s" 2>/dev/null)
	fi

	while IFS= read -r commit; do
		[[ -z "$commit" ]] && continue
		[[ "$commit" == chore\(release\):* ]] && continue
		echo "$commit"
	done <<<"$commits"

	return 0
}

# Function to generate changelog preview from commits
generate_changelog_preview() {
	local prev_tag
	prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

	local version
	version=$(get_current_version)

	echo "## [$version] - $(date +%Y-%m-%d)"
	echo ""

	# Categorize commits
	local added="" changed="" fixed="" security=""

	local commits
	if [[ -n "$prev_tag" ]]; then
		commits=$(git log "$prev_tag"..HEAD --pretty=format:"%s" 2>/dev/null)
	else
		commits=$(git log --oneline -20 --pretty=format:"%s" 2>/dev/null)
	fi

	while IFS= read -r commit; do
		case "$commit" in
		feat:* | feat\(*) added="$added\n- ${commit#feat: }" ;;
		fix:* | fix\(*) fixed="$fixed\n- ${commit#fix: }" ;;
		security:*) security="$security\n- ${commit#security: }" ;;
		refactor:* | docs:* | chore:*) changed="$changed\n- $commit" ;;
		*) ;; # Ignore other commit types
		esac
	done <<<"$commits"

	[[ -n "$added" ]] && echo -e "### Added\n$added\n"
	[[ -n "$changed" ]] && echo -e "### Changed\n$changed\n"
	[[ -n "$fixed" ]] && echo -e "### Fixed\n$fixed\n"
	[[ -n "$security" ]] && echo -e "### Security\n$security\n"

	return 0
}

# Classify a single commit message into a changelog category.
# Appends the formatted entry to the appropriate category variable (passed by name).
# Arguments: commit message
# Outputs: sets caller's added/changed/fixed/security/removed/deprecated via printf to stdout
# Returns category:entry on stdout as "CATEGORY\tentry" for the caller to accumulate.
_classify_commit_to_category() {
	local commit="$1"
	local clean_msg="$commit"

	case "$commit" in
	feat:*)
		clean_msg="${commit#feat: }"
		printf 'added\t- %s\n' "$clean_msg"
		;;
	feat\(*\):*)
		clean_msg=$(echo "$commit" | sed 's/^feat([^)]*): //')
		printf 'added\t- %s\n' "$clean_msg"
		;;
	fix:*)
		clean_msg="${commit#fix: }"
		printf 'fixed\t- %s\n' "$clean_msg"
		;;
	fix\(*\):*)
		clean_msg=$(echo "$commit" | sed 's/^fix([^)]*): //')
		printf 'fixed\t- %s\n' "$clean_msg"
		;;
	security:*)
		clean_msg="${commit#security: }"
		printf 'security\t- %s\n' "$clean_msg"
		;;
	docs:*)
		clean_msg="${commit#docs: }"
		printf 'changed\t- Documentation: %s\n' "$clean_msg"
		;;
	refactor:*)
		clean_msg="${commit#refactor: }"
		printf 'changed\t- Refactor: %s\n' "$clean_msg"
		;;
	perf:*)
		clean_msg="${commit#perf: }"
		printf 'changed\t- Performance: %s\n' "$clean_msg"
		;;
	BREAKING\ CHANGE:* | breaking:*)
		clean_msg="${commit#BREAKING CHANGE: }"
		clean_msg="${clean_msg#breaking: }"
		printf 'removed\t- **BREAKING**: %s\n' "$clean_msg"
		;;
	deprecate:* | deprecated:*)
		clean_msg="${commit#deprecate: }"
		clean_msg="${clean_msg#deprecated: }"
		printf 'deprecated\t- %s\n' "$clean_msg"
		;;
	test:*)
		clean_msg="${commit#test: }"
		printf 'changed\t- Tests: %s\n' "$clean_msg"
		;;
	test\(*\):*)
		clean_msg=$(echo "$commit" | sed 's/^test([^)]*): //')
		printf 'changed\t- Tests: %s\n' "$clean_msg"
		;;
	ci:*)
		clean_msg="${commit#ci: }"
		printf 'changed\t- CI: %s\n' "$clean_msg"
		;;
	ci\(*\):*)
		clean_msg=$(echo "$commit" | sed 's/^ci([^)]*): //')
		printf 'changed\t- CI: %s\n' "$clean_msg"
		;;
	build:*)
		clean_msg="${commit#build: }"
		printf 'changed\t- Build: %s\n' "$clean_msg"
		;;
	build\(*\):*)
		clean_msg=$(echo "$commit" | sed 's/^build([^)]*): //')
		printf 'changed\t- Build: %s\n' "$clean_msg"
		;;
	chore:*)
		clean_msg="${commit#chore: }"
		printf 'changed\t- Maintenance: %s\n' "$clean_msg"
		;;
	chore\(*\):*)
		clean_msg=$(echo "$commit" | sed 's/^chore([^)]*): //')
		printf 'changed\t- Maintenance: %s\n' "$clean_msg"
		;;
	# Task-ID prefixed commits (e.g., t1741: plan autoresearch, GH#14391: tighten miniflare)
	# These use the commit description which already serves as the issue title.
	t[0-9]*:*)
		clean_msg=$(echo "$commit" | sed 's/^t[0-9][0-9]*[^:]*: //')
		printf 'changed\t- %s\n' "$clean_msg"
		;;
	GH\#[0-9]*:*)
		clean_msg=$(echo "$commit" | sed 's/^GH#[0-9]*: //')
		printf 'changed\t- %s\n' "$clean_msg"
		;;
	*) ;; # Ignore other commit types
	esac
	return 0
}

# Function to generate changelog content from commits (cleaner format)
generate_changelog_content() {
	local prev_tag
	prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")

	local commits
	if [[ -n "$prev_tag" ]]; then
		commits=$(git log "$prev_tag"..HEAD --pretty=format:"%s" 2>/dev/null)
	else
		commits=$(git log --oneline -20 --pretty=format:"%s" 2>/dev/null)
	fi

	# Categorize commits
	local added="" changed="" fixed="" security="" removed="" deprecated=""

	while IFS= read -r commit; do
		[[ -z "$commit" ]] && continue
		[[ "$commit" == chore\(release\):* ]] && continue
		# Skip task ID claim commits (internal bookkeeping, not user-facing)
		[[ "$commit" == "chore: claim t"* ]] && continue
		# Skip bare version bump commits that lack (release) scope
		[[ "$commit" == "chore: bump version to "* ]] && continue

		local category entry classified
		classified=$(_classify_commit_to_category "$commit")
		[[ -z "$classified" ]] && continue

		category="${classified%%	*}"
		entry="${classified#*	}"

		case "$category" in
		added) added="${added}${entry}\n" ;;
		changed) changed="${changed}${entry}\n" ;;
		fixed) fixed="${fixed}${entry}\n" ;;
		security) security="${security}${entry}\n" ;;
		removed) removed="${removed}${entry}\n" ;;
		deprecated) deprecated="${deprecated}${entry}\n" ;;
		esac
	done <<<"$commits"

	# Output in Keep a Changelog order
	[[ -n "$added" ]] && printf "### Added\n\n%b\n" "$added"
	[[ -n "$changed" ]] && printf "### Changed\n\n%b\n" "$changed"
	[[ -n "$deprecated" ]] && printf "### Deprecated\n\n%b\n" "$deprecated"
	[[ -n "$removed" ]] && printf "### Removed\n\n%b\n" "$removed"
	[[ -n "$fixed" ]] && printf "### Fixed\n\n%b\n" "$fixed"
	[[ -n "$security" ]] && printf "### Security\n\n%b\n" "$security"

	return 0
}

# Replace the [Unreleased] section in a changelog file with new version content.
# Reads the changelog, inserts the new version section after [Unreleased], and
# discards old unreleased content.
# Arguments: changelog_file content_file output_file
_replace_unreleased_section() {
	local changelog_file="$1"
	local content_file="$2"
	local output_file="$3"
	local in_unreleased=0

	while IFS= read -r line || [[ -n "$line" ]]; do
		if [[ "$line" == "## [Unreleased]"* ]]; then
			echo "## [Unreleased]"
			echo ""
			cat "$content_file"
			in_unreleased=1
		elif [[ "$line" == "## ["* ]] && [[ $in_unreleased -eq 1 ]]; then
			# Next version section found, stop skipping
			in_unreleased=0
			echo ""
			echo "$line"
		elif [[ $in_unreleased -eq 0 ]]; then
			echo "$line"
		fi
		# Skip lines while in_unreleased=1 (old unreleased content)
	done <"$changelog_file" >"$output_file"
	return 0
}

# Ensure CHANGELOG.md exists with the standard template.
# Arguments: changelog_file
_ensure_changelog_exists() {
	local changelog_file="$1"

	if [[ -f "$changelog_file" ]]; then
		return 0
	fi

	print_warning "CHANGELOG.md not found, creating new one"
	cat >"$changelog_file" <<'EOF'
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

EOF
	return 0
}

# Function to update CHANGELOG.md with new version
update_changelog() {
	local version="$1"
	local changelog_file="$REPO_ROOT/CHANGELOG.md"
	local today
	today=$(date +%Y-%m-%d)

	_ensure_changelog_exists "$changelog_file"

	# Check if [Unreleased] section exists
	if ! grep -q "^\## \[Unreleased\]" "$changelog_file"; then
		print_error "CHANGELOG.md missing [Unreleased] section"
		return 1
	fi

	# Generate changelog content from commits
	local changelog_content
	changelog_content=$(generate_changelog_content)

	if [[ -z "$changelog_content" ]]; then
		print_warning "No conventional commits found for changelog generation"
		changelog_content="### Changed

- Version bump and maintenance updates
"
	fi

	# Create temp files for the update with trap cleanup
	local temp_file content_file
	temp_file=$(mktemp)
	content_file=$(mktemp)
	# shellcheck disable=SC2064
	trap "rm -f '$temp_file' '$content_file'" EXIT

	# Write the new version section to a temp file (avoids awk multiline issues)
	cat >"$content_file" <<EOF
## [$version] - $today

$changelog_content
EOF

	_replace_unreleased_section "$changelog_file" "$content_file" "$temp_file"

	# Replace original file
	mv "$temp_file" "$changelog_file"
	rm -f "$content_file"
	trap - EXIT

	print_success "Updated CHANGELOG.md: [Unreleased] → [$version] - $today"
	return 0
}
