#!/bin/sh
#
# Ansible Boilerplate Collection: Setup & Maintenance Script.
#

set -eu

# Detect commands to use:
PYTHON="${PYTHON:-$(command -v python3 2>/dev/null || echo "python3")}"
PIP="${PIP:-$(command -v pip 2>/dev/null || echo "pip")}"
ANSIBLE_GALAXY="${ANSIBLE_GALAXY:-$(command -v ansible-galaxy 2>/dev/null || echo "ansible-galaxy")}"

BASE_D="ansible_galaxy/ansible_collections/alexbarton/boilerplate"

#
# Show usage information on stderr.
#
Usage() {
	{
		echo "ansible-boilerplate <command>"
		echo
		echo   "  help		Show this help text and exit."
		echo   "  init		Initialize project and boilerplate code."
		echo   "  update	Update boilerplate code and dependencies. [Alias: upgrade, up]"
		echo   "    --force	Force overwriting an existing role or collection."
		for plugin in $(Get_Plugin_Files); do
			cmd=$(basename "${plugin}" | sed -e 's/^ab-//')
			description=$(grep -F '# description=' "${plugin}" | cut -d= -f2-)
			[ ${#cmd} -lt 8 ] && cmd="${cmd}\t"
			echo "  ${cmd}	${description:-(no description found)}"
		done | sort
		echo
	} >&2
}

#
# Initialize a new project.
#
# Create some default files and call Upgrade() afterwards. This function does not
# overwrite any already existing file.
#
Init() {
	if [ $# -ne 0 ]; then
		Usage
		exit 1
	fi
	if [ -e Makefile.boilerplate ]; then
		echo "This is the upstream project! Don't call \"init\" on it!" >&2
		exit 1
	fi
	echo "Initialize project:"

	for file in \
		README.md \
		LICENSE \
	; do
		test -e "${file}" || touch "${file}"
	done
	mkdir -p .vscode playbooks roles
	test -e "hosts.ini" || Init_HostsIni
	test -e "Makefile" || Init_Makefile
	test -e "requirements.yml" || Init_RequirementsYml

	Upgrade --init
}

#
# Create a Makefile template file.
#
Init_Makefile() {
	echo "Creating \"Makefile\" ..."
	cat >Makefile <<EOF
#
# Makefile
#

SOURCE_ROOT ?= \$(CURDIR)

# Make sure that the Ansible Boilerplate Collection project is set up (its
# files are "available"), and initialize it if not!
_DUMMY := \$(shell test -e ansible_galaxy/ansible_collections/alexbarton/boilerplate/Makefile.boilerplate || bin/ansible-boilerplate upgrade >&2)

default: all

# Include the Ansible Boilerplate Collection Makefile fragment:
include ansible_galaxy/ansible_collections/alexbarton/boilerplate/Makefile.boilerplate

all:

check:

install:

clean:

distclean: clean

maintainer-clean: distclean

.PHONY: default all check install clean distclean maintainer-clean
EOF
}

#
# Create a hosts.ini template file.
#
Init_HostsIni() {
	echo "Creating \"hosts.ini\" ..."
	cat >hosts.ini <<EOF
# Ansible hosts list

[all_managed]
localhost
EOF
}

#
# Create a requirements.yml template file.
#
Init_RequirementsYml() {
	echo "Creating \"requirements.yml\" ..."
	cat >requirements.yml <<EOF
---
# Ansible dependencies

collections:
  - ${BOILERPLATE_COLLECTION_SRC:-alexbarton.boilerplate}

roles:
  []
EOF
}

#
# Upgrade a project.
#
# - Initialize a Python virtual environment (when a ".venv" directory exists
#   or the ansible-galaxy command is not found).
# - Install Ansible when ansible-galaxy command is not found.
# - Install "ansible-boilerplate" collection when not found.
# - Update local "ansible-boilerplate" setup: copy script, create links, ...
# - Upgrade template files (when "--init" is given).
# - Install/upgrade Python dependencies (from requirements.txt file).
# - Install/upgrade Ansible Galaxy dependencies (from requirements.yml file).
#
# --force: Passed to "ansible-galaxy install" command.
# --init: Upgrade() is called by the Init() function.
#
Upgrade() {
	is_init=
	do_force=

	while [ $# -gt 0 ]; do
		case "$1" in
			"--force")
				do_force="--force --force-with-deps"
				;;
			"--init")
				is_init="--init"
				do_force="--force --force-with-deps"
				;;
			*)
				Usage
				exit 1
		esac
		shift
	done
	[ -z "${is_init}" ] && echo "Upgrade project:"

	# Check Python virtual environment
	if [ -d .venv ] || ! command -v "${ANSIBLE_GALAXY}" >/dev/null; then
		# Either an existing ".venv" folder was found or the
		# ansible-galaxy(1) command was not found on the system, so
		# let's use a Python virtual environment!
		echo "Using a Python virtual environment."
		PIP="./.venv/bin/pip"
		if ! env | grep "^ANSIBLE_GALAXY=" >/dev/null; then
			# Override "ANSIBLE_GALAXY" variable to point to the
			# command in the virtual environment when not already
			# explicitly set in the environment! (So you can still
			# override it and use some wrapper scripts ...)
		ANSIBLE_GALAXY="./.venv/bin/ansible-galaxy"
		fi
		if ! [ -x .venv/bin/pip ]; then
			echo "Initializing Python virtual environment ..."
			"${PYTHON}" -m venv .venv
			"${PIP}" install -U pip setuptools
		fi
	fi
	for var in PYTHON PIP ANSIBLE_GALAXY; do
		eval 'echo " - ${var} is \"$'"${var}"'\"."'
	done

	if [ -r requirements.txt ]; then
		echo "Installing Python dependencies ..."
		"${PIP}" install -U -r requirements.txt
	fi

	# Make sure that the "ansible-galaxy" command is available now:
	if ! [ -x "${ANSIBLE_GALAXY}" ]; then
		echo "Oops, \"${ANSIBLE_GALAXY}\" not found!" >&2
		echo "You either need Ansible installed locally or list it as a dependency in" >&2
		echo "the \"requirements.txt\" file of this project!" >&2
		exit 1
	fi

	if [ -r requirements.yml ]; then
		echo "Upgrading Ansible Galaxy dependencies ..."
		# shellcheck disable=SC2086
		"${ANSIBLE_GALAXY}" collection install -U -r requirements.yml ${do_force}
		# shellcheck disable=SC2086
		"${ANSIBLE_GALAXY}" role install -r requirements.yml ${do_force}
	fi

	# Are we running in a dependent project? If so, perform specific upgrade tasks!
	# shellcheck disable=SC2086
	[ -e Makefile.boilerplate ] || Upgrade_Dependent ${is_init}
}

#
# Upgrade steps for dependent projects only.
#
# --init: Upgrade() is called by the Init() function.
#
Upgrade_Dependent() {
	# Verify that the Boilerplate Collection is available now.
	# NOTE: This dependency must be properly listed in the requirements.yml
	# file inside of the (dependent) project!
	"${ANSIBLE_GALAXY}" collection verify --offline alexbarton.boilerplate

	echo "Copying \"boilerplate\" script into bin/ directory ..."
	mkdir -p bin
	cp -av "${BASE_D}/bin/ansible-boilerplate" "bin/ansible-boilerplate"

	echo "Creating symbolic links to files inside of the Boilerplate Collection ..."
	for file in \
		bin/a \
		bin/ap \
		bin/aps \
	; do
		# Create (new) symbolic links, when the target already is a symbolic link or
		# does not yet exists. Don't overwrite existing regular files etc.!
		test -L "${file}" && ln -fsv "../${BASE_D}/${file}" "${file}"
		test -e "${file}" || ln -fsv "../${BASE_D}/${file}" "${file}"
	done

	echo "Upgrading template files from the Boilerplate Collection ..."
	for file in \
		.ansible-lint \
		.editorconfig \
		.gitignore \
		.vscode/settings.json \
		.yamllint.yml \
		ansible.cfg \
		requirements.txt \
	; do
		# shellcheck disable=SC2086
		Upgrade_Template "${file}" "${1:-}"
	done

	# List differences in *.new template files, if any:
	find . \( -name '*.new' -o -name '.*.new' \) | while read -r fname; do
		diff -u "${fname%*.new}" "${fname}" || true; echo
	done
}

#
# Upgrade a template file.
#
# --init: Initialize a new project, therefore create the template file if it
#         does not yet exist.
#
Upgrade_Template() {
	# Does the target directory exist? Skip this template file if not!
	[ -d "$(dirname "$1")" ] || return 0

	# Return when the target file does not exist and not in "init mode":
	[ ! -e "$1" ] && [ "${2:-}" != "--init" ] && return 0

	# Remove the target when it is a symbolic link.
	[ -L "$1" ] && rm -v "$1"

	# Do not override the target when it exists already!
	if [ -e "$1" ]; then
		# Target already exists. Is it different?
		if [ "${2:-}" = "--init" ] && ! cmp -s "$1" "${BASE_D}/$1"; then
			# Files are not the same! Install new version in parallel:
			install -b -m 0644 -p -v "${BASE_D}/$1" "$1.new"
		fi
	else
		# Target does not yet exist:
		install -b -m 0644 -p -v "${BASE_D}/$1" "$1"
	fi
}

#
# Get all plugin file names.
#
# This function returns a newline separated list of full path names of all
# found plugins, including the "ab-" prefix.
#
# $1: Explicit plugin name to search for, _without_ the "ab-" prefix! When
#     given, exactly 0 or 1 (the 1st) results are returned, even when a
#     matching plugin can be found in more than one plugin directories.
#
Get_Plugin_Files() {
	[ -z "${1:-}" ] && max=9999 || max=1
	find \
		./libexec \
		/usr/local/libexec/ansible-boilerplate/ \
		/usr/local/lib/ansible-boilerplate/ \
		./ansible_galaxy/ansible_collections/alexbarton/boilerplate/libexec \
		/usr/libexec/ansible-boilerplate/ \
		-type f \( -perm -u=x -o -perm -g=x -o -perm -o=x \) \
		-name "ab-${1:-*}" 2>/dev/null \
		| head -n "${max}"
}

#
# Check that we are in the project root directory.
#
Check_Project_Dir() {
	[ -r ansible.cfg ] && return 0
	# Oops, not in an Ansible project root directory!?
	echo "Oops, looks like \"$PWD\" is not an Ansible project directory!?" >&2
	if [ -x ./ansible_galaxy/ansible_collections/alexbarton/boilerplate/bin/ansible-boilerplate ]; then
		echo "You can initialize a new project with the following command:" >&2
		echo "  ${0} init" >&2
	else
		echo "Probably you have not initialized it yet?" >&2
	fi
	echo "Aborting." >&2
	exit 1
}

# Change working directory to the base directory in the "main" script:
if [ "${1:-}" != '__is_reexec__' ]; then
	# This is the "main" script, good. Now try to detect if we were called
	# from the "galaxy" directory or from the "bin" directory of the local
	# project:
	if [ -e ".venv" ]; then
		# Looks like we _are_ in the toplevel project directory. Either
		# of the ansible-boilerplate project, or a project using it.
		# Both is good, so nothing to do!
		:
	elif [ -e "$(dirname "$0")/../Makefile.boilerplate" ]; then
		# Looks like the "galaxy" directory.
		cd "$(dirname "$0")/../../../../.." || exit 1
	else
		# Does not look like the "galaxy" directory ...
		cd "$(dirname "$0")/.." || exit 1
	fi
fi

case " $* " in
	*" init "*|*" upgrade "*|*" update "*|*" up "*)
		# Copy the script to a temporary place and re-execute it from
		# there. This allows us to safely update the script itself
		# while running.
		if [ "${1:-}" != '__is_reexec__' ]; then
			tmp_script=$(mktemp -t "$(basename "$0").XXXXXX") || exit 1
			cp "$0" "${tmp_script}" || exit 1
			exec /bin/sh "${tmp_script}" __is_reexec__ "$@"
		fi
		# This is the re-executed script; skip the marker argument!
		[ $# -gt 0 ] && shift
		;;
	*)
esac

cmd="${1:-}"
[ $# -gt 0 ] && shift

case "${cmd}" in
	"init")
		Init "$@"
		;;
	"upgrade"|"update"|"up")
		Check_Project_Dir
		Upgrade "$@"
		;;
	"help"|"--help")
		Usage
		;;
	"")
		Usage
		exit 1
		;;
	*)
		# Look for a plugin:
		Check_Project_Dir
		cmd_file=$(Get_Plugin_Files "${cmd}") || true
		if [ -x "${cmd_file}" ]; then
			export AP_VERBOSE=0
			exec "${cmd_file}" "$@" || exit 1
		fi

		# No plugin found, subcommand is unknown!
		Usage
		exit 1
esac
exit $?
