#!/usr/bin/env bash

# Author: Santhosh Siva
# Date Created: 03-08-2025

# Colors
BLUE=$(tput setaf 4)
PROMPT=$(tput setaf 3)
GREEN=$(tput setaf 2)
RED=$(tput setaf 1)
NC=$(tput sgr0)

overwrite() { echo -e "\r\033[1A\033[0K$@"; }

default_spaces="    "

print_message() {
	local number=$2
	local message=$1

	if [ -z "$message" ]; then
		message=""
	fi

	if [ -z "$number" ]; then
		number=0
	fi

	# -1 means no indentation (flush left)
	if [ "$number" -eq -1 ]; then
		echo -e "$message"
		return 0
	fi

	if [ "$number" -eq 0 ]; then
		echo -e "   $message"
		return 0
	fi

	# print_message ""

	if [ "$number" -lt 10 ]; then
		printf "%d. %s\n" "$number" "$message"
		return 0
	fi

	printf "%02d. %s\n" "$number" "$message"
}

indent() {
	local prefix="   │ "
	sed "s/^/${prefix}/"
}

# Run a git command, capturing its output to a temp file and indenting it.
#
# Why not pipe git directly into indent? After fetch/pull, git spawns
# `git maintenance run --auto --detach`. That detached process inherits git's
# stdout — i.e. the write end of the pipe to indent's sed — and holds it open
# while it runs background gc/repack. sed then blocks waiting for EOF that
# never comes until maintenance finishes, so the whole command appears to hang.
# Writing to a file instead means the detached process inherits a file fd (no
# reader waiting on EOF), so we return immediately and maintenance runs async.
#
# Returns git's exit code (not sed's, as a direct `git ... | indent` would).
run_git_indented() {
	local tmp
	tmp=$(mktemp)
	git "$@" >"$tmp" 2>&1
	local rc=$?
	indent <"$tmp"
	rm -f "$tmp"
	return $rc
}

install_dependency() {
	local cmd=$1
	local package=$2

	if ! command -v "$cmd" >/dev/null; then
		if brew install "$package" >>"$log_file" 2>>"$error_log_file"; then
			return
		else
			print_message "" -1
			print_message "${RED}Failed to install $package.${NC}" -1
			exit 1
		fi
	fi
}

validate_dependencies() {
	for cmd in $@; do
		install_dependency "$cmd" "$cmd"
	done
}

print_banner() {
	print_message ""
	figlet -f slant "Gitsy" | lolcat
	print_message ""
}

prompt_user() {
	local default_to_yes=$1
	local message=$2
	local step_number=$3

	if [ -n "$step_number" ] && [ "$step_number" -ne 0 ]; then
		local prefix="$(printf "%${default_spaces}s")${step_number}. "
	elif [ "$step_number" -eq 0 ]; then
		local prefix="$(printf "%${default_spaces}s")   "
	else
		local prefix="$(printf "%${default_spaces}s")- "
	fi

	local prompt="${prefix}${PROMPT}${message}${NC} "

	if [ "$default_to_yes" = "true" ]; then
		echo -n "${prompt}(Y/n): " >&2
		read response
		response="${response:-y}"
	else
		echo -n "${prompt}(y/N): " >&2
		read response
		response="${response:-n}"
	fi

	case "$response" in
	[Yy]) echo "y" ;;
	[Nn]) echo "n" ;;
	*)
		print_message "" -1
		print_message "${RED}Invalid input.${NC}" -1
		exit 1
		;;
	esac
}

get_current_branch() {
	local -n result=$1
	result=$(git rev-parse --abbrev-ref HEAD 2>&1)
	local get_branch_exit=$?
	if [ $get_branch_exit -ne 0 ] || [ -z "$result" ]; then
		print_message "" -1
		print_message "${RED}Failed to get current branch. [Fail]${NC}" -1
		exit 1
	fi
	return 0
}

get_default_branch() {
	local -n result=$1
	local branch_result=""

	# Try to get from remote HEAD
	branch_result=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
	local exit_code=$?

	if [ $exit_code -eq 0 ] && [ -n "$branch_result" ]; then
		result="$branch_result"
		return 0
	fi

	# Check if main exists locally
	if git show-ref --verify --quiet "refs/heads/main"; then
		result="main"
		return 0
	fi

	# Check if master exists locally
	if git show-ref --verify --quiet "refs/heads/master"; then
		result="master"
		return 0
	fi

	# Fallback to main
	result="main"
	return 0
}

