#!/bin/bash
# -*- mode: sh; c-basic-offset: 4; tab-width: 8; indent-tabs-mode: nil -*-
# vi: set shiftwidth=4 tabstop=8 softtabstop=4 expandtab:
# :indentSize=4:tabSize=8:noTabs=true:
# vim: filetype=sh

# ---- error handling
set -o errexit;
set -o posix;
set -o pipefail;
set -o errtrace;
set -o nounset;

unexpected_error() {
    local errstat=$?
    echo "${g_prog:-$(basename "$0")}: Error! Encountered unexpected error at 'line $(caller)', bailing out..." 1>&2
    exit $errstat;
}
trap unexpected_error ERR;

# ---- error functions

merror() {
    echo "$g_prog: Error! ""$@" 1>&2;
    exit 1;
}
minterror() {
    echo "$g_prog: Internal Error! ""$@" 1>&2;
    exit 1;
}
mwarn() {
    echo "$g_prog: Warning! ""$@" 1>&2;
}

# ---- usage

usage() {
    local exitstat=2;
    if [[ ! -z "${2:-}" ]] ; then
        if [[ ! $2 =~ [[:digit:]]+ ]] ; then
            minterror "usage(): exitstat ($2) must be numeric."
        fi
        exitstat=$2;
    fi

    # Only redirect to stderr on non-zero exit status
    if [[ $exitstat -ne 0 ]] ; then
        exec 1>&2;
    fi

    if [[ ! -z "${1:-}" ]] ; then
        echo "$g_prog: Error! $1"
    fi

    echo "Usage: $g_prog [--help] -u|--user admin|pbicsuser -p|--password passwd"
    echo "       $g_prog [--help] -u|--user admin|pbicsuser"
    echo "       PASSWD=\"passwd\" $g_prog [--help] -u|--user admin|pbicsuser"
    echo ""
    echo "         -u|--user     -- WSO2 Username ('admin' or 'pbicsuser' only)"
    echo "         -p|--password -- New WSO2 Password"
    echo "         -h|--help     -- print this usage";
    echo "";

    if [[ $exitstat -ne 0 ]] ; then
        # Print error again, useful for long usages messages
        if [[ ! -z "${1:-}" ]] ; then
            echo ""
            echo "$g_prog: Error! $1"
        fi
    fi

    # bash only:
    if [[ $exitstat -ne 0 ]] ; then
        echo "  at: $(caller)";
    fi
    exit $exitstat;
}

# ---- argument parsing

achk() {
    if [[ $1 -eq 0 ]] ; then usage "Missing argument to $2 option"; fi
}

parseargs() {
    opt_user="";
    opt_password="";


    while [[ $# != 0 ]]; do
        opt="$1"; shift;
        case "$opt" in
            # Flag with no argument example:
            #   --flag|--fla|--fl|--f)
            #     opt_flag=true;;
            # Option with argument example:
            #   --arg|--ar|--a)
            #     achk $# $opt; opt_somearg=$1; shift;;
            --bundle) opt_bundle=true;;
            --upload) opt_upload=true;;
            -u|--user) achk $# $opt; opt_user=$1; shift;;
            -p|--password) achk $# $opt; opt_password=$1; shift;;
            -h|-help|--help|--hel|--he|--h) usage "" 0;;
            -*) usage "Unrecognized option: $opt";;
            *)
                local argstr=${1+"$@"};
                usage "Unexpected extraneous arguments detected: $opt $argstr"
                ;;
        esac
    done

    if  [[ -z "$opt_user" ]] ; then
        usage "Must specify the --user option"
    fi

    if  [[ x"$opt_user" != x"admin" ]] &&
        [[ x"$opt_user" != x"pbicsuser" ]]; then
        usage "Argument to --user option must be either 'admin' or 'pbicsuser'"
    fi
}

# ---- globals

set_preglobals() {
    # Force the path to only what we need (/sbin needed for ifconfig)
    PATH_ORIG=$PATH
    PATH="/usr/bin:/bin"

    g_prog=$(basename "$0");
    g_progdir=$(dirname "$0");
    g_progdir_abs=$(dirname "$(readlink -f "$0")");

    # ---- global env

    . "$g_progdir_abs/../../private/runtime-common/lib/globalenv.ish"
}

