#!/bin/bash

# Options
#
#   -V, --verbose
#     Enable verbose output for the installer
#
#   -f, -y, --force, --yes
#     Skip the confirmation prompt during installation
#
#   -p, --platform
#     Override the platform identified by the installer
#
#   -b, --bin-dir
#     Override the bin installation directory (defaults to --symlink-dir for non-darwin platforms)
#
#   -S, -symlink-dir
#     Create a symlink in a directory expected to be in your PATH.
#     If different from --bin-dir, this creates a symlink to --bin-dir binary.
#
#   -a, --arch
#     Override the architecture identified by the installer
#
#   -B, --base-url
#     Override the base URL used for downloading releases
#
#   -e, --exec
#     Execute the new binary with the args given after --exec

set -euo pipefail
printf "\n"

BOLD="$(tput bold 2>/dev/null || echo '')"
GREY="$(tput setaf 0 2>/dev/null || echo '')"
UNDERLINE="$(tput smul 2>/dev/null || echo '')"
RED="$(tput setaf 1 2>/dev/null || echo '')"
GREEN="$(tput setaf 2 2>/dev/null || echo '')"
YELLOW="$(tput setaf 3 2>/dev/null || echo '')"
BLUE="$(tput setaf 4 2>/dev/null || echo '')"
MAGENTA="$(tput setaf 5 2>/dev/null || echo '')"
NO_COLOR="$(tput sgr0 2>/dev/null || echo '')"

SUPPORTED_TARGETS="darwin_amd64 darwin_arm64 linux_amd64 linux_arm64"

header() {
	printf "%s\n" "${BOLD}${MAGENTA}$*${NO_COLOR}"
}

plain() {
	printf "%s\n" "$*"
}

info() {
	printf "%s\n" "${BOLD}${GREY}→${NO_COLOR} $*"
}

warn() {
	printf "%s\n" "${YELLOW}! $*${NO_COLOR}"
}

error() {
	printf "%s\n" "${RED}x $*${NO_COLOR}" >&2
}

complete() {
	printf "%s\n" "${GREEN}✓${NO_COLOR} $*"
}

intro() {
	header "Welcome to Jetpack.io 🚀"
	plain "Jetpack is a backend development platform that makes it easy to build complex backends"
	plain "on Kubernetes without being an expert."
	printf "\n"
	plain "This script downloads and installs the jetpack CLI."
	printf "\n"
}

# This function is added to display messages that we would like to not show,
# except just prior to a sudo prompt. This is to improve UX for users whose
# sudo timeout is set to zero, and they get repeatedly prompted for their
# password. For such users, this message lets them know that they are progressing
# and not just failing to enter the correct password.
info_for_sudo() {
	local sudo
	local msg

	sudo="$1"
	msg="$2"

	if [ "$sudo" != "" ]; then
		info "$msg"
	fi
}

get_tmpfile() {
	local suffix
	suffix="$1"
	if hash mktemp; then
		printf "%s.%s" "$(mktemp)" "${suffix}"
	else
		printf "/tmp/jetpack.%s" "${suffix}"
	fi
}

# Test if a location is writeable by trying to write to it. Windows does not let
# you test writeability other than by writing: https://stackoverflow.com/q/1999988
test_writeable() {
	local path
	path="${1:-}/test.txt"
	if touch "${path}" 2>/dev/null; then
		rm "${path}"
		return 0
	else
		return 1
	fi
}

fetch() {
	local command
	if hash curl 2>/dev/null; then
		set +e
		command="curl --silent --fail --location $1"
		curl --silent --fail --location "$1"
		rc=$?
		set -e
	else
		if hash wget 2>/dev/null; then
			set +e
			command="wget -O- -q $1"
			wget -O- -q "$1"
			rc=$?
			set -e
		else
			error "No HTTP download program (curl, wget) found…"
			exit 1
		fi
	fi

	if [ $rc -ne 0 ]; then
		printf "\n" >&2
		error "Command failed (exit code $rc): ${BLUE}${command}${NO_COLOR}"
		printf "\n" >&2
		info "Jetpack.io does not yet support ${MAGENTA}${PLATFORM}_${ARCH}${NO_COLOR}." >&2
		exit $rc
	fi
}

