#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

repo_go_version() {
	sed -n 's/^go //p' "$SCRIPT_DIR/go.mod" 2>/dev/null | head -n 1
}

repo_node_version() {
	sed -n 's/.*"node": ">= *\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" 2>/dev/null | head -n 1
}

repo_pnpm_version() {
	sed -n 's/.*"packageManager": "pnpm@\([^"]*\)".*/\1/p' "$SCRIPT_DIR/package.json" 2>/dev/null | head -n 1
}

GO_VERSION="${GO_VERSION:-$(repo_go_version)}"
GO_VERSION="${GO_VERSION:-1.26.2}"
NODE_VERSION="${NODE_VERSION:-$(repo_node_version)}"
NODE_VERSION="${NODE_VERSION:-22.18.0}"
PNPM_VERSION="${PNPM_VERSION:-$(repo_pnpm_version)}"
PNPM_VERSION="${PNPM_VERSION:-10.33.0}"
NVM_VERSION="${NVM_VERSION:-v0.40.3}"

INSTALL_SYSTEM=1
INSTALL_GO=1
INSTALL_NODE=1
INSTALL_FRONTEND_DEPS=1
VERIFY_ONLY=0
WITH_CROSS_CGO=0
SHOW_MENU=0
VERIFY_REPORT_ONLY=0
VERIFY_HAD_ISSUES=0

usage() {
	cat <<EOF
Usage: ./configure.sh [options]

Installs and configures the local build requirements for this Gitea tree.

Options:
  --with-cross-cgo       Also install/configure cross-CGO toolchains for
                         sqlite builds targeting linux/armv7 and windows/amd64.
                         This is useful for "All Arch" + sqlite builds, but it
                         downloads noticeably larger packages.
  --skip-system          Do not install system packages.
  --skip-go              Do not install Go.
  --skip-node            Do not install NVM/Node/pnpm.
  --skip-frontend-deps   Do not run pnpm install --frozen-lockfile.
  --verify-only          Only check the current environment.
  --menu                 Show the interactive run menu.
  -h, --help             Show this help.

Environment overrides:
  GO_VERSION=$GO_VERSION
  NODE_VERSION=$NODE_VERSION
  PNPM_VERSION=$PNPM_VERSION
  NVM_VERSION=$NVM_VERSION
EOF
}

show_description() {
	cat <<EOF
Gitea build environment setup

This script prepares the local machine for the Gitea build used in this tree.
It can install or verify:
  - the required system build packages, including gcc, make, git, pkg-config,
    and the SQLite/PAM libraries;
  - git-lfs when this repository has Git LFS hooks installed;
  - Go $GO_VERSION, as required by go.mod;
  - Node $NODE_VERSION and pnpm $PNPM_VERSION, as required by package.json;
  - frontend dependencies with pnpm install --frozen-lockfile;
  - optional cross-CGO compilers for SQLite builds targeting linux/armv7
    and windows/amd64.

For automated runs, you can keep using CLI options, for example:
  ./configure.sh --verify-only
  ./configure.sh --with-cross-cgo

EOF
}

interactive_menu() {
	show_description
	cat <<EOF
Select install mode:
  1) Normal mode
  2) With cross cgo
  3) Verify only
  4) Quit

EOF
    #  Install the standard requirements: system packages, Go, Node, pnpm, and frontend deps.
    #  Normal mode plus cross-CGO toolchains for multi-arch SQLite builds.
    #  Install nothing; only check the current environment.

	while true; do
		printf 'Choose option [1-4]: '
		if ! read -r choice; then
			die "No menu option selected."
		fi

		case "$choice" in
			1)
				printf 'Selected: Normal\n'
				return
				;;
			2)
				WITH_CROSS_CGO=1
				printf 'Selected: With cross cgo\n'
				return
				;;
			3)
				VERIFY_ONLY=1
				INSTALL_SYSTEM=0
				INSTALL_GO=0
				INSTALL_NODE=0
				INSTALL_FRONTEND_DEPS=0
				VERIFY_REPORT_ONLY=1
				printf 'Selected: Verify only\n'
				return
				;;
			4)
				printf 'Canceled.\n'
				exit 0
				;;
			*)
				printf 'Invalid option: %s\n' "$choice"
				;;
		esac
	done
}

log() {
	printf '\n==> %s\n' "$*"
}

warn() {
	printf 'WARNING: %s\n' "$*" >&2
}

die() {
	printf 'ERROR: %s\n' "$*" >&2
	exit 1
}

command_exists() {
	command -v "$1" >/dev/null 2>&1
}

repo_has_git_lfs_hooks() {
	local hook
	for hook in pre-push post-checkout post-commit post-merge; do
		[ -f "$SCRIPT_DIR/.git/hooks/$hook" ] || continue
		if grep -q 'git-lfs' "$SCRIPT_DIR/.git/hooks/$hook"; then
			return 0
		fi
	done
	return 1
}

