#!/bin/bash -e
NVMI_VERSION=0.10.0
# Copyright 2019-2023 xander@ashnazg.com
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# this is the ashnazg NVMI (node version manager installer) infra tool
# usage: source ./nvmi (you can skip sourcing if you don't need NVM and your chosen versions active in your current shell after this run is finished.)
# usage: source ./nvmi [$yarntarget] [-opts]
#	in this mode, nvmi will run setup and then act as an alias for 'yarn'
# usage: ./nvmi --no-project
#	in this mode, nvmi will run global install steps but quit before getting to project-specific processing.
#
# for my typical makefile setup, I have this as the first two lines:
#dev serve setup build:
#	source ./nvmi $@
#
# note that the versions can be overridden at the caller by prefixing envvars:
# node_ver=12.19.0 source ./nvmi $@

if [[ "$node_ver" == nonvmrc ]]; then
	# this is just a special value to disable the above code, as some invocations of nvmi are intended to be global scope only and not change behavior based on CWD
	unset node_ver
else
	# .nvmrc overrides this script's default but not an envvar set by parent.
	if [[ -f .nvmrc && -z "$node_ver" ]]; then
		node_ver=$(sed 's/^v//' .nvmrc)
		if [[ "$node_ver" == node ]]; then
			node_ver=
		fi
	fi
	if [[ -f .npm_version && -z "$npm_ver" ]]; then
		npm_ver=$(cat .npm_version)
	fi
	if [[ -f .yarn_version && -z "$yarn_ver" ]]; then
		yarn_ver=$(cat .yarn_version)
	fi
fi

nvm_ver=${nvm_ver:-0.40.3} # see https://github.com/creationix/nvm
node_ver=${node_ver:-24.4.1} # LTS as of 2024-06-22, though 24 is just about to come out. See for more pinnable options:  https://nodejs.org/en/about/previous-releases
yarn_ver=${yarn_ver:-1.22.22} # to probe for newer versions of an "npm -g" cli tool, use `npm update -g yarn`
npm_ver=${npm_ver:-11.4.2}

install_npm() {
	local r=0
	log2 checking npm version
	if ! checkver npm; then
		log2 installing npm@$npm_ver
		npm install -g npm@$npm_ver || r=$?
		log2 updated, checking again, status was $r
		if ! checkver npm; then
			die "node $node_ver npm installer did not result in npm $npm_ver"
		fi
		log npm has been updated to $(npm --version)
		return
	else
		log2 npm adequate
	fi
}

install_node() {
	local r=0
	log2 checking node for version $node_ver
	checkver node || r=$?
	if ((r != 0)); then
		if ((r != 3)); then
			# try switching to it; it may just be installed but another engine is active
			nvm use v$node_ver || r=$? # just suppress error; if it's not available, we're about to install it.
			checkver node || r=$?
		fi
		if ((r != 0)); then
			echored installing node $1
			nvm install v$node_ver # || r=$?
			nvm use v$node_ver
			log node has been updated to $(node --version)
			if ! checkver node; then
				die could not install and switch to node $node_ver
				return
			fi
		fi
	else
		log2 node version adequate
	fi
}

install_yarn() {
	if ! checkver yarn; then
		echored \
		npm install -g yarn@$yarn_ver
		npm install -g yarn@$yarn_ver
		if ! checkver yarn; then
			die yarn install
		fi
		log yarn has been updated to $(yarn --version)
	fi
}

# full version of color funcs at 1-bash-loggers
echoyellow() {
	fg=33
	echo -e "\x1b[${fg}m\x1b[1m$*\x1b[0m" >&2
}
echored() {
	fg=31
	echo -e "\x1b[${fg}m\x1b[1m$*\x1b[0m" >&2
}
echogreen() {
	fg=32
	echo -e "\x1b[${fg}m\x1b[1m$*\x1b[0m" >&2
}
echoblue() {
	fg=34
	echo -e "\x1b[${fg}m\x1b[1m$*\x1b[0m" >&2
}
if ! type die >/dev/null 2>&1; then
	die() {
		echored FAIL: "$*" >&2
		exit 1
	}
	warn() {
		echoyellow WARN: "$@" >&2
	}