set_globals() {
    g_topdir=$(readlink -f "$g_progdir_abs/../..")
    g_javahome="$g_topdir/bundles/smrtlink-analysisservices-gui/current/private/thirdparty/java/jre/jre_8u192b12-hotspot"

    g_wso2_rootdir="$g_topdir/bundles/smrtlink-analysisservices-gui/current/private/pacbio/smrtlink-analysisservices-gui/wso2am-2.0.0"

    g_wso2_chpasswd_exe="$g_wso2_rootdir/bin/chpasswd.sh"
    g_wso2_pidfile="$g_wso2_rootdir/wso2carbon.pid"

    g_python3_exe="$g_topdir/bundles/smrttools/current/private/thirdparty/python3/python3_3.7.3/binwrap/python3"

    g_userdata_dir="$g_topdir/../../userdata"
    g_serverconfig_file="$g_userdata_dir/generated/config/smrtlink-system-config.json"

    g_dbhelper_exe="$g_progdir/dbhelper"


    g_admin_passwordminlen=5;
    g_admin_passwordmaxlen=80;
    g_admin_passwordlegalchar_re='^[]a-zA-Z0-9\`\~\!\@\#\$\%\^\&\*'\''\(\)\[\{\_\+\=\\\|\;\"\,\<\>\.\/\?\;-]+$'
    g_admin_passworderrmsg="Only printable ascii characters allowed (excluding white space, ':' and '}')."
    g_admin_credfile="$g_topdir/bundles/smrtlink-analysisservices-gui/current/private/pacbio/smrtlink-analysisservices-gui/wso2-credentials.json"

    g_pbicsuser_passwordminlen=5;
    g_pbicsuser_passwordmaxlen=80;
    g_pbicsuser_passwordlegalchar_re='^[]a-zA-Z0-9\`\~\!\@\#\$\%\^\&\*'\''\(\)\[\{\_\+\=\\\|\;\"\,\<\>\.\/\?\;\:\}-]+$'
    g_pbicsuser_passworderrmsg="Only printable ascii characters allowed (excluding white space)."
    g_pbicsuser_credfile="$g_topdir/bundles/smrtlink-analysisservices-gui/current/private/pacbio/smrtlink-analysisservices-gui/ics-default-credentials.json"


}


# ---- subroutines

# This fail_if_stderr() function will generate a 125 exit status if a command
# exits with zero status, but does have output on stderr.  Any non-zero exit
# status will be preserved
#
# This seems to be the behavior of the chpasswd.sh script. Errors in reading
# the user-mgt.xml, like encountering a unencoded '&' in the admin password,
# will print a Fatal Error to stderr, but exit with 0 status.
#
# This function basically taken from this question/answer on stackexchange:
#    https://unix.stackexchange.com/questions/387897/is-there-a-simple-test-for-anything-printed-on-stderr-in-shell-bash
#    https://unix.stackexchange.com/a/387918
fail_if_stderr() (
    err=0;
    rc=$({
        ("$@" 2>&1 >&3 3>&- 4>&-; echo "$?" >&4) |
        grep '^' >&2 3>&- 4>&-
    } 4>&1) || err=$?
    [[ "$rc" -eq 0 ]] || exit "$rc"
    [[ "$err" -ne 0 ]] || exit 125
) 3>&1

# ---- main

# Save off original cmdline args, use as "${g_origargs[@]}" (with doublequotes)
g_origargs=( ${1+"$@"} )

set_preglobals ${1+"$@"};
parseargs ${1+"$@"};
set_globals;

newpassword=$opt_password;
if [[ -z "$opt_password" ]] ; then
    if [[ ! -z "${PASSWD+set}" ]] ; then
        newpassword=$PASSWD;
    else
        read -s -p "New Password: " newpassword
    fi
fi



if [[ x"$opt_user" == x"admin" ]] ; then
    passwordminlen=$g_admin_passwordminlen
    passwordmaxlen=$g_admin_passwordmaxlen
    passwordlegalchar_re=$g_admin_passwordlegalchar_re
    passworderrmsg=$g_admin_passworderrmsg
    credfile=$g_admin_credfile
elif [[ x"$opt_user" == x"pbicsuser" ]] ; then
    passwordminlen=$g_pbicsuser_passwordminlen
    passwordmaxlen=$g_pbicsuser_passwordmaxlen
    passwordlegalchar_re=$g_pbicsuser_passwordlegalchar_re
    passworderrmsg=$g_pbicsuser_passworderrmsg
    credfile=$g_pbicsuser_credfile
else
    minterror "Unsupported value for --user option '$opt_user;"
fi

# Password sanity check (min len, max len, illegal characters)
if [[ ${#newpassword} -lt $passwordminlen ]] ; then
    merror "New password must be at least $passwordminlen characters";
fi
if [[ ${#newpassword} -gt $passwordmaxlen ]] ; then
    merror "New password must be no more than $passwordmaxlen characters";
fi
if [[ ! $newpassword =~ ${passwordlegalchar_re} ]]; then
    merror "Illegal characters in '$opt_user' password.  $passworderrmsg"
fi


# Need to make sure that wso2 is not running, and that database is running

# Check to see that wso2 is not running

if [[ -f "$g_wso2_pidfile" ]] ; then
    wso2_pid=$( sed -e 's/\x0//g' "${g_wso2_pidfile}" )
    if [[ ${wso2_pid} =~ ^[0-9]+$ ]] ; then
        if kill -0 "$wso2_pid" > /dev/null 2>&1 ; then
            merror "WSO2 server is running.  Stop services and retry (run: '$g_progdir/services-stop')"
        fi
    fi
fi

# Start database if needed
stat=0
"$g_dbhelper_exe" --status > /dev/null 2>&1 || stat=$?

# Determine database port
db_port=$("$g_python3_exe" -c "import json; d = json.load(open('$g_serverconfig_file')); print(d['smrtflow']['db']['properties']['portNumber'])")
if [[ -z "$db_port" ]] ; then
    merror "Could not determine database port."
fi
db_user=$("$g_python3_exe" -c "import json; d = json.load(open('$g_serverconfig_file')); print(d['smrtflow']['db']['properties']['user'])")
if [[ -z "$db_port" ]] ; then
    merror "Could not determine database user."
fi

g_db_was_running=false;
if [[ $stat -eq 0 ]] ; then
    g_db_was_running=true;
elif [[ $stat -eq 3 ]] ; then
    g_db_was_running=false;
    # Start database
    "$g_dbhelper_exe" --start
else
    merror "Unexpected exit code ($stat) in checking database status ('$g_dbhelper_exe --status')"
fi


echo "  Setting WSO2 '$opt_user' password..."
stat=0
stderrout=$(JAVA_HOME="$g_javahome" JAVA_OPTS="-Djava.library.path=" \
                fail_if_stderr bash "$g_wso2_chpasswd_exe" \
                    --db-url "jdbc:postgresql://localhost:${db_port}/wso2am" \
                    --db-driver "org.postgresql.Driver" \
                    --db-username "$db_user" \
                    --username "$opt_user" --new-password "$newpassword") || stat=$?

if [[ $stat -ne 0 ]] ; then
    # In case of an error, print out everything we got on stdout and stderr,
    # with the exception of the two (presumably innocuous) WARNs we expect
    # to see.
    echo "$stderrout" | \
        grep -v -E 'WARN.*Unable to determine dialect of the StAX implementation' | \
        grep -v -E 'WARN.*Required property DomainName missing in secondary user store'
    echo
fi

# Stop database if it was not running
if ! $g_db_was_running; then
    # Stop database
    "$g_dbhelper_exe" --stop
fi

if [[ $stat -ne 0 ]] ; then
    merror "Could not set WSO2 password for user '$opt_user'."
fi

echo "  Updating credential file..."
tmp_credfile="${credfile}.tmp"
stat=0;
(   rm -f "$tmp_credfile" &&
    "$g_python3_exe" -c 'import sys; import json; json_str = json.dumps({"wso2User": sys.argv[1], "wso2Password": sys.argv[2]}, indent=4, sort_keys=True, separators=(",", ": ")); print(json_str)' "$opt_user" "$newpassword" > "$tmp_credfile" &&
    chmod 600  "$tmp_credfile" &&
    rm -f "$credfile" &&
    mv "$tmp_credfile" "$credfile") || stat=$?

if [[ $stat -ne 0 ]] ; then
    echo
    merror "Could not modify WSO2 configuration files."
fi

echo "Successfully updated password for WSO2 user '$opt_user'."
exit 0;