repo_requires_git_lfs() {
	repo_has_git_lfs_hooks
}

run_as_root() {
	if [ "$(id -u)" -eq 0 ]; then
		"$@"
	elif command_exists sudo; then
		sudo "$@"
	else
		die "This step needs root privileges. Install sudo or run the script as root."
	fi
}

fetch_to() {
	local url="$1"
	local output="$2"

	if command_exists curl; then
		curl -fL --retry 3 --retry-delay 2 -o "$output" "$url"
	elif command_exists wget; then
		wget -O "$output" "$url"
	else
		die "curl or wget is required to download $url"
	fi
}

version_ge() {
	local actual="$1"
	local required="$2"
	[ "$(printf '%s\n%s\n' "$required" "$actual" | sort -V | head -n 1)" = "$required" ]
}

append_profile_line() {
	local file="$1"
	local line="$2"

	touch "$file"
	if ! grep -qxF "$line" "$file"; then
		printf '\n%s\n' "$line" >>"$file"
	fi
}

ensure_go_path() {
	activate_go_path
	append_profile_line "$HOME/.profile" 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"'
	append_profile_line "$HOME/.bashrc" 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"'
}

activate_go_path() {
	export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"
}

install_system_packages() {
	log "Installing system packages"

	if command_exists apt-get; then
		local -a packages=(
			bash
			build-essential
			ca-certificates
			coreutils
			curl
			findutils
			gawk
			git
			gzip
			libpam0g-dev
			libsqlite3-dev
			make
			openssh-client
			perl
			pkg-config
			python3
			sed
			sqlite3
			tar
			unzip
			wget
			xz-utils
			zip
		)
		repo_requires_git_lfs && packages+=(git-lfs)
		if [ "$WITH_CROSS_CGO" -eq 1 ]; then
			packages+=(
				gcc-arm-linux-gnueabihf
				g++-arm-linux-gnueabihf
				gcc-mingw-w64-x86-64
				g++-mingw-w64-x86-64
			)
		fi
		run_as_root apt-get update
		run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
		return
	fi

	if command_exists dnf; then
		local -a packages=(
			bash
			ca-certificates
			coreutils
			curl
			findutils
			gawk
			gcc
			gcc-c++
			git
			gzip
			make
			openssh-clients
			pam-devel
			perl-core
			pkgconf-pkg-config
			python3
			sed
			sqlite
			sqlite-devel
			tar
			unzip
			wget
			xz
			zip
		)
		repo_requires_git_lfs && packages+=(git-lfs)
		run_as_root dnf install -y "${packages[@]}"
		[ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems."
		return
	fi

	if command_exists yum; then
		local -a packages=(
			bash
			ca-certificates
			coreutils
			curl
			findutils
			gawk
			gcc
			gcc-c++
			git
			gzip
			make
			openssh-clients
			pam-devel
			perl
			pkgconfig
			python3
			sed
			sqlite
			sqlite-devel
			tar
			unzip
			wget
			xz
			zip
		)
		repo_requires_git_lfs && packages+=(git-lfs)
		run_as_root yum install -y "${packages[@]}"
		[ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems."
		return
	fi

	if command_exists pacman; then
		local -a packages=(
			base-devel
			bash
			ca-certificates
			coreutils
			curl
			findutils
			gawk
			git
			gzip
			make
			openssh
			pam
			perl
			pkgconf
			python
			sed
			sqlite
			tar
			unzip
			wget
			xz
			zip
		)
		repo_requires_git_lfs && packages+=(git-lfs)
		run_as_root pacman -Sy --needed --noconfirm "${packages[@]}"
		[ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems."
		return
	fi

	if command_exists zypper; then
		local -a packages=(
			bash
			ca-certificates
			coreutils
			curl
			findutils
			gawk
			gcc
			gcc-c++
			git
			gzip
			make
			openssh
			pam-devel
			perl
			pkg-config
			python3
			sed
			sqlite3
			sqlite3-devel
			tar
			unzip
			wget
			xz
			zip
		)
		repo_requires_git_lfs && packages+=(git-lfs)
		run_as_root zypper install -y "${packages[@]}"
		[ "$WITH_CROSS_CGO" -eq 0 ] || warn "Cross-CGO package setup is only automated for apt-based systems."
		return
	fi

	die "Unsupported package manager. Install git, make, gcc/g++, pkg-config, sqlite3 headers, curl/wget, tar, unzip, xz, python3, and then rerun with --skip-system."
}

go_download_arch() {
	case "$(uname -m)" in
		x86_64 | amd64)
			printf 'amd64'
			;;
		aarch64 | arm64)
			printf 'arm64'
			;;
		i386 | i686)
			printf '386'
			;;
		armv6l | armv7l)
			printf 'armv6l'
			;;
		*)
			die "Unsupported CPU architecture for automatic Go install: $(uname -m)"
			;;
	esac
}