fetch_and_unpack() {
	info "Downloading ${URL}"
	info "Extracting to ${BIN_DIR}"
	local sudo
	local tmpfile
	sudo="$1"
	if [ "${EXT}" = "tar.gz" ]; then
		# DEV-948: We can end up corrupting the binary, leading to segfaults. To avoid:
		# First, untar to a temporary location.
		# Then do a filesystem-move to override any existing jetpack binary.
		tmpbin="$(mktemp)"
		fetch "${URL}" | tar -xOz"${VERBOSE}"f - jetpack >"$tmpbin"
		mv -f"${VERBOSE}" "$tmpbin" "${BIN_DIR}/jetpack"
		chmod 555 "${BIN_DIR}/jetpack" # ensure binary's permissions are correct

	# Warning for engineers: this code path is for Windows, and AFAIK has not been
	# exercised.
	elif [ "${EXT}" = "zip" ]; then
		tmpfile="$(get_tmpfile "${EXT}")"
		fetch "${URL}" >"${tmpfile}"
		${sudo} unzip "${tmpfile}" -d "${BIN_DIR}"
		rm "${tmpfile}"
	else
		error "Unknown package extension."
		exit 1
	fi
	complete "Binary successfully installed."
}

elevate_priv() {
	if ! hash sudo 2>/dev/null; then
		error 'Could not find the command "sudo", needed to get permissions for install.'
		info "If you are on Windows, please run your shell as an administrator, then"
		info "rerun this script. Otherwise, please run this script as root, or install"
		info "sudo."
		exit 1
	fi
}

install() {
	local msg
	local sudo

	mkdir -p ${BIN_DIR} # Try to create the directory without extended permissions
	if test_writeable "${BIN_DIR}"; then
		sudo=""
		msg="Installing the jetpack CLI, please wait..."
	else
		warn "sudo required to install to ${BIN_DIR}"
		elevate_priv
		sudo="sudo"
		msg="Installing the jetpack CLI using sudo, please wait..."
	fi
	info "$msg"

	info_for_sudo "$sudo" "  Executing: ${sudo} mkdir -p ${BIN_DIR}"
	${sudo} sh -c "mkdir -p ${BIN_DIR} && chmod 0755 ${BIN_DIR} && chown $(id -u):$(id -g) ${BIN_DIR}"
	fetch_and_unpack "${sudo}"
}

create_symlink() {
	local msg
	local sudo

	if test_writeable "${SYMLINK_DIR}"; then
		sudo=""
	else
		warn "sudo is required to create ${SYMLINK_DIR}/jetpack"
		elevate_priv
		sudo="sudo"
	fi

	# remove the symlink if the target is pointing elsewhere
	if [ -f "${SYMLINK_DIR}/jetpack" ] && [ -L "${SYMLINK_DIR}"/jetpack ]; then
		existing_dest=$(readlink ${SYMLINK_DIR}/jetpack)
		if [ "$existing_dest" != "${BIN_DIR}"/jetpack ]; then
			info "Found existing symlink at ${SYMLINK_DIR}/jetpack that points at the wrong location, replacing it."
			info_for_sudo "$sudo" "  Executing: sudo rm ${SYMLINK_DIR}/jetpack"
			${sudo} rm "${SYMLINK_DIR}"/jetpack
		fi
	fi

	if [ -f "${SYMLINK_DIR}/jetpack" ] && [ ! -L "${SYMLINK_DIR}"/jetpack ]; then
		info "Found existing file at ${SYMLINK_DIR}/jetpack, replacing it."
		info_for_sudo "$sudo" "  Executing: sudo rm ${SYMLINK_DIR}/jetpack"
		${sudo} rm "${SYMLINK_DIR}"/jetpack
	fi

	# -L tests whether a link exists
	# -e tests whether link is valid and points to a dir or file
	# https://stackoverflow.com/a/36180056
	if [ ! -L "${SYMLINK_DIR}/jetpack" ] || [ ! -e "${SYMLINK_DIR}/jetpack" ]; then
		info_for_sudo "$sudo" "  Executing: ${sudo} ln -s "${BIN_DIR}"/jetpack "${SYMLINK_DIR}"/jetpack"
		${sudo} ln -s "${BIN_DIR}"/jetpack "${SYMLINK_DIR}"/jetpack
	fi
	complete "Symlink successfully created"
}

