#!/bin/bash
#
# Synex Repo Manager
# Debian 13 / Trixie only
#

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${SCRIPT_DIR}/lib"
REPOS_DIR="${SCRIPT_DIR}/repos"

# Runtime paths
STATE_DIR="/var/lib/synex-repo-manager"
STATE_FILE="${STATE_DIR}/managed.list"
KEYRING_DIR="/etc/apt/keyrings"
SOURCES_DIR="/etc/apt/sources.list.d"

# i18n defaults
export TEXTDOMAIN="synex-repo-manager"
export TEXTDOMAINDIR="${TEXTDOMAINDIR:-/usr/share/locale}"

# shellcheck source=/dev/null
. "${LIB_DIR}/common.sh"

# Repo metadata
REPO_IDS=()
declare -A REPO_NAME REPO_DESCRIPTION REPO_CATEGORY REPO_KEY_URL REPO_KEY_FILE
declare -A REPO_KEY_MODE REPO_LIST_FILE REPO_SOURCE_LINES REPO_NOTES

# Session changes
PENDING_ADD=()
PENDING_REMOVE=()

CURRENT_SEARCH=""
PAGE_SIZE=10

init_runtime()
{
	mkdir -p "${STATE_DIR}" "${KEYRING_DIR}" "${SOURCES_DIR}"
	touch "${STATE_FILE}"
	sync_managed_state
}

check_platform()
{
	return 0
}

