#!/bin/bash
#
# Create a Nogggin Integration Framework (NIF) system account and or branch
#
# TODO
#  * Support SNI domains (non-prefix)
#
#

B=
R=
Y=
N=
# Only enable ANSII colors if this script is run without debugging and in an interactive shell
if [[ $- != *x* ]] && [[ -t 0 ]]; then
		B=$'\033[1m'
		R=$'\033[31m'
		Y=$'\033[33m'
		N=$'\033[0m'
fi

declare -A FLAGS
FLAGS=(
		[FORCE]=0
		[DRYRUN]=0
		[CHECK]=0
		[CREATEDB]=0
)


# Display an optional error, the usage text and then exit
function usage {
local STATUS="$1"
local ERROR="$2"
if [[ -n "$STATUS" && -z "$ERROR" ]]; then
		ERROR="$STATUS"
		STATUS=
fi

cat <<-EOD
        ${ERROR:+${B}${R}ERROR:${N}${B} ${ERROR}${N}}
        ${B}USAGE:${N} $(basename "$0") [options] <project> [<branch> <domain-prefix>]

        ${B}ARGUMENTS${N}

            ${N}<project>${B}       := The name of the project (system account) for example 'supportoca-forms'
                               Note that a leading 'nif_' will be added to the system username

            ${N}<branch>${B}        := The branch (sub-account) name of the project to configure for example 'uat', 'beta', 'prod'...

            ${N}<domain-prefix>${B} := The domain prefix of the project branch, eg supportoca-forms-uat (default domain will be apppended)
                               At present custom domains are not supported by this script

       ${B}OPTIONS${N}

            ${B}--force${B}     Do not prompt the user before making system modifications (eg run non-interactively)

            ${B}--createdb${N}  Enable database creation and configuration (this is DISABLED by default - ie no database or users are created)

            ${B}--dry-run${N}   Just show what would be done, do not actually apply any changes to the system

            ${B}--check${N}     Just check whether the specified project (or project branch domain) is already configured
                        Note that domain-prefix checking is not yet implemented

EOD
[ -n "$STATUS" ] && exit $STATUS
[ -n "$ERROR"  ] && exit 1
exit 0
}


# Confirm (y/n) an action to the user interactively
function confirm {
	local MESSAGE="$1"
		while read -p "$MESSAGE (y/n)? " -n 1 -r; do
			echo
			case "$REPLY" in
				y|Y) return 0 ;;
				n|N) return 1 ;;
				*) "Invalid response '$REPLY'"
			esac
		done
}


# Parse argument flags
while [ "${1:0:2}" ==  "--" ]; do
        case "$1" in
                --createdb)
                        # Perform database configuration
                        FLAGS[CREATEDB]=1
                        ;;
                --force)
                        # Don't prompt for confirmation
                        FLAGS[FORCE]=1
                        ;;

                --dry-run)
                        FLAGS[DRYRUN]=1
                        ;;

                --check)
                        # Don't create anything just check if it exists
                        FLAGS[CHECK]=1
                        ;;

                --help|--usage)
                        usage
                        ;;

                --)
                        # End of arguments, exit the loop
                        shift;
                        break;
                        ;;

                --*)
                        # Unknown flag
                        usage "Unknown flag '$1' (use -- to terminate flags?)"
                        ;;
        esac
        shift
done

PROJECT="$1"
BRANCH="$2"
DOMAIN="$3"
OWPROMPT='already exists. Do you want to overwrite it?'

