#!/bin/bash
#
# Create a Nogggin OCA Public (OP) system account and or branch
#
# TODO
#  * Support SNI domains (non-prefix)
#
#

B=
R=
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=(
        [CREATEALL]=0
        [CREATEUSER]=0
        [CREATEWEB]=0
        [CREATEDB]=0
        [FORCE]=0
        [DRYRUN]=0
        [CHECK]=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") [mode] [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 'op_' 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}MODE${N}

            ${B}--createall${N}   Create all configuration (equivalent to specifying --createuser --createweb --createdb)  
                                 
            ${B}--createuser${N}  Create user configuration only (user account and home directory)
                                 
            ${B}--createweb${N}   Create web configuration only (apache vhost and php-fpm pool)  
                                 
            ${B}--createdb${N}    Create database configuration only (mysql database and users)   
       
       
       ${B}OPTIONS${N}

            ${B}--force${B}    Do not prompt the user before making system modifications (eg run non-interactively)
            
            ${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
                --createall)
                        # Don't prompt for confirmation
                        FLAGS[CREATEALL]=1
                        ;;

                --createuser)
                        # Don't prompt for confirmation
                        FLAGS[CREATEUSER]=1
                        ;;

                --createweb)
                        # Don't prompt for confirmation
                        FLAGS[CREATEWEB]=1
                        ;;
                        
                --createdb)
                        # Don't prompt for confirmation
                        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?'


# Validate mode
[[ "FLAGS[CREATEALL]" -eq 0 ]] && [[ "FLAGS[CREATEUSER]" -eq 0 ]] && [[ "FLAGS[CREATEWEB]" -eq 0 ]] && [[ "FLAGS[CREATEDB]" -eq 0 ]] && usage "You must specify at least ONE mode"


# 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 [[ ${FLAGS[CREATEALL]} -eq 1 ]] || [[ ${FLAGS[CREATEUSER]} -eq 1 ]]; then
    if ! getent passwd "op_${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 op 'op_${PROJECT}'"
            elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create system user account op_${PROJECT}"; then
                    useradd -G op "op_${PROJECT}"
            fi
    fi

    if [ ${FLAGS[CHECK]} -eq 1 ]; then
            getent passwd "op_${PROJECT}" >/dev/null || CHECK_OK=0
    elif [ ${FLAGS[DRYRUN]} -eq 1 ]; then
            echo -n
    elif ! getent passwd "op_${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 \"op_${PROJECT}\" - -c 'mkdir -p \$HOME/accounts/${BRANCH}/code/web'"
        echo "runuser \"op_${PROJECT}\" - -c 'mkdir -p \$HOME/code'"
    elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create document-root holding directory"; then
        runuser "op_${PROJECT}" - -c "mkdir -p \$HOME/accounts/${BRANCH}/code/web"
        runuser "op_${PROJECT}" - -c "mkdir -p \$HOME/code"
    fi
fi