load_repo_definitions()
{
	local file
	local repo_id
	local repo_name
	local repo_description
	local repo_category
	local repo_key_url
	local repo_key_file
	local repo_key_mode
	local repo_list_file
	local repo_source_lines
	local repo_notes

	for file in "${REPOS_DIR}"/*.repo; do
		[ -f "$file" ] || continue

		repo_id=""
		repo_name=""
		repo_description=""
		repo_category=""
		repo_key_url=""
		repo_key_file=""
		repo_key_mode=""
		repo_list_file=""
		repo_source_lines=""
		repo_notes=""

		# shellcheck source=/dev/null
		. "$file"

		if [ -z "$repo_id" ] || [ -z "$repo_name" ] || [ -z "$repo_key_url" ] || [ -z "$repo_key_file" ] || [ -z "$repo_list_file" ] || [ -z "$repo_source_lines" ]; then
			print_warning "$(printf "$(_t 'Skipping invalid repository definition: %s')" "$(basename "$file")")"
			continue
		fi

		REPO_IDS+=("$repo_id")
		REPO_NAME["$repo_id"]="$repo_name"
		REPO_DESCRIPTION["$repo_id"]="$repo_description"
		REPO_CATEGORY["$repo_id"]="$repo_category"
		REPO_KEY_URL["$repo_id"]="$repo_key_url"
		REPO_KEY_FILE["$repo_id"]="$repo_key_file"
		REPO_KEY_MODE["$repo_id"]="${repo_key_mode:-ascii}"
		REPO_LIST_FILE["$repo_id"]="$repo_list_file"
		REPO_SOURCE_LINES["$repo_id"]="$repo_source_lines"
		REPO_NOTES["$repo_id"]="${repo_notes:-}"
	done
}

repo_exists()
{
	local repo_id="$1"
	local current_id

	for current_id in "${REPO_IDS[@]}"; do
		[ "$current_id" = "$repo_id" ] && return 0
	done

	return 1
}

is_managed_repo()
{
	local repo_id="$1"
	grep -qxF "$repo_id" "$STATE_FILE" 2>/dev/null
}

sync_managed_state()
{
	local tmp_file
	local repo_id

	tmp_file="$(mktemp)"
	while IFS= read -r repo_id; do
		[ -n "$repo_id" ] || continue
		if repo_exists "$repo_id"; then
			if [ -f "${REPO_LIST_FILE[$repo_id]}" ] && [ -f "${REPO_KEY_FILE[$repo_id]}" ]; then
				printf '%s\n' "$repo_id" >> "$tmp_file"
			fi
		fi
	done < "$STATE_FILE"

	sort -u "$tmp_file" > "$STATE_FILE"
	rm -f "$tmp_file"
}

add_managed_repo()
{
	local repo_id="$1"
	printf '%s\n' "$repo_id" >> "$STATE_FILE"
	sort -u "$STATE_FILE" -o "$STATE_FILE"
}

remove_managed_repo()
{
	local repo_id="$1"
	local tmp_file

	tmp_file="$(mktemp)"
	grep -vxF "$repo_id" "$STATE_FILE" > "$tmp_file" || true
	mv "$tmp_file" "$STATE_FILE"
}

is_pending_add()
{
	local repo_id="$1"
	local item
	for item in "${PENDING_ADD[@]}"; do
		[ "$item" = "$repo_id" ] && return 0
	done
	return 1
}

is_pending_remove()
{
	local repo_id="$1"
	local item
	for item in "${PENDING_REMOVE[@]}"; do
		[ "$item" = "$repo_id" ] && return 0
	done
	return 1
}

append_pending_add()
{
	local repo_id="$1"
	is_pending_add "$repo_id" || PENDING_ADD+=("$repo_id")
}

remove_pending_add()
{
	local repo_id="$1"
	local item
	local new_items=()
	for item in "${PENDING_ADD[@]}"; do
		[ "$item" = "$repo_id" ] || new_items+=("$item")
	done
	PENDING_ADD=("${new_items[@]}")
}

append_pending_remove()
{
	local repo_id="$1"
	is_pending_remove "$repo_id" || PENDING_REMOVE+=("$repo_id")
}

remove_pending_remove()
{
	local repo_id="$1"
	local item
	local new_items=()
	for item in "${PENDING_REMOVE[@]}"; do
		[ "$item" = "$repo_id" ] || new_items+=("$item")
	done
	PENDING_REMOVE=("${new_items[@]}")
}

get_sorted_repo_ids()
{
	local repo_id
	for repo_id in "${REPO_IDS[@]}"; do
		printf '%s\t%s\n' "${REPO_NAME[$repo_id]}" "$repo_id"
	done | LC_ALL=C sort -f | awk -F '\t' '{print $2}'
}

build_available_add_list()
{
	local repo_id
	local query="$1"
	for repo_id in $(get_sorted_repo_ids); do
		if is_managed_repo "$repo_id" && ! is_pending_remove "$repo_id"; then
			continue
		fi
		if is_pending_add "$repo_id"; then
			printf '%s\n' "$repo_id"
			continue
		fi
		if [ -n "$query" ]; then
			if ! printf '%s %s %s\n' "${REPO_NAME[$repo_id]}" "${REPO_DESCRIPTION[$repo_id]}" "${REPO_CATEGORY[$repo_id]}" | grep -iq -- "$query"; then
				continue
			fi
		fi
		printf '%s\n' "$repo_id"
	done
}

build_available_remove_list()
{
	local repo_id
	local query="$1"
	while IFS= read -r repo_id; do
		[ -n "$repo_id" ] || continue
		repo_exists "$repo_id" || continue
		if [ -n "$query" ]; then
			if ! printf '%s %s %s\n' "${REPO_NAME[$repo_id]}" "${REPO_DESCRIPTION[$repo_id]}" "${REPO_CATEGORY[$repo_id]}" | grep -iq -- "$query"; then
				continue
			fi
		fi
		printf '%s\n' "$repo_id"
	done < <(LC_ALL=C sort -f "$STATE_FILE")
}

show_selector()
{
	local mode="$1"
	local title="$2"
	local total_pages=1
	local current_page=1
	local query=""
	local -a items=()
	local total_items=0
	local start_index=0
	local end_index=0
	local idx=0
	local item_no=0
	local item_id
	local status_mark
	local option
	local selected_index

	while true; do
		items=()
		if [ "$mode" = "add" ]; then
			while IFS= read -r item_id; do
				[ -n "$item_id" ] && items+=("$item_id")
			done < <(build_available_add_list "$query")
		else
			while IFS= read -r item_id; do
				[ -n "$item_id" ] && items+=("$item_id")
			done < <(build_available_remove_list "$query")
		fi

		total_items="${#items[@]}"
		if [ "$total_items" -eq 0 ]; then
			total_pages=1
		else
			total_pages=$(( (total_items + PAGE_SIZE - 1) / PAGE_SIZE ))
		fi
		if [ "$current_page" -gt "$total_pages" ]; then
			current_page="$total_pages"
		fi
		if [ "$current_page" -lt 1 ]; then
			current_page=1
		fi

		start_index=$(( (current_page - 1) * PAGE_SIZE ))
		end_index=$(( start_index + PAGE_SIZE - 1 ))
		if [ "$end_index" -ge "$total_items" ]; then
			end_index=$(( total_items - 1 ))
		fi

		clear
		show_header "$title"
		echo ""
		if [ -n "$query" ]; then
			echo "$(printf "$(_t 'Search: %s')" "$query")"
			echo ""
		fi

		if [ "$total_items" -eq 0 ]; then
			if [ "$mode" = "add" ]; then
				print_info "$(_t 'No repositories available with the current filter.')"
			else
				print_info "$(_t 'There are no managed repositories to remove.')"
			fi
		else
			item_no=1
			for (( idx=start_index; idx<=end_index; idx++ )); do
				item_id="${items[$idx]}"
				status_mark="[ ]"
				if [ "$mode" = "add" ]; then
					is_pending_add "$item_id" && status_mark="[x]"
				else
					is_pending_remove "$item_id" && status_mark="[x]"
				fi
				printf '  %d) %s %s\n' "$item_no" "$status_mark" "${REPO_NAME[$item_id]}"
				printf '      %s\n' "${REPO_DESCRIPTION[$item_id]}"
				item_no=$(( item_no + 1 ))
			done
		fi

		echo ""
		printf '%s\n' "$(printf "$(_t 'Page %s of %s')" "$current_page" "$total_pages")"
		echo ""
		echo "  /  - $(_t 'Search')"
		echo "  c  - $(_t 'Clear search')"
		echo "  n  - $(_t 'Next page')"
		echo "  p  - $(_t 'Previous page')"
		echo "  a  - $(_t 'Accept selection')"
		echo "  0  - $(_t 'Back')"
		echo ""

		read_menu_option "$(_t 'Choose an option or toggle a number: ')"
		option="$MENU_INPUT"

		case "$option" in
			0)
				return 0
				;;
			/)
				read -r -p "$(_t 'Search text: ')" query || true
				current_page=1
				;;
			c|C)
				query=""
				current_page=1
				;;
			n|N)
				if [ "$current_page" -lt "$total_pages" ]; then
					current_page=$(( current_page + 1 ))
				fi
				;;
			p|P)
				if [ "$current_page" -gt 1 ]; then
					current_page=$(( current_page - 1 ))
				fi
				;;
			a|A)
				return 0
				;;
			'' )
				;;
			*[!0-9]*)
				print_warning "$(_t 'Invalid option.')"
				pause_execution
				;;
			*)
				selected_index=$(( start_index + option - 1 ))
				if [ "$selected_index" -lt "$start_index" ] || [ "$selected_index" -gt "$end_index" ] || [ "$selected_index" -ge "$total_items" ]; then
					print_warning "$(_t 'Invalid option.')"
					pause_execution
					continue
				fi
				item_id="${items[$selected_index]}"
				if [ "$mode" = "add" ]; then
					if is_pending_add "$item_id"; then
						remove_pending_add "$item_id"
					else
						remove_pending_remove "$item_id"
						append_pending_add "$item_id"
					fi
				else
					if is_pending_remove "$item_id"; then
						remove_pending_remove "$item_id"
					else
						remove_pending_add "$item_id"
						append_pending_remove "$item_id"
					fi
				fi
				;;
		esac
	done
}

show_pending_changes()
{
	local repo_id
	clear
	show_header "$(_t 'Review pending changes')"
	echo ""

	if [ "${#PENDING_ADD[@]}" -eq 0 ] && [ "${#PENDING_REMOVE[@]}" -eq 0 ]; then
		print_info "$(_t 'There are no pending changes.')"
		pause_execution
		return
	fi

	if [ "${#PENDING_ADD[@]}" -gt 0 ]; then
		echo "$(_t 'Repositories to add:')"
		for repo_id in "${PENDING_ADD[@]}"; do
			printf '  [ADD] %s - %s\n' "${REPO_NAME[$repo_id]}" "${REPO_DESCRIPTION[$repo_id]}"
		done
		echo ""
	fi

	if [ "${#PENDING_REMOVE[@]}" -gt 0 ]; then
		echo "$(_t 'Repositories to remove:')"
		for repo_id in "${PENDING_REMOVE[@]}"; do
			printf '  [REMOVE] %s - %s\n' "${REPO_NAME[$repo_id]}" "${REPO_DESCRIPTION[$repo_id]}"
		done
		echo ""
	fi

	print_warning "$(_t 'Removing a repository does not uninstall packages already installed from it.')"
	pause_execution
}

download_file()
{
	local url="$1"
	local destination="$2"

	if command_exists curl; then
		curl -fsSL "$url" -o "$destination"
	else
		wget -qO "$destination" "$url"
	fi
}

install_repo()
{
	local repo_id="$1"
	local tmp_file
	local key_mode

	print_info "$(printf "$(_t 'Adding repository: %s')" "${REPO_NAME[$repo_id]}")"

	tmp_file="$(mktemp)"
	download_file "${REPO_KEY_URL[$repo_id]}" "$tmp_file"

	key_mode="${REPO_KEY_MODE[$repo_id]}"
	case "$key_mode" in
		dearmor)
			gpg --dearmor --batch --yes -o "${REPO_KEY_FILE[$repo_id]}" "$tmp_file"
			chmod 0644 "${REPO_KEY_FILE[$repo_id]}"
			;;
		ascii)
			install -m 0644 "$tmp_file" "${REPO_KEY_FILE[$repo_id]}"
			;;
		*)
			rm -f "$tmp_file"
			print_error "$(printf "$(_t 'Unsupported key mode for %s')" "$repo_id")"
			return 1
			;;
	esac

	printf '%s\n' "${REPO_SOURCE_LINES[$repo_id]}" > "${REPO_LIST_FILE[$repo_id]}"
	chmod 0644 "${REPO_LIST_FILE[$repo_id]}"
	add_managed_repo "$repo_id"
	rm -f "$tmp_file"

	if [ -n "${REPO_NOTES[$repo_id]}" ]; then
		print_info "${REPO_NOTES[$repo_id]}"
	fi
}

remove_repo()
{
	local repo_id="$1"

	print_info "$(printf "$(_t 'Removing repository: %s')" "${REPO_NAME[$repo_id]}")"
	[ ! -f "${REPO_LIST_FILE[$repo_id]}" ] || rm -f "${REPO_LIST_FILE[$repo_id]}"
	[ ! -f "${REPO_KEY_FILE[$repo_id]}" ] || rm -f "${REPO_KEY_FILE[$repo_id]}"
	remove_managed_repo "$repo_id"
}

apply_changes()
{
	local repo_id
	local changed=0

	clear
	show_header "$(_t 'Apply changes')"
	echo ""

	if [ "${#PENDING_ADD[@]}" -eq 0 ] && [ "${#PENDING_REMOVE[@]}" -eq 0 ]; then
		print_info "$(_t 'There are no pending changes.')"
		pause_execution
		return
	fi

	show_pending_changes
	if ! confirm_yes_no "$(_t 'Apply the pending changes now?')"; then
		return
	fi

	for repo_id in "${PENDING_REMOVE[@]}"; do
		remove_repo "$repo_id"
		changed=1
	done

	for repo_id in "${PENDING_ADD[@]}"; do
		install_repo "$repo_id"
		changed=1
	done

	if [ "$changed" -eq 1 ]; then
		print_info "$(_t 'Running apt update...')"
		apt-get update
	fi

	PENDING_ADD=()
	PENDING_REMOVE=()
	sync_managed_state
	print_success "$(_t 'Changes applied successfully.')"
	pause_execution
}

show_main_menu()
{
	while true; do
		clear
		show_header "$(_t 'Synex Repo Manager')"
		echo ""
		echo "  1) $(_t 'Add repositories')"
		echo "  2) $(_t 'Remove repositories')"
		echo "  3) $(_t 'Review pending changes')"
		echo "  4) $(_t 'Apply changes')"
		echo "  5) $(_t 'Exit')"
		echo ""
		read_menu_option "$(_t 'Select an option: ')"

		case "$MENU_INPUT" in
			1)
				show_selector "add" "$(_t 'Add repositories')"
				;;
			2)
				show_selector "remove" "$(_t 'Remove repositories')"
				;;
			3)
				show_pending_changes
				;;
			4)
				apply_changes
				;;
			5)
				exit 0
				;;
			*)
				print_warning "$(_t 'Invalid option.')"
				pause_execution
				;;
		esac
	done
}

main()
{
	require_root
	ensure_runtime_dependencies
	check_platform
	load_repo_definitions
	init_runtime
	show_main_menu
}

Handle_interrupt()
{
	echo ""
	print_warning "$(_t "Operation interrupted.")"
	exit 130
}

trap 'Handle_interrupt' INT TERM

main "$@"