# Check remaining arguments
# TODO Domain suffix support
[[ $# -lt 2 ]] && usage "Too few arguments passed"
[[ $# -gt 3 ]] && usage "Too many arguments passed"
[[ ${#BRANCH} -gt 4 ]] && usage "Branch exceeds maximum length (4)"
[[ -z "$PROJECT" ]] && usage "You must specific at least a (non-empty) project name argument"
[[ -n "$BRANCH" && -z "$DOMAIN" ]] && usage "You must specify a (non-empty) domain when a branch is specified"
[[ -z "$BRANCH" && -n "$DOMAIN" ]] && usage "You must specify a (non-empty) branch when a domain is specified"
[[ "$PROJECT" =~ ^[a-z][-a-z0-9]*[a-z0-9] ]] || usage "Invalid project name '$PROJECT' ( must match [a-z][-a-z0-9]*[a-z0-9] )"
if [ -n "$BRANCH" ]; then
	[[ "$BRANCH" =~ ^[a-z][a-z0-9]+ ]] || usage "Invalid branch name '$BRANCH' ( must match [a-z][a-z0-9]+ )"
	[[ "$DOMAIN" =~ ^[a-z]+(-?[a-z0-9]+)*$ ]] || usage "Invalid domain prefix '$DOMAIN' ( must match [a-z]+(-?[a-z0-9]+)* )"
fi



# Create the system user if it's missing
if ! getent passwd "nif_${PROJECT}" >/dev/null; then
        if [ ${FLAGS[CHECK]} -eq 1 ]; then
                CHECK_OK=0
        elif [ ${FLAGS[DRYRUN]} -eq 1 ]; then
                echo -e "\\n${B}# Creating system user (dry-run)${N}"
                echo "useradd -G nif 'nif_${PROJECT}'"
        elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create system user account nif_${PROJECT}"; then
                useradd -G nif "nif_${PROJECT}"
        fi
fi

if [ ${FLAGS[CHECK]} -eq 1 ]; then
        getent passwd "nif_${PROJECT}" >/dev/null || CHECK_OK=0
elif [ ${FLAGS[DRYRUN]} -eq 1 ]; then
        echo -n
elif ! getent passwd "nif_${PROJECT}" >/dev/null; then
        echo "${B}${R}ERROR:${N} User creation skipped or failed, unable to continue"
        exit 1
fi



# Create the account directory if it's are missing to avoid apache warnings
if [ ${FLAGS[CHECK]} -eq 1 ]; then
	# Pass, we don't actually include this in the check results
	echo -n
elif [ ${FLAGS[DRYRUN]} -eq 1 ]; then
	echo -e "\\n${B}# Ensure document root directory exists to prevent apache warnings (dry-run)${N}"
	echo "runuser \"nif_${PROJECT}\" - -c 'mkdir -p \$HOME/accounts/${BRANCH}/code/dist'"
	echo "runuser \"nif_${PROJECT}\" - -c 'mkdir -p \$HOME/code/dummy/dist'"
elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create document-root holding directory"; then
	runuser "nif_${PROJECT}" - -c "mkdir -p \$HOME/accounts/${BRANCH}/code/dist"
	runuser "nif_${PROJECT}" - -c "mkdir -p \$HOME/code/dummy/dist"
fi



# Create the php-fpm-pool for the project if it's missing
function create_fpmpool () {
        pushd /etc/php-fpm-pool.d >/dev/null
        if [ ${FLAGS[CHECK]} -eq 1 ]; then
                CHECK_OK=0
        elif [ ${FLAGS[DRYRUN]} -eq 1 ]; then
                echo -e "\\n${B}# Creating php-fpm-pool configuration (dry-run)${N}"
                echo "env USER='nif_${PROJECT}' APP='nif' envsubst '\$\$USER\$\$APP' \<pool.template \>nif_${PROJECT}.conf"
                echo -e "\\n${B}# Enabling php-fpm-pool socket (dry-run)${N}"
                echo "systemctl enable --no-block php-fpm-pool@nif_${PROJECT}.socket"
                echo -e "\\n${B}# Starting php-fpm-pool socket (dry-run)${N}"
                echo "systemctl start  --no-block php-fpm-pool@nif_${PROJECT}.socket"
        elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create and start php-fpm-pool socket for 'nif_${PROJECT}'"; then
                env USER="nif_${PROJECT}" APP="nif" envsubst '$$USER$$APP' <pool.template >nif_${PROJECT}.conf
                systemctl enable --no-block php-fpm-pool@nif_${PROJECT}.socket
                systemctl start  --no-block php-fpm-pool@nif_${PROJECT}.socket
        fi
        popd >/dev/null
}

if ! [[ -f /etc/php-fpm-pool.d/nif_${PROJECT}.conf ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]]; then
	create_fpmpool
else
	if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "${Y}WARNING:${N} /etc/php-fpm-pool.d/nif_${PROJECT}.conf $OWPROMPT"; then
		create_fpmpool
	fi
fi


# Create the apache vhost for this project branch if it's missing
function create_vhost () {
	pushd /etc/httpd/vhosts.d >/dev/null

	if [ ${FLAGS[CHECK]} -eq 1 ]; then
		CHECK_OK=0
	elif [ ${FLAGS[DRYRUN]} -eq 1 ]; then
		echo -e "\\n${B}# Create the vhost configuration file for the branch (dry-run)${N}"
		echo "pushd /etc/httpd/vhosts.d >/dev/null"
		echo "env PROJECT=\"$PROJECT\" BRANCH=\"$BRANCH\" DOMAIN=\"$DOMAIN\" envsubst '\$\$PROJECT\$\$BRANCH\$\$DOMAIN' <nif_vhost.template >nif_${PROJECT}_${BRANCH}.conf"
		echo -e "\\n${B}# Test the apache configuration and load the new config on a pass (dry-run)${N}"
		echo "apachectl configtest 2>&1 | validate-config-test-result && systemctl reload httpd"
	elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create and try to load the new apache virtualhost for nif ${PROJECT} ${BRANCH} branch"; then
		env PROJECT="$PROJECT" BRANCH="$BRANCH" PREFIX="$DOMAIN" envsubst '$$PROJECT$$BRANCH$$PREFIX' <nif_vhost.template >nif_${PROJECT}_${BRANCH}.conf

		if apachectl configtest 2>&1 | grep -qvE '^Syntax OK$' || ! apachectl configtest 2>&1 | grep -qE '^Syntax OK'; then
			# Possible config problem, could be us, but either way don't reload apache!
			echo "${Y}WARNING:${N} Apache configtest failed vhost config saved as '/etc/httpd/vhosts.d/nif_${PROJECT}_${BRANCH}.conf.needs-review'" >&2
			echo "${Y}WARNING:${N} apachectl configtest gave:" >&2
			apachectl configtest 2>&1 | grep -vE 'Syntax OK' | sed -re 's/^/  > /' >&2
			mv nif_${PROJECT}_${BRANCH}.conf{,.needs-review}
			echo "${Y}WARNING:${N} VirtualHost configuration for new VirtualHost is NOT enabled, check the .needs-review config and/or apachectl configtest"
		else
			# configtest yielded 'Syntax OK' and nothing else, should be ok to run a reload.....
			systemctl reload httpd
		fi
	fi

	popd >/dev/null
}

if ! [[ -f "/etc/httpd/vhosts.d/nif_${PROJECT}_${BRANCH}.conf" ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]]; then
	create_vhost
else
	if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "${Y}WARNING:${N} /etc/http/vhosts.d/nif_${PROJECT}_${BRANCH}.conf ${OWPROMPT}"; then
		create_vhost
	fi
fi



# Create the database configuration for this project branch if it's missing
# NOTE: We escape underscores as mysql interprets these as metacharacters for LIKE (grant) matches
DBNAME="nif_${PROJECT}_${BRANCH}"
ESC_DBNAME="${DBNAME//_/\\_}"
PROJECTHOME=$(getent passwd nif_${PROJECT} | cut -d: -f6)

# Maximum database and user name length is 16 characters, automatically truncate the project name
# For uniformity reasons, the branch is assumed to be 4 characters long (the maximum)
if [[ ${#DBNAME} -ge 16 ]]; then
	TRUNCATE_DB=1
	# We know that 'nif_' (4), plus $BRANCH (max 4), plus the user suffix (1) will never exceed a total of 9 characters
	# So we need to truncate $PROJECT to 6 characters (for a total of 15, allowing +1 for the user suffix)
	ORIG_DBNAME=$DBNAME
	DBNAME="nif_${PROJECT:0:6}_${BRANCH}"
	ESC_DBNAME="${DBNAME//_/\\_}"
fi

# Check if database exists only after we've performed any possible truncation of name
DBEXISTS=$(mysql -NB -e "SHOW DATABASES LIKE '${ESC_DBNAME}'")
# Try to determine the database host, or just use localhost if its unset
DBHOST=$(my_print_defaults client | grep '^--host=.*$' | tail -n1 | sed -re 's/--host=//g')
if [[ -z "$DBHOST" ]]; then
	DBHOST='localhost'
fi

# Mysql Read-only (RO) credentials
READUSER_NAME="${DBNAME}?"
READUSER_PASSWD=$(/usr/libexec/ng-server-config/mk-auth-token "mysql:${READUSER_NAME}" 16)}

# Mysql Web (operational) credentials
OPSUSER_NAME="${DBNAME}$"
OPSUSER_PASSWD=$(/usr/libexec/ng-server-config/mk-auth-token "mysql:${OPSUSER_NAME}" 16)

# Mysql Admin (dba) credentials
if [[ $TRUNCATE_DB -eq 1 ]]; then
	# If we truncated the database name then the project variable (for the admin_user name) should also be shortened
	ADMINUSER_NAME="nif_${PROJECT:0:6}#"
else
	ADMINUSER_NAME="nif_${PROJECT}#"
fi

ESC_ADMINUSER_NAME=${ADMINUSER_NAME//#/\\\#}
ADMINUSER_PASSWD=$(/usr/libexec/ng-server-config/mk-auth-token "mysql:nif_${PROJECT}" 16)

# We'll be needing this later..
MYSQL_BRANCHDIR="$PROJECTHOME/.my.cnf.d"

# Dry run mode
if [[ ${FLAGS[DRYRUN]} -eq 1 ]]; then

	echo -e "\\n${B}# Create the database (dry-run)${N}"
	echo "env HOME=/root mysql <<-EOD"
	echo "CREATE DATABASE \`${DBNAME}\`;"
	echo EOD

	echo -e "\\n${B}# Create the MySQL admin user and set grants (dry-run)${N}"
	echo "env HOME=/root mysql -NB -e \"CREATE USER '${ESC_ADMINUSER_NAME}'@'${DBHOST}' IDENTIFIED BY '${ADMINUSER_PASSWD}'\""
	echo "env HOME=/root mysql -NB -e \"GRANT REPLICATION CLIENT ON *.* TO '${ADMINUSER_NAME}'@'${DBHOST}'\""
	echo "env HOME=/root mysql -NB -e \"GRANT ALTER, CREATE, CREATE TEMPORARY TABLES, DELETE, DROP, INDEX, INSERT, LOCK TABLES, REFERENCES, SELECT, UPDATE ON \`${ESC_DBNAME%_*}_%\`.* TO '${ADMINUSER_NAME}'@'${DBHOST}'\""

	echo -e "\\n${B}# Create the MySQL web user and set grants (dry-run)${N}"
	echo "env HOME=/root mysql -NB -e \"CREATE USER '${OPSUSER_NAME}'@'${DBHOST}' IDENTIFIED BY '${OPSUSER_PASSWD}'\""
	echo "env HOME=/root mysql -NB -e \"GRANT REPLICATION CLIENT ON *.* TO '${OPSUSER_NAME}'@'${DBHOST}'\""
	echo "env HOME=/root mysql -NB -e \"GRANT DELETE, INSERT, SELECT, UPDATE ON \`${ESC_DBNAME}\`.* TO '${OPSUSER_NAME}'@'${DBHOST}'\""

	echo -e "\\n${B}# Create the MySQL Read Only user and set grants (dry-run)${N}"
	echo "env HOME=/root mysql -NB -e \"CREATE USER '${READUSER_NAME}'@'${DBHOST}' IDENTIFIED BY '${READUSER_PASSWD}'\""
	echo "env HOME=/root mysql -NB -e \"GRANT CREATE GRANT REPLICATION CLIENT ON *.* TO '${READUSER_NAME}'@'${DBHOST}'\""
	echo "env HOME=/root mysql -NB -e \"GRANT SELECT ON \`${ESC_DBNAME}\`.* TO '${READUSER_NAME}'@'${DBHOST}'\""

	echo -e "\\n${B}# Create the MySQL credentials file "[\$HOME/.my.cnf]" (dry-run)${N}"
	echo "runuser \"nif_${PROJECT}\" - -c 'mkdir -p ${MYSQL_BRANCHDIR}'"
	echo ""
	echo "[${ADMINUSER_NAME}]"
	echo "user=${ADMINUSER_NAME}"
	echo "password=\"${ADMINUSER_PASSWD}\""
	echo ""
	echo "[${OPSUSER_NAME}]"
	echo "user=${OPSUSER_NAME}"
	echo "password=\"${OPSUSER_PASSWD}\""
	echo "database=${DBNAME}"
	echo ""
	echo "[${READUSER_NAME}]"
	echo "user=${READUSER_NAME}"
	echo "password=\"${READUSER_PASSWD}\""
	echo "database=${DBNAME}"
	echo "EOD'"
	echo ""
fi

# The real thing
if [[ ${FLAGS[DRYRUN]} -eq 0 ]] && [[ ${FLAGS[CREATEDB]} -eq 1 ]] ; then
	if [[ -z $DBEXISTS ]] || [[ ${FLAGS[FORCE]} -eq 1 ]]; then
		if [[ $TRUNCATE_DB -eq 1 ]]; then
			echo "${Y}WARNING:${N} Database name of ${B}$ORIG_DBNAME${N} exceeds (or is equal to) the maximum allowed length ${B}(16)${N} and has been truncated to ${B}$DBNAME${N}"
		fi

		if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create database ${DBNAME}?"; then
			# Create database
			env HOME=/root mysql <<-EOD
				CREATE DATABASE \`${DBNAME}\`;
			EOD
		fi

		# INFO: Do not create users if they dont already exist (mysql throws an error)
		if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create the MySQL admin user ${ADMINUSER_NAME} and set grants?"; then
			ADMINUSEREXISTS=$(mysql -NB -e "select User from mysql.user WHERE User = '${ADMINUSER_NAME}' AND Host = '${DBHOST}'")
			if [[ -z $ADMINUSEREXISTS ]]; then
				ADMIN_DBNAME=${ESC_DBNAME%\\*}
				# The use of single quotes and back ticks is intentional otherwise we run into syntax errors on the database and user names
				env HOME=/root mysql -NB -e "CREATE USER '${ESC_ADMINUSER_NAME}'@'${DBHOST}' IDENTIFIED BY '${ADMINUSER_PASSWD}'"
				env HOME=/root mysql -NB -e "GRANT REPLICATION CLIENT ON *.* TO '${ESC_ADMINUSER_NAME}'@'${DBHOST}'"
				env HOME=/root mysql -NB -e "GRANT ALTER, CREATE, CREATE TEMPORARY TABLES, DELETE, DROP, INDEX, INSERT, LOCK TABLES, REFERENCES, SELECT, UPDATE ON  \`${ADMIN_DBNAME}\\_%\`.* TO '${ADMINUSER_NAME}'@'${DBHOST}'"
			else
				echo "${B}INFO:${N} MySQL user ${ADMINUSER_NAME} already exists (this is expected if you are adding branches to an existing project), skipping user creation and configuration"
			fi
		fi

		if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create the MySQL web user ${OPSUSER_NAME} and set grants?"; then
			OPSUSEREXISTS=$(mysql -NB -e "select User from mysql.user WHERE User = '${OPSUSER_NAME}' AND Host = '${DBHOST}'")
			if [[ -z $OPSUSEREXISTS ]]; then
				env HOME=/root mysql -NB -e "CREATE USER '${OPSUSER_NAME}'@'${DBHOST}' IDENTIFIED BY '${OPSUSER_PASSWD}'"
				env HOME=/root mysql -NB -e "GRANT REPLICATION CLIENT ON *.* TO '${OPSUSER_NAME}'@'${DBHOST}'"
				env HOME=/root mysql -NB -e "GRANT DELETE, INSERT, SELECT, UPDATE ON \`${ESC_DBNAME}\`.* TO '${OPSUSER_NAME}'@'${DBHOST}'"
			else
				echo "${B}INFO:${N} MySQL user ${OPSUSER_NAME} already exists, skipping user creation and configuration"
			fi
		fi

		if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create the MySQL read only user ${READUSER_NAME} and set grants?"; then
			READUSEREXISTS=$(mysql -NB -e "select User from mysql.user WHERE User = '${READUSER_NAME}' AND Host = '${DBHOST}'")
			if [[ -z $READUSEREXISTS ]]; then
				env HOME=/root mysql -NB -e "CREATE USER '${READUSER_NAME}'@'${DBHOST}' IDENTIFIED BY '${READUSER_PASSWD}'"
				env HOME=/root mysql -NB -e "GRANT REPLICATION CLIENT ON *.* TO '${READUSER_NAME}'@'${DBHOST}'"
				env HOME=/root mysql -NB -e "GRANT SELECT ON \`${ESC_DBNAME}\`.* TO '${READUSER_NAME}'@'${DBHOST}'"
			else
				echo "${B}INFO:${N} MySQL user ${READUSER_NAME} already exists, skipping user creation and configuration"
			fi
		fi

		# Create the MySQL Admin (DBA) credentials file if it doesn't already exist
		MYSQL_ADMINCRED="$PROJECTHOME/.my.cnf"
		function create_mysqladmincred () {
			runuser "nif_${PROJECT}" - -c \
			"cat <<-EOD >$PROJECTHOME/.my.cnf
			# Each user listed below is accessible by using the following command
			# env MYSQL_GROUP_SUFFIX=_FOO mysql
			# Where FOO is the value of 'user=' below

			# Administrative (DBA) MySQL user
			# All DDL and DML against all databases of this project
			[client_${ADMINUSER_NAME}]
			user=\"${ADMINUSER_NAME}\"
			password=\"${ADMINUSER_PASSWD}\"

			# Include all .cnf files in the project home directory
			# Covers all potential branch configuration
			!includedir $MYSQL_BRANCHDIR
			EOD"
		}

		# Create the MySQL (per-branch) credentials file
		runuser "nif_${PROJECT}" - -c "mkdir -p ${MYSQL_BRANCHDIR}"

		MYSQL_BRANCHCRED="$MYSQL_BRANCHDIR/${BRANCH}-my.cnf"
		function create_mysqlbranchcred () {
			runuser "nif_${PROJECT}" - -c \
			"cat <<-EOD >$MYSQL_BRANCHCRED
			# Each user listed below is accessible by using the following command
			# env MYSQL_GROUP_SUFFIX=_FOO mysql
			# Where FOO is the value of 'user=' below

			# Operational MySQL user
			# Read/Write/Upgrade/Delete (DML) against the branch denoted below
			[client_${OPSUSER_NAME}]
			user=\"${OPSUSER_NAME}\"
			password=\"${OPSUSER_PASSWD}\"
			database=\"${DBNAME}\"

			# RO Mysql user
			# Read only (DQL) against the branch denoted below
			[client_${READUSER_NAME}]
			user=\"${READUSER_NAME}\"
			password=\"${READUSER_PASSWD}\"
			database=\"${DBNAME}\"
			EOD"
		}

		if ! [[ -f $MYSQL_ADMINCRED ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]] || [[ ${FLAGS[FORCE]} -eq 1 ]]; then
			create_mysqladmincred
		else
			echo "${B}INFO:${N} MySQL admin cnf file ${MYSQL_ADMINCRED} already exists, skipping creation of this file"
		fi

		if ! [[ -f $MYSQL_BRANCHCRED ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]]; then
			create_mysqlbranchcred
		elif [[ ${FLAGS[FORCE]} -eq 1 ]] || confirm "${Y}WARNING:${N} $MYSQL_BRANCHCRED ${OWPROMPT}"; then
			create_mysqlbranchcred
		fi
	else
		# Database already exists
		echo "${Y}WARNING:${N} Database ${B}${DBNAME}${N} already exists. No database configuration will be performed."
		if [ ${FLAGS[CHECK]} -eq 1 ]; then
			CHECK_OK=0
		fi
	fi
fi