if [[ ${FLAGS[CREATEALL]} -eq 1 ]] || [[ ${FLAGS[CREATEWEB]} -eq 1 ]]; then
    # 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='op_${PROJECT}' APP='op' envsubst '\$\$USER\$\$APP' \<pool.template \>op_${PROJECT}.conf"
                    echo -e "\\n${B}# Enabling php-fpm-pool socket (dry-run)${N}"
                    echo "systemctl enable --no-block php-fpm-pool@op_${PROJECT}.socket"
                    echo -e "\\n${B}# Starting php-fpm-pool socket (dry-run)${N}"
                    echo "systemctl start  --no-block php-fpm-pool@op_${PROJECT}.socket"
            elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create and start php-fpm-pool socket for 'op_${PROJECT}'"; then
                    env USER="op_${PROJECT}" APP="op" envsubst '$$USER$$APP' <pool.template >op_${PROJECT}.conf
                    systemctl enable --no-block php-fpm-pool@op_${PROJECT}.socket
                    systemctl start  --no-block php-fpm-pool@op_${PROJECT}.socket
            fi
            popd >/dev/null
    }

    if ! [[ -f /etc/php-fpm-pool.d/op_${PROJECT}.conf ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]]; then
        create_fpmpool
    else
        if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "${Y}WARNING:${N} /etc/php-fpm-pool.d/op_${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' <op_vhost.template >op_${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 --no-block reload httpd"
        elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create and try to load the new apache virtualhost for op ${PROJECT} ${BRANCH} branch"; then
            env PROJECT="$PROJECT" BRANCH="$BRANCH" PREFIX="$DOMAIN" envsubst '$$PROJECT$$BRANCH$$PREFIX' <op_vhost.template >op_${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/op_${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 op_${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 --no-block reload httpd
            fi
        fi

        popd >/dev/null
    }

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



if [[ ${FLAGS[CREATEALL]} -eq 1 ]] || [[ ${FLAGS[CREATEDB]} -eq 1 ]]; then
	# 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="op_${PROJECT}_${BRANCH}"
	ESC_DBNAME="${DBNAME//_/\\_}"
	PROJECTHOME=$(getent passwd op_${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 'op_' (3), plus $BRANCH (max 4), plus the user suffix (1) will never exceed a total of 8 characters
		# So we need to truncate $PROJECT to 7 characters (for a total of 15, allowing +1 for the user suffix)
		ORIG_DBNAME=$DBNAME
		DBNAME="op_${PROJECT:0:7}_${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 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="op_${PROJECT:0:7}#"
	else
		ADMINUSER_NAME="op_${PROJECT}#"
	fi

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

	# 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 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 SELECT, INSERT, UPDATE, DELETE, DROP, CREATE TEMPORARY TABLES, EXECUTE ON \`${ESC_DBNAME}\`.* TO '${OPSUSER_NAME}'@'${DBHOST}'\""

		echo -e "\\n${B}# Create the per-branch (web user) ini (dry-run)${N}"
		echo "runuser \"op_${PROJECT}\" - -c 'cat <<-EOD >\$HOME/accounts/${BRANCH}/config.ini"
			echo "[Database]"
			echo "Host     = \"${DBHOST}\""
			echo "Type     = \"MySQL\""
			echo "User     = \"${OPSUSER_NAME}\""
			echo "Password = \"${OPSUSER_PASSWD}\""
			echo "Name     = \"${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 SUPER ON *.* TO '${ESC_ADMINUSER_NAME}'@'${DBHOST}'\""
		echo "env HOME=/root mysql -NB -e \"GRANT ALL PRIVILEGES ON \`${ESC_DBNAME%_*}_%\`.* TO '${ADMINUSER_NAME}'@'${DBHOST}'\""

		echo -e "\\n${B}# Create the per-project admin user credentials (dry-run)${N}"
		echo "runuser \"op_${PROJECT}\" - -c 'cat <<-EOD >\$HOME/.my.cnf"
			echo "[mysql]"
			echo "database=${DBNAME}"

		echo "[client]"
		echo "user=${ADMINUSER_NAME}"
		echo "password=${ADMINUSER_PASSWD}"
		echo "EOD'"
		echo ""
	fi

	# The real thing
	if ! [[ ${FLAGS[DRYRUN]} -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

			if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create the MySQL web user ${OPSUSER_NAME}, set grants and create config.ini?"; then

				# Do not create user if it already exist (since this throws an error)
				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 SELECT, INSERT, UPDATE, DELETE, DROP, CREATE TEMPORARY TABLES, EXECUTE ON \`${ESC_DBNAME}\`.* TO '${OPSUSER_NAME}'@'${DBHOST}'"
				else
					echo "${B}INFO:${N} MySQL user ${OPSUSER_NAME} already exists, skipping user creation and configuration"
				fi
				
				# Create the per-branch (web user) ini *but avoid overwriting existing config*
				OPSCONFIG="$PROJECTHOME/accounts/${BRANCH}/config.ini"
				function create_opsconfig () {
					runuser "op_${PROJECT}" - -c \
					"cat <<-EOD >$OPSCONFIG
					[Database]
					Host     = \"${DBHOST}\"
					Type     = \"MySQL\"
					User     = \"${OPSUSER_NAME}\"
					Password = \"${OPSUSER_PASSWD}\"
					Name     = \"${DBNAME}\"
					EOD"
				}

				if ! [[ -f $OPSCONFIG ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]]; then
					create_opsconfig
				elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "${Y}WARNING:${N} $OPSCONFIG ${OWPROMPT}"; then
						create_opsconfig
				fi
			fi

			if [ ${FLAGS[FORCE]} -eq 1 ] || confirm "Create the MySQL admin user ${ADMINUSER_NAME}, set grants and create .my.cnf?"; then

				# Create admin user and set grants (this user is SHARED by all branches)
				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 SUPER ON *.* TO '${ESC_ADMINUSER_NAME}'@'${DBHOST}'"
					env HOME=/root mysql -NB -e "GRANT ALL PRIVILEGES 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
			
			# Create the per-project admin user credentials if they don't already exist
			ADMINCONFIG="$PROJECTHOME/.my.cnf"
			function create_adminconfig () {
				runuser "op_${PROJECT}" - -c \
				"cat <<-EOD >$PROJECTHOME/.my.cnf
				[mysql]
				database=\"${DBNAME}\"
			
				[client]
				user=\"${ADMINUSER_NAME}\"
				password=\"${ADMINUSER_PASSWD}\"
				EOD"
			}

			if ! [[ -f $ADMINCONFIG ]] || [[ ${FLAGS[DRYRUN]} -eq 1  ]]; then
				create_adminconfig
			elif [ ${FLAGS[FORCE]} -eq 1 ] || confirm "${Y}WARNING:${N} $ADMINCONFIG ${OWPROMPT} (this is expected if you are adding new branches to an existing project)"; then
				create_adminconfig
			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
fi