detect_platform() {
	local platform
	platform="$(uname -s | tr '[:upper:]' '[:lower:]')"
	echo "${platform}"
}

detect_arch() {
	local arch
	arch="$(uname -m | tr '[:upper:]' '[:lower:]')"

	# `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check
	if [ "${arch}" = "x64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then
		arch=i686
	fi

	if [ "${arch}" = "aarch64" ]; then
		arch=arm64
	fi

	case "${arch}" in
	x86_64) arch="amd64" ;;
	esac

	echo "${arch}"
}

confirm_installation() {
	if [ -z "${FORCE-}" ]; then
		printf "%s " "${MAGENTA}?${NO_COLOR} $* ${BOLD}[Y/n]${NO_COLOR}"
		set +e
		read -r yn </dev/tty
		rc=$?
		set -e
		if [ $rc -ne 0 ]; then
			error "Error reading from prompt (please re-run with the '--yes' option)"
			exit 1
		fi
		if [ "$yn" != "y" ] && [ "$yn" != "Y" ] && [ "$yn" != "yes" ] && [ "$yn" != "" ]; then
			error 'Installation cancelled'
			exit 1
		fi
	fi
}

confirm_symlink() {
	if [ -z "${FORCE-}" ]; then
		printf "%s " "${MAGENTA}?${NO_COLOR} $* ${BOLD}[Y/n]${NO_COLOR}"
		set +e
		read -r yn </dev/tty
		rc=$?
		set -e
		if [ $rc -ne 0 ]; then
			error "Error reading from prompt (please re-run with the '--yes' option)"
			exit 1
		fi
		if [ "$yn" != "y" ] && [ "$yn" != "Y" ] && [ "$yn" != "yes" ] && [ "$yn" != "" ]; then
			warn 'Not creating symlink'
			printf "\n"
			info "Add the Jetpack CLI to your path by adding the following to your .bashrc/.zshrc:"
			printf "\n"
			plain "    export PATH=\"\$PATH:${BIN_DIR}\""
			return 1 # This is a shell-script, so of-course we're gonna have to use 1 for "false". *face-palm*
		fi
		return 0 # true
	fi
	return 0 # true
}

check_symlink_dir() {
	local symlink_dir="$1"

	if [ ! -d "$symlink_dir" ]; then
		error "Installation location $symlink_dir does not appear to be a directory"
		info "Make sure the location exists and is a directory, then try again."
		exit 1
	fi

	# https://stackoverflow.com/a/11655875
	local in_path
	in_path=$(
		IFS=:
		for path in $PATH; do
			if [ "${path}" = "${symlink_dir}" ]; then
				echo 1
				break
			fi
		done
	)

	if [ "${in_path}" != "1" ]; then
		warn "Symlink directory ${symlink_dir} is not in your \$PATH"
		info "Add the Jetpack CLI to your path by adding the following to your .bashrc/.zshrc:"
		printf "\n"
		plain "  export PATH=\"\$PATH:${symlink_dir}\""
		printf "\n"
	fi
}

check_bin_dir() {
	local bin_dir="$1"

	if [ -f "$bin_dir" ]; then
		error "Installation location $bin_dir does not appear to be a directory"
		info "Move or rename the existing file, then try again."
		exit 1
	fi
}

is_build_available() {
	local arch="$1"
	local platform="$2"

	local target="${platform}_${arch}"
	local good

	good=$(
		IFS=" "
		for t in $SUPPORTED_TARGETS; do
			if [ "${t}" == "${target}" ]; then
				echo 1
				break
			fi
		done
	)

	if [ "${good}" != "1" ]; then
		error "${arch} builds for ${platform} are not yet available for Jetpack"
		printf "\n" >&2
		printf "\n"
		exit 1
	fi
}

# defaults
if [ -z "${PLATFORM-}" ]; then
	PLATFORM="$(detect_platform)"
fi

if [ -z "${SYMLINK_DIR-}" ]; then
	SYMLINK_DIR=/usr/local/bin
fi

if [ -z "${ARCH-}" ]; then
	ARCH="$(detect_arch)"
fi

if [ -z "${BIN_DIR-}" ]; then
	BIN_DIR=${HOME}/.jetpack/bin