current_go_version() {
	if command_exists go; then
		go version | awk '{print $3}' | sed 's/^go//'
	fi
}

install_go() {
	log "Configuring Go $GO_VERSION"
	ensure_go_path

	if [ "$(current_go_version)" = "$GO_VERSION" ]; then
		printf 'Go %s is already installed.\n' "$GO_VERSION"
		return
	fi

	local arch
	arch="$(go_download_arch)"
	local archive="/tmp/go${GO_VERSION}.linux-${arch}.tar.gz"
	local url="https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz"

	fetch_to "$url" "$archive"
	run_as_root rm -rf /usr/local/go
	run_as_root tar -C /usr/local -xzf "$archive"
	ensure_go_path

	[ "$(current_go_version)" = "$GO_VERSION" ] || die "Go install finished, but go version is not $GO_VERSION."
}

load_nvm() {
	export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
	# shellcheck disable=SC1091
	[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
}

install_nvm() {
	export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
	if [ -s "$NVM_DIR/nvm.sh" ]; then
		load_nvm
		return
	fi

	log "Installing NVM $NVM_VERSION"
	local installer="/tmp/nvm-install-${NVM_VERSION}.sh"
	fetch_to "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" "$installer"
	bash "$installer"
	load_nvm
	command_exists nvm || die "NVM could not be loaded after installation."
}

current_node_version() {
	if command_exists node; then
		node -p 'process.versions.node'
	fi
}

current_pnpm_version() {
	if command_exists pnpm; then
		pnpm --version
	fi
}

install_node_and_pnpm() {
	log "Configuring Node $NODE_VERSION and pnpm $PNPM_VERSION"
	install_nvm

	nvm install "$NODE_VERSION"
	nvm alias default "$NODE_VERSION" >/dev/null
	nvm use "$NODE_VERSION" >/dev/null
	hash -r

	if command_exists corepack; then
		corepack enable
		corepack prepare "pnpm@${PNPM_VERSION}" --activate
		hash -r
	fi

	if ! command_exists pnpm || ! version_ge "$(current_pnpm_version)" "$PNPM_VERSION"; then
		npm install -g "pnpm@${PNPM_VERSION}"
		hash -r
	fi

	if ! version_ge "$(current_node_version)" "$NODE_VERSION"; then
		die "Node $(current_node_version) is older than required $NODE_VERSION."
	fi
	if ! version_ge "$(current_pnpm_version)" "$PNPM_VERSION"; then
		die "pnpm $(current_pnpm_version) is older than required $PNPM_VERSION."
	fi
}

install_frontend_deps() {
	log "Installing frontend dependencies"
	cd "$SCRIPT_DIR"
	pnpm install --frozen-lockfile
}

configure_git_lfs() {
	repo_requires_git_lfs || return

	log "Configuring Git LFS"
	command_exists git-lfs || die "git-lfs is required for this repository but is not installed."

	cd "$SCRIPT_DIR"
	git lfs install --local >/dev/null
}

configure_cross_cgo() {
	[ "$WITH_CROSS_CGO" -eq 1 ] || return

	log "Configuring Makefile.local for cross-CGO sqlite builds"
	cd "$SCRIPT_DIR"
	if [ -f Makefile.local ] && grep -qF '# BEGIN configure.sh cross-cgo' Makefile.local; then
		printf 'Makefile.local already contains configure.sh cross-CGO settings.\n'
		return
	fi

	cat >>Makefile.local <<'EOF'

# BEGIN configure.sh cross-cgo
ifeq ($(GOOS)/$(GOARCH),windows/amd64)
CC := x86_64-w64-mingw32-gcc
CXX := x86_64-w64-mingw32-g++
export CC
export CXX
endif

ifeq ($(GOOS)/$(GOARCH)/$(GOARM),linux/arm/7)
CC := arm-linux-gnueabihf-gcc
CXX := arm-linux-gnueabihf-g++
export CC
export CXX
endif
# END configure.sh cross-cgo
EOF
}

verify_environment() {
	log "Verifying build environment"
	activate_go_path
	load_nvm || true
	hash -r

	local -a missing=()
	local -a problems=()
	local command_name
	for command_name in git make go node pnpm gcc pkg-config; do
		if ! command_exists "$command_name"; then
			missing+=("$command_name")
		fi
	done
	if repo_requires_git_lfs && ! command_exists git-lfs; then
		missing+=("git-lfs")
	fi

	if command_exists go; then
		local found_go
		found_go="$(current_go_version)"
		[ "$found_go" = "$GO_VERSION" ] || problems+=("Go $found_go found, but $GO_VERSION is required.")
	fi

	if command_exists node; then
		local found_node
		found_node="$(current_node_version)"
		version_ge "$found_node" "$NODE_VERSION" || problems+=("Node $found_node found, but >= $NODE_VERSION is required.")
	fi

	if command_exists pnpm; then
		local found_pnpm
		found_pnpm="$(current_pnpm_version)"
		version_ge "$found_pnpm" "$PNPM_VERSION" || problems+=("pnpm $found_pnpm found, but >= $PNPM_VERSION is required.")
	fi

	cd "$SCRIPT_DIR"
	if command_exists make && ! make help >/dev/null; then
		problems+=("make help failed.")
	fi

	if [ "$WITH_CROSS_CGO" -eq 1 ]; then
		for command_name in arm-linux-gnueabihf-gcc x86_64-w64-mingw32-gcc; do
			if ! command_exists "$command_name"; then
				missing+=("$command_name")
			fi
		done
	fi

	if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
		problems+=("node_modules is missing; frontend dependencies are not installed.")
	fi

	if [ "${#missing[@]}" -gt 0 ] || [ "${#problems[@]}" -gt 0 ]; then
		VERIFY_HAD_ISSUES=1
		if [ "${#missing[@]}" -gt 0 ]; then
			printf '\nMissing commands:\n'
			for command_name in "${missing[@]}"; do
				printf '  - %s\n' "$command_name"
			done
		fi
		if [ "${#problems[@]}" -gt 0 ]; then
			printf '\nConfiguration issues:\n'
			local problem
			for problem in "${problems[@]}"; do
				printf '  - %s\n' "$problem"
			done
		fi
		printf '\nTo fix the standard build environment, run ./configure.sh and choose 1) Normal.\n'
		printf 'For sqlite multi-arch builds, choose 2) With cross cgo.\n'
		if [ "$VERIFY_REPORT_ONLY" -eq 1 ]; then
			printf '\nVerify only completed; no changes were made.\n'
			return 0
		fi
		return 1
	fi

	printf 'Go:   %s\n' "$(go version)"
	printf 'Node: %s\n' "$(node --version)"
	printf 'pnpm: %s\n' "$(pnpm --version)"
	printf 'gcc:  %s\n' "$(gcc --version | head -n 1)"
	if repo_requires_git_lfs; then
		printf 'git-lfs: %s\n' "$(git lfs version)"
	fi
	if [ -d "$SCRIPT_DIR/node_modules" ]; then
		printf 'node_modules: present\n'
	else
		printf 'node_modules: missing; run ./configure.sh or pnpm install --frozen-lockfile\n'
	fi
}

if [ "$#" -eq 0 ] && [ -t 0 ]; then
	SHOW_MENU=1
fi

while [ "$#" -gt 0 ]; do
	case "$1" in
		--with-cross-cgo)
			WITH_CROSS_CGO=1
			;;
		--skip-system)
			INSTALL_SYSTEM=0
			;;
		--skip-go)
			INSTALL_GO=0
			;;
		--skip-node)
			INSTALL_NODE=0
			;;
		--skip-frontend-deps)
			INSTALL_FRONTEND_DEPS=0
			;;
		--verify-only)
			VERIFY_ONLY=1
			INSTALL_SYSTEM=0
			INSTALL_GO=0
			INSTALL_NODE=0
			INSTALL_FRONTEND_DEPS=0
			;;
		--menu)
			SHOW_MENU=1
			;;
		-h | --help)
			usage
			exit 0
			;;
		*)
			usage
			die "Unknown option: $1"
			;;
	esac
	shift
done

if [ "$SHOW_MENU" -eq 1 ]; then
	interactive_menu
fi

if [ "$(id -u)" -eq 0 ]; then
	warn "You are running as root. NVM/Node will be configured for root, not for your normal user."
fi

if [ "$VERIFY_ONLY" -eq 0 ]; then
	[ "$INSTALL_SYSTEM" -eq 0 ] || install_system_packages
	configure_git_lfs
	[ "$INSTALL_GO" -eq 0 ] || install_go
	[ "$INSTALL_NODE" -eq 0 ] || install_node_and_pnpm
	configure_cross_cgo
	[ "$INSTALL_FRONTEND_DEPS" -eq 0 ] || install_frontend_deps
fi

verify_environment

if [ "$VERIFY_HAD_ISSUES" -eq 1 ]; then
	cat <<EOF

Done. Missing requirements were reported above and no changes were made.
Run ./configure.sh again and choose:
  1) Normal

For sqlite builds across all smart-build architectures, choose:
  2) With cross cgo
EOF
else
	cat <<EOF

Done.
Recommended next steps:
  ./smart-build.sh

For sqlite builds across all smart-build architectures, prepare the heavier
cross-CGO tools with:
  ./configure.sh --with-cross-cgo
EOF
fi