fi
log() {
	if ((verbose)); then
		echogreen "$@"
	fi
}
log2() {
	if ((verbose > 1)); then
		echoblue "$@"
	fi
}

checkver() {
	local global_name=${1}_ver
	local func_flag=${1}_is_func
	local tool=$1 ver=${!global_name} is_func=${!func_flag}

	if ! type $tool >/dev/null 2>&1; then
		echored $tool not found
		return 3
	fi
	if [[ "$ver" == lts ]]; then
		warn skipping $tool version check as target is "'lts'"
		return 0
	fi
	local actual_version="$($tool --version)"
	echogreen "$tool expected: $ver actual: $actual_version"
	if echo "$actual_version" | grep "$ver" >/dev/null; then
		return 0
	elif is_newer_than $ver $actual_version; then
		echored "YOUR $tool IS OUT OF DATE (expected: $ver actual: $actual_version)"
		return 2
	elif is_newer_than $actual_version $ver; then
		echoyellow "YOUR $tool IS NEWER THAN SPEC (expected: $ver actual: $actual_version)"
		return 1
	else
		die unsupported input into version-comparator
	fi
}

# main copy in 0-init-functions
isnumeric() {
	local arg="$*" i char
	[[ -z "$arg" ]] && return 1
	for ((i = 0; i < ${#arg}; ++i )); do
		char=${arg:$i:1}
		if [[ 0 > $char || $char > 9 ]]; then
			return 1
		fi
	done
	return 0
}

# keep this in sync w/ ~/bin/boot/all/4-node
is_newer_than() {
	local expected=${1#v} actual=${2#v} a e
	while true; do
		e=${expected%%.*}
		a=${actual%%.*}
		expected=${expected#*.}
		actual=${actual#*.}
		#echo sliced $e vs $a which leaves $expected vs $actual
		if isnumeric $e; then
			if isnumeric $a; then
				if ((e == a)); then
					continue # check the next section
				elif ((e > a)); then
					#echo yep, $1 is newer than $2
					return 0 # expected is newer than actual
				else
					#echo nope, $2 is newer than $1
					return 1 # expected is older than actual
				fi
			fi
		fi
		return 1
	done
}

TARGET_RC=
persist() {
	if [[ -z "$TARGET_RC" ]]; then
		log2 autodetecting TARGET_RC using SHELL="'$SHELL'"
		case ${SHELL##*/} in
			(bash)
				TARGET_RC=~/.bash_profile
			;;
			(zsh)
				TARGET_RC=~/.zprofile
			;;
			(*)
				die "$SHELL is not a known shell for TARGET_RC defaulting, set TARGET_RC explictly using --rc=..."
			;;
		esac
		log2 TARGET_RC has been defaulted to $TARGET_RC
	fi

	# supports "line is/isn't present" and "don't act if line is there but commented out"
	# doesn't technically support "line is a subset of another line, _should_ install line" but expected usage precludes caring.
	if [[ ! -f "$TARGET_RC" ]] || ! fgrep "$line" "$TARGET_RC" >/dev/null; then
		echo "$line" >> "$TARGET_RC"
	fi
}

# nvm is different from the other cli tools; #1 it's a bash func, #2 we want to support auto-activation but don't want the overhead of activating if this shell has it already activated.
export NVM_DIR=${NVM_DIR:-~/.nvm}
nvm_is_active() {
	type nvm >/dev/null 2>&1
}
nvm_activate() {
	log2 checking to see if nvm is active
	if ! nvm_is_active; then
		log2 nvm is not active, loading "$NVM_DIR/nvm.sh"
		local r # up til 2022-02, I wasn't seeing any non-zero exit statuses, but now...
		. "$NVM_DIR/nvm.sh" || r=$?
		if ((r)); then
			warn nvm.sh returned $r
		fi
		log2 loaded. checking again...
		if ! nvm_is_active; then
			echo FAIL: nvm load
			return 1
		fi
	fi
}

nvm_is_installed() {
	[[ -s "$NVM_DIR/nvm.sh" ]]
}

install_nvm() {
	if nvm_is_installed; then
		log2 nvm is installed, proceeding to activate
		nvm_activate
		log2 activated, checking version
		local r=0
		checkver nvm || r=$? # suppressing effect of bash -e
		if (($r < 2)); then # accept equal or newer versions of nvm
			log2 install_nvm is returning early due to discovering an existing nvm
			return 0
		fi
	fi

	# https://github.com/nvm-sh/nvm#installing-and-updating
	# https://github.com/nvm-sh/nvm/blob/v0.38.0/install.sh#L257
	curl -o- https://raw.githubusercontent.com/creationix/nvm/v$nvm_ver/install.sh | PROFILE=/dev/null bash

	if [[ ! -s "$NVM_DIR/nvm.sh" ]]; then
		echo FAIL: nvm installer did not create nvm.sh
		return 1
	fi

	# make nvm part of every terminal shell:
	if ((install_to_rc)); then
		persist "export NVM_DIR=$NVM_DIR"
		persist '. $NVM_DIR/nvm.sh'
	fi

	nvm_activate
	if ! checkver nvm; then
		die nvm install
	fi
	log nvm has been updated to $(nvm --version)
}

nvm_call=()
link_dev_modules=0
install_modules=0
set_default=0
shelling=0
verbose=0
install_to_rc=1
breaking=0
fix_darwin=0
if [[ -f package.json ]]; then
	install_modules=1
	link_dev_modules=1
fi
for arg; do
	if ((breaking)); then
		nvm_call+=("$arg")
		continue
	fi
	case $arg in
		(--)
			breaking=1
		;;
		(--help)
			echo USAGE:
			echo 'nvmi [--opts] [cmds]'
			echo 'just `nvmi`: ensure NVM, Node, Yarn, NPM versions, then install project packages'
			echo '`nvmi foo bar`: do the above, then run `yarn foo bar`'
			echo
			echo opts:
			echo '--verify              makes nvmi do a dry-run of the prep stages, ignoring cmds.'
			echo '--set-default         update nvm default engine to selected one'
			echo '--no-project          ignore package.json in CWD; normally it yarns every time package.json is newer than node_modules.'
			echo '--no-auto-link        normally, it looks for any `yarn link`ed packages and hot-links to them.'
			echo '--shell               cmds are run _as_ cmd, not prefixed with `yarn `'
			echo '--rc=~/.bashrc        install nvm.sh here instead of guessing based on shell'
			echo '--no-rc               turn off rc changes entirely'
			echo '--fix-darwin-profile  ensure that any .bash_profile or .profile you have includes a source .bashrc'
			echo '--nocolor             disable color escapes for logging convenience'
			echo '-v                    turns on lib autolinking trace'
			do_not_continue=1
		;;
		(--version)
			echo nvmi $NVMI_VERSION MIT licensed, by xander@ashnazg.com
			echo nvm $nvm_ver
			echo node $node_ver
			echo yarn $yarn_ver
			echo npm $npm_ver
			do_not_continue=1
		;;
		(-v|--verbose)
			let ++verbose
			# ranks:
			# 1 show auto lib linking status
			# 2 trace where process is failing
		;;
		(--verify)
			log verifying node tool versions...
			nvm_activate
			checkver nvm && checkver node && checkver yarn && checkver npm
			do_not_continue=1
		;;
		(--set-default)
			set_default=1
		;;
		(--no-project)
			install_modules=0
			link_dev_modules=0
		;;
		(--no-auto-link)
			link_dev_modules=0
		;;
		(--shell)
			shelling=1
		;;
		(--rc=*)
			TARGET_RC=${arg#*=}
			install_to_rc=1
		;;
		(--no-rc)
			install_to_rc=0
		;;
		(--fix-darwin-profile)
			fix_darwin=1
		;;
		(--nocolor|--no-color)
			# disable colors for ease of syslogging
			echored() { echo "$@" ; }
			echogreen() { echo "$@" ; }
			echoblue() { echo "$@" ; }
			echoyellow() { echo "$@" ; }
		;;
		(setup)
			: # this is always in effect and is just a legacy call signature
		;;
		(*)
			nvm_call+=("$arg")
		;;
	esac
done

if ((do_not_continue)); then
	# BASH_SOURCE (https://stackoverflow.com/questions/2683279) is not helping in this use case, so resorting to the 2nd best kludge
	return 0 2>/dev/null || true # support source nvmi mode, but don't complain if not sourced. also, do not trip the 'bash -e' mode's quick exit over this.
	exit 0
fi

if ((fix_darwin)); then
	for prof in ~/.{bash_,}profile; do
		if [[ -f "$prof" ]]; then
			TARGET_RC="$prof" persist '. ~/.bashrc'
			break
		fi
	done
fi

# pre-pre-flight: is there a git commit around here?
commit=
if which git >/dev/null 2>&1; then
	commit="$(git rev-parse HEAD 2>/dev/null || true)"
	if [[ -n "$commit" ]]; then
		# then "get the branch name" should work as well
		branch_name="$(git rev-parse --abbrev-ref HEAD)"
		commit=" $branch_name@$commit"
	fi
fi
# this pre-flight list runs before any wrapped yarn actions:
echogreen
echogreen "$(date --iso-8601=seconds) NVMI@$NVMI_VERSION$commit is checking on node tools in $PWD"
log "prepping nvm($nvm_ver)..."
install_nvm
log "prepping node($node_ver)..."
install_node
log "prepping npm($npm_ver)..."
install_npm
log "prepping yarn($yar_ver)..."
install_yarn

if ((set_default)); then
	log setting default
	nvm alias default $node_ver
	nvm use default
fi

if ((install_modules)); then
	log installing packages
	re_yarn=1
	# install modules through yarn if node version tracking is not up to date.
	if [[ ! -f package.json ]]; then
		die 'NVMI must be called from the root directory of your project (no package.json found)'
	fi
	if [[ -f node_modules/.nvmi-node-version ]]; then
		if [[ "$(node --version)" != "$(cat node_modules/.nvmi-node-version)" ]]; then
			echo reinstalling all packages due to node version change
			rm -rf node_modules
		elif [[ package.json -nt node_modules/.nvmi-node-version ]]; then
			echo package.json has changed, running yarn
		else
			re_yarn=0
		fi
	fi
	if ((re_yarn)); then
		yarn
		node --version > node_modules/.nvmi-node-version
	fi
fi

if ((link_dev_modules)); then
	log linking dev packages
	check_package_for_linked_version() {
		local package
		while read package; do
			if [[ -d ~/.config/yarn/link/$package ]]; then
				if [[ -L node_modules/$package ]]; then
					log $package already a symlink
				else
					log $package is linkable
					yarn link "$package"
				fi
			else
				log $package is not linkable
			fi
		done
	}
	# if you've run 'yarn link' in a module-dev-folder on this machine, and that package is depended on by _this_ app, use yarn linkage
	node <<-\EOF | check_package_for_linked_version
		var conf = require('./package.json');
		['dependencies', 'devDependencies'].map(section => {
			if (!conf[section]) return;
			Object.keys(conf[section]).map(package => {
				console.log(package);
			});
		});
	EOF
fi

if ((${#nvm_call[@]})); then
	if ((shelling)); then
		log shelling: "${nvm_call[@]}"
		"${nvm_call[@]}"
	else
		log yarn "${nvm_call[@]}"
		yarn "${nvm_call[@]}"
	fi
fi