sanitize_branch_name() {
	local branch_name=$1
	local -n result=$2
	local max_length=${3:-30}  # Default max length is 30

	# Replace non-alphanumeric characters with underscores and convert to lowercase
	local sanitized
	sanitized=$(echo "$branch_name" | sed -E 's/[^[:alnum:]]/_/g' | tr '[:upper:]' '[:lower:]')
	local sanitize_exit=$?
	if [ $sanitize_exit -ne 0 ]; then
		return 1
	fi

	# Truncate if longer than max_length
	if [ ${#branch_name} -gt $max_length ]; then
		sanitized="${sanitized:0:$max_length}.."
	fi

	result="$sanitized"
	return 0
}

stash_changes() {
	local stash_changes=$1
	local step_number=$2
	local tag_message=$3

	if [ -z "$step_number" ]; then
		step_number=0
	fi

	if [ "$stash_changes" = "true" ]; then
		print_message "${BLUE}Stashing changes...${NC}" $step_number

		if ! (git -c color.ui=always add -A 2>&1 | indent); then
			print_message "" -1
			print_message "${RED}Failed to add changes to stash. [Fail]${NC}" -1
			exit 1
		fi

		local stash_message
		local branch_name
		branch_name=$(git rev-parse --abbrev-ref HEAD 2>&1)
		if [ $? -ne 0 ] || [ -z "$branch_name" ]; then
			print_message "" -1
			print_message "${RED}Failed to get current branch name for stash. [Fail]${NC}" -1
			exit 1
		fi
		local stash_date=$(date '+%Y-%m-%d %H:%M:%S')

		if [ -n "$tag_message" ]; then
			stash_message="Manual stash; Branch: ${branch_name}; Date: ${stash_date}; Message: ${tag_message}"
		else
			stash_message="Auto stash; Branch: ${branch_name}; Date: ${stash_date}"
		fi

		if ! (git -c color.ui=always stash push -m "$stash_message" 2>&1 | indent 4); then
			print_message "" -1
			print_message "${RED}Failed to stash changes. [Fail]${NC}" -1
			exit 1
		fi

		print_message "${GREEN}Changes stashed successfully.${NC}" 0
	fi
}

fetch_changes() {
	local target_branch=$1

	if [ -z "$target_branch" ]; then
		print_message "${RED}Target branch is not set. [Fail]${NC}" 0
		return 1
	fi

	if ! run_git_indented -c color.ui=always fetch origin "${target_branch}"; then
		print_message "${PROMPT}Auto-Fetch failed, Please do it manually.${NC}"
		return 1
	else
		return 0
	fi

	return 0
}

checkout_branch() {
	local target_branch=$1
	local new_branch=$2

	if [ -z "$target_branch" ]; then
		print_message "" -1
		print_message "${RED}Target branch is not set. [Fail]${NC}" -1
		exit 1
	fi

	if [ "$new_branch" = "true" ]; then
		if ! git -c color.ui=always checkout -b "${target_branch}" 2>&1 | indent; then
			print_message "" -1
			print_message "${RED}Failed to create new branch. [Fail]${NC}" -1
			exit 1
		fi
		print_message "${GREEN}Created new local branch ${NC}${target_branch}${GREEN}. [DONE]${NC}"
		return 0
	fi

	if ! git -c color.ui=always checkout "${target_branch}" 2>&1 | indent; then
		print_message "" -1
		print_message "${RED}Failed to checkout to branch ${NC}${target_branch}${RED}. [Fail]${NC}" -1
		exit 1
	fi
	return 0
}

create_worktree() {
	local target_branch="$1"
	local worktree_path="$2"
	local new_branch="$3"
	local base_branch
	local created_worktree_message="${GREEN}Successfully created worktree for branch ${NC}$target_branch${GREEN}. [DONE]${NC}"
	local failed_worktree_message="${RED}Failed to create worktree for branch ${NC}$target_branch${RED}. [Fail]${NC}"

	if [ -z "$target_branch" ]; then
		print_message "" -1
		print_message "${RED}Target branch is not set. [Fail]${NC}" -1
		exit 1
	fi
	if [ -z "$worktree_path" ]; then
		print_message "" -1
		print_message "${RED}Worktree path is not set. [Fail]${NC}" -1
		exit 1
	fi
	if [ -d "$worktree_path" ]; then
		print_message "" -1
		print_message "${RED}Worktree path already exists. [Fail]${NC}" -1
		exit 1
	fi

	local base_branch
	get_current_branch base_branch

	if [ "$new_branch" = "true" ]; then
		# Make sure the source branch exists remotely!
		if ! git rev-parse --verify "origin/$base_branch" >/dev/null 2>&1; then
			print_message "" -1
			print_message "${RED}Base branch ${NC}origin/$base_branch${RED} not found. Cannot create new branch.${NC}" -1
			exit 1
		fi

		# Use the -b flag on worktree directly:
		local worktree_output
		worktree_output=$(git -c color.ui=always worktree add -b "$target_branch" "$worktree_path" "origin/$base_branch" 2>&1)
		local worktree_exit_code=$?

		echo "$worktree_output" | indent

		if [ $worktree_exit_code -ne 0 ]; then
			print_message "${failed_worktree_message}"
			return 1
		fi

		print_message "${BLUE}Pushing new branch ${NC}$target_branch${BLUE} to remote...${NC}"
		if ! git -c color.ui=always push -u origin "$target_branch" 2>&1 | indent; then
			print_message "${RED}Failed to push new branch ${NC}$target_branch${RED} to remote. [Fail]${NC}"
			return 1
		fi

		print_message "${created_worktree_message}"
		copy_to_clipboard "cd ${worktree_path}" "cd ${worktree_path} copied to clipboard."
		return 0
	fi

	# Just add the worktree to an existing branch:
	local worktree_output
	worktree_output=$(git -c color.ui=always worktree add "$worktree_path" "$target_branch" 2>&1)
	local worktree_exit_code=$?

	echo "$worktree_output" | indent

	if [ $worktree_exit_code -ne 0 ]; then
		print_message "${failed_worktree_message}"
		return 1
	fi

	print_message "${created_worktree_message}"
	copy_to_clipboard "cd ${worktree_path}" "cd ${worktree_path} copied to clipboard."
	return 0
}

reset_to_target_branch() {
	local target_branch=$1
	local step_number=$2

	if [ -z "$step_number" ]; then
		step_number=0
	fi

	if [ -z "$target_branch" ]; then
		print_message "" -1
		print_message "${RED}Target branch is not set. [Fail]${NC}" -1
		exit 1
	fi

	print_message "${BLUE}Resetting to ${NC}origin/${target_branch}${BLUE}...${NC}" $step_number

	if ! git -c color.ui=always reset --hard "origin/${target_branch}" 2>&1 | indent; then
		print_message "" -1
		print_message "${RED}Failed to reset to ${NC}origin/${target_branch}${RED}. [Fail]${NC}" -1
		exit 1
	fi

	print_message "${GREEN}Reset to ${NC}origin/${target_branch}${GREEN} successfully.${NC}"
}

pull_changes() {
	local target_branch=$1
	local step_number=$2

	if [ -z "$step_number" ]; then
		step_number=0
	fi

	if [ -z "$target_branch" ]; then
		print_message "" -1
		print_message "${RED}Target branch is not set. [Fail]${NC}" -1
		exit 1
	fi

	print_message "${BLUE}Pulling changes from ${NC}remote/${target_branch}${BLUE}...${NC}" $step_number
	run_git_indented -c color.ui=always pull origin "${target_branch}"
	local pull_exit=$?
	if [ $pull_exit -ne 0 ]; then
		print_message "" -1
		if git diff --name-only --diff-filter=U 2>/dev/null | grep -q .; then
			print_message "${RED}Merge conflicts detected. Resolve conflicts and then commit. [Fail]${NC}" -1
		else
			print_message "${RED}Failed to pull changes from remote. [Fail]${NC}" -1
		fi
		exit 1
	fi
	print_message "${GREEN}Pulled changes from ${NC}remote/${target_branch} ${GREEN}successfully.${NC}"
}

push_changes() {
	local target_branch=$1
	local should_force_push=$2
	local step_number=$3

	if [ -z "$step_number" ]; then
		step_number=0
	fi

	if [ -z "$target_branch" ]; then
		print_message "" -1
		print_message "${RED}Target branch is not set. [Fail]${NC}" -1
		exit 1
	fi

	# Check if local branch is behind remote
	if [ "$should_force_push" = "false" ]; then
		# Check if the remote branch actually exists
		if branch_exists_on_remote "${target_branch}"; then
			fetch_changes "${target_branch}" >/dev/null 2>&1
			local local_commit=$(git rev-parse "${target_branch}" 2>/dev/null)
			local remote_commit=$(git rev-parse "origin/${target_branch}" 2>/dev/null)

			if [ -n "$local_commit" ] && [ -n "$remote_commit" ] && [ "$local_commit" != "$remote_commit" ]; then
				if ! git merge-base --is-ancestor "origin/${target_branch}" "${target_branch}" 2>/dev/null; then
					print_message "" -1
					print_message "${RED}Error: Your local branch is behind or has diverged from remote.${NC}" -1
					print_message "${RED}Use ${NC}--force${RED} flag if you want to force push.${NC}" -1
					exit 1
				fi
			fi
		fi
	fi

	if [ "$should_force_push" = "true" ]; then
		print_message "${BLUE}Force pushing changes to ${NC}remote/${target_branch}${BLUE}...${NC}" $step_number
		if ! git -c color.ui=always push --force origin "${target_branch}" 2>&1 | indent; then
			print_message "" -1
			print_message "${RED}Failed to force push changes to remote. [Fail]${NC}" -1
			exit 1
		fi
	else
		print_message "${BLUE}Pushing changes to ${NC}remote/${target_branch}${BLUE}...${NC}" $step_number
		if ! git -c color.ui=always push -u origin "${target_branch}" 2>&1 | indent; then
			print_message "" -1
			print_message "${RED}Failed to push changes to remote. [Fail]${NC}" -1
			exit 1
		fi
	fi
	print_message "${GREEN}Pushed changes to ${NC}remote/${target_branch} ${GREEN}successfully.${NC}"
}

already_on_branch() {
	local target_branch=$1
	local step_number=$2
	if [ -z "$step_number" ]; then
		step_number=1
	fi
	print_message "${BLUE}Checking current branch...${NC}" $step_number
	local current_branch
	get_current_branch current_branch
	if [ "${target_branch}" = "${current_branch}" ]; then
		print_message "${GREEN}Already on branch ${NC}${target_branch}${GREEN}. [DONE]${NC}"
		exit 1
	fi
}

check_if_target_branch_is_set() {
	local target_branch=$1
	if [ -z "${target_branch}" ]; then
		print_message "" -1
		print_message "${RED}Error: No target branch specified, use -t or --target-branch option.${NC}" -1
		exit 1
	fi
}

navigate_to_dir() {
	local dir=$1
	if [ -z "$dir" ]; then
		return 1
	fi
	local target
	target=$(cd "$dir" 2>/dev/null && pwd)
	if [ $? -ne 0 ]; then
		return 1
	fi
	if [ "$(pwd)" = "$target" ]; then
		return 0
	fi
	if ! cd "$target" 2>/dev/null; then
		return 1
	fi
	return 0
}

navigate_to_main_dir() {
	# Auto-detect main directory
	local current_dir=$(pwd)
	local current_dirname=$(basename "$current_dir")
	local parent_dirname=$(basename "$(dirname "$current_dir")")
	local should_exit_on_failure=$1

	# Already in main directory (but not nested main/main)
	if [ "$current_dirname" = "main" ] && [ "$parent_dirname" != "main" ]; then
		return 0
	fi

	# Try ../main
	if [ -d "../main" ]; then
		if navigate_to_dir "../main"; then
			return 0
		fi
	fi

	# Try ../../main
	if [ -d "../../main" ]; then
		if navigate_to_dir "../../main"; then
			return 0
		fi
	fi

	if [ "$should_exit_on_failure" = "true" ]; then
		print_message "" -1
		print_message "${RED}Failed to find main directory. [Fail]${NC}" -1
		exit 1
	fi

	return 1
}

check_uncommitted_changes() {
	if ! git diff-index --quiet HEAD --; then
		return 1
	fi
	return 0
}

is_git_repo() {
	local dir=$1
	local original_path=$PWD

	if ! navigate_to_dir "$dir"; then
		print_message "" -1
		print_message "${RED}Failed to navigate to directory: ${NC}${dir}${RED}. [Fail]${NC}" -1
		exit 1
	fi

	if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
		navigate_to_dir "$original_path"
		return 0
	fi

	navigate_to_dir "$original_path"
	return 1
}

has_uncommitted_changes() {
	local original_path=$PWD
	local worktree_path=$1

	if ! navigate_to_dir "$worktree_path"; then
		print_message "" -1
		print_message "${RED}Failed to navigate to worktree directory: ${NC}${worktree_path}${RED}. [Fail]${NC}" -1
		exit 1
	fi

	if ! is_git_repo "$worktree_path"; then
		return 1
	fi

	if ! check_uncommitted_changes; then
		if ! navigate_to_dir "$original_path"; then
			print_message "" -1
			print_message "${RED}Failed to navigate to original directory: ${NC}${original_path}${RED}. [Fail]${NC}" -1
			exit 1
		fi
		return 0
	fi

	if ! navigate_to_dir "$original_path"; then
		print_message "" -1
		print_message "${RED}Failed to navigate to original directory: ${NC}${original_path}${RED}. [Fail]${NC}" -1
		exit 1
	fi
	return 1
}

branch_exists_locally() {
	local branch=$1
	if git show-ref --verify --quiet "refs/heads/${branch}"; then
		return 0
	fi
	return 1
}

branch_exists_on_remote() {
	local branch=$1
	if git ls-remote --heads origin "${branch}" | grep -q "${branch}"; then
		return 0
	fi
	return 1
}

copy_to_clipboard() {
	local content="$1"
	local message="$2"

	if [ -n "$content" ]; then
		# Strip all ANSI escape sequences: colors, character sets, etc.
		printf '%s' "$content" | sed -E $'s/\033(\\[[0-9;]*[a-zA-Z]|\\([0-9A-B])//g' | pbcopy
	fi

	if [ -n "$message" ]; then
		print_message "${GREEN}${message}${NC}"
	fi
}

get_repo_name() {
	local -n result=$1

	# Try to get repo name from git remote
	local remote_url
	remote_url=$(git config --get remote.origin.url 2>/dev/null || true)
	if [ -n "$remote_url" ]; then
		# Extract repo name from URL (handles both https and ssh)
		result=$(echo "$remote_url" | sed -E 's#.*/([^/]+)(\.git)?$#\1#' | sed 's/\.git$//')
		local extract_exit=$?
		if [ $extract_exit -ne 0 ]; then
			return 1
		fi
		return 0
	fi

	# Fallback to directory name
	local toplevel
	toplevel=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
	result="${toplevel##*/}"
	return 0
}

sync_submodules() {
	local step_number=$1

	if [ -z "$step_number" ]; then
		step_number=0
	fi

	# Skip if no submodules exist
	if [ ! -f ".gitmodules" ] || ! grep -q '\[submodule' .gitmodules 2>/dev/null; then
		return 0
	fi

	print_message "${BLUE}Syncing submodules...${NC}" $step_number

	if ! git -c color.ui=always submodule update --init --recursive 2>&1 | indent; then
		print_message "" -1
		print_message "${RED}Failed to sync submodules. [Fail]${NC}" -1
		exit 1
	fi

	print_message "${GREEN}Submodules synced successfully.${NC}" 0
}

get_repo_info() {
	local -n git_root_ref=$1
	local -n current_dir_ref=$2
	local -n parent_dir_ref=$3
	local -n repo_name_ref=$4

	# Get git root
	git_root_ref=$(git rev-parse --show-toplevel)
	local git_root_exit=$?
	if [ $git_root_exit -ne 0 ] || [ -z "$git_root_ref" ]; then
		return 1
	fi

	# Get current directory name (basename of git root)
	current_dir_ref="${git_root_ref##*/}"

	# Get parent directory (dirname of git root, then basename of that)
	local parent_path="${git_root_ref%/*}"
	parent_dir_ref="${parent_path##*/}"

	# Get repo name using a local variable to avoid nameref-to-nameref issues
	local temp_repo_name
	get_repo_name temp_repo_name
	repo_name_ref="$temp_repo_name"
	return 0
}