fi

if [ -z "${BASE_URL-}" ]; then
	BASE_URL="https://releases.jetpack.io/jetpack/nightly"
fi

# parse argv variables
EXEC=()
while [ "$#" -gt 0 ]; do
	case "$1" in
	-p | --platform)
		PLATFORM="$2"
		shift 2
		;;
	-b | --bin-dir)
		BIN_DIR="$2"
		shift 2
		;;
	-A | --app-dir)
		SYMLINK_DIR="$2"
		shift 2
		;;
	-a | --arch)
		ARCH="$2"
		shift 2
		;;
	-B | --base-url)
		BASE_URL="$2"
		shift 2
		;;

	-V | --verbose)
		VERBOSE=1
		shift 1
		;;
	-f | -y | --force | --yes)
		FORCE=1
		shift 1
		;;

	-p=* | --platform=*)
		PLATFORM="${1#*=}"
		shift 1
		;;
	-b=* | --bin-dir=*)
		BIN_DIR="${1#*=}"
		shift 1
		;;
	-A=* | --app-dir=*)
		SYMLINK_DIR="${1#*=}"
		shift 1
		;;
	-a=* | --arch=*)
		ARCH="${1#*=}"
		shift 1
		;;
	-B=* | --base-url=*)
		BASE_URL="${1#*=}"
		shift 1
		;;
	-V=* | --verbose=*)
		VERBOSE="${1#*=}"
		shift 1
		;;
	-f=* | -y=* | --force=* | --yes=*)
		FORCE="${1#*=}"
		shift 1
		;;
	-e | --exec)
		IS_EXEC=yes
		shift 1
		;;
	*)
		if [ -n "${IS_EXEC-}" ]; then
			EXEC+=("$1")
			shift 1
		else
			error "Unknown option: $1"
			exit 1
		fi
		;;
	esac
done

is_build_available "${ARCH}" "${PLATFORM}"

intro

printf "  %s\n" "${UNDERLINE}Installation Details${NO_COLOR}"
plain "  ${BOLD}Bin directory${NO_COLOR}:     ${GREEN}${BIN_DIR}${NO_COLOR}"
plain "  ${BOLD}Symlink directory${NO_COLOR}: ${GREEN}${SYMLINK_DIR}${NO_COLOR}"
plain "  ${BOLD}Platform${NO_COLOR}:          ${GREEN}${PLATFORM}${NO_COLOR}"
plain "  ${BOLD}Arch${NO_COLOR}:              ${GREEN}${ARCH}${NO_COLOR}"

# non-empty VERBOSE enables verbose untarring
if [ -n "${VERBOSE-}" ]; then
	VERBOSE=v
	info "${BOLD}Verbose${NO_COLOR}: yes"
else
	VERBOSE=
fi

echo

EXT=tar.gz
if [ "${PLATFORM}" = "pc-windows-msvc" ]; then
	EXT=zip
fi

URL="${BASE_URL}/jetpack_${PLATFORM}_${ARCH}.${EXT}"
plain "  Tarball URL: ${UNDERLINE}${BLUE}${URL}${NO_COLOR}"
printf "\n"
confirm_installation "Install ${BOLD}${GREEN}jetpack${NO_COLOR} to ${BOLD}${GREEN}${BIN_DIR}${NO_COLOR}?"
check_bin_dir "${BIN_DIR}"
install

if [ "${BIN_DIR}" != "${SYMLINK_DIR}" ]; then
	printf "\n"
	if confirm_symlink "Add ${BOLD}${GREEN}${SYMLINK_DIR}/jetpack${NO_COLOR} as a symlink? (recommended, requires sudo)"; then
		check_symlink_dir "${SYMLINK_DIR}"
		create_symlink
	fi
fi

printf "\n\n"
complete "${BOLD}Successfully installed the ${GREEN}jetpack${NO_COLOR}${BOLD} CLI${NO_COLOR} 🚀"
info "Head over to our documentation for next steps: ${UNDERLINE}${BLUE}https://www.jetpack.io/docs/${NO_COLOR}"

# call a command on the new executable if `--exec <cmd> <args>` is specified
if [ -n "${EXEC-}" ]; then
	$SYMLINK_DIR/jetpack ${EXEC[@]} --skip-version-check
fi
