#!/bin/sh

# define default configuration
POLL=true
ERL_MAX_PORTS=32000
ERL_PROCESSES=250000
ERL_MAX_ETS_TABLES=1400
FIREWALL_WINDOW=""
INET_DIST_INTERFACE=""
ERLANG_NODE=ejabberd@localhost

# define default environment variables
[ -z "$SCRIPT" ] && SCRIPT=$0
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)"
# shellcheck disable=SC2034
# ERTS_VSN="{{erts_vsn}}"
ERL="/usr/local/bin/erl28"
EPMD="/usr/local/bin/epmd28"
IEX=""
INSTALLUSER="_ejabberd"

# check the proper system user is used
case $(id -un) in
    "$INSTALLUSER")
        EXEC_CMD="as_current_user"
        ;;
    root)
        if [ -n "$INSTALLUSER" ] ; then
            EXEC_CMD="as_install_user"
        else
            EXEC_CMD="as_current_user"
            echo "WARNING: It is not recommended to run ejabberd as root" >&2
        fi
        ;;
    *)
        if [ -n "$INSTALLUSER" ] ; then
            echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2
            exit 7
        else
            EXEC_CMD="as_current_user"
        fi
        ;;
esac

# parse command line parameters
while [ $# -gt 0 ]; do
    case $1 in
        -n|--node) ERLANG_NODE_ARG=$2; shift 2;;
        -s|--spool) SPOOL_DIR=$2; shift 2;;
        -l|--logs) LOGS_DIR=$2; shift 2;;
        -f|--config) EJABBERD_CONFIG_PATH=$2; shift 2;;
        -c|--ctl-config) EJABBERDCTL_CONFIG_PATH=$2; shift 2;;
        -d|--config-dir) CONFIG_DIR=$2; shift 2;;
        -t|--no-timeout) NO_TIMEOUT="--no-timeout"; shift;;
        *) break;;
    esac
done

# define ejabberd variables if not already defined from the command line
: "${CONFIG_DIR:="/etc/ejabberd"}"
: "${LOGS_DIR:="/var/log/ejabberd"}"
: "${EJABBERD_CONFIG_PATH:="$CONFIG_DIR/ejabberd.yml"}"
: "${EJABBERDCTL_CONFIG_PATH:="$CONFIG_DIR/ejabberdctl.cfg"}"
# Allows passing extra Erlang command-line arguments in vm.args file
: "${VMARGS:="$CONFIG_DIR/vm.args"}"
# shellcheck source=ejabberdctl.cfg.example
[ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH"
[ -n "$ERLANG_NODE_ARG" ] && ERLANG_NODE="$ERLANG_NODE_ARG"
[ "$ERLANG_NODE" = "${ERLANG_NODE%@*}" ] && ERLANG_NODE="$ERLANG_NODE@$(hostname -s)"
[ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s"
: "${SPOOL_DIR:="/var/db/ejabberd"}"
: "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}"

# backward support for old mnesia spool dir path
: "${SPOOL_DIR_OLD:="$SPOOL_DIR/$ERLANG_NODE"}"
[ -r "$SPOOL_DIR_OLD/schema.DAT" ] && [ ! -r "$SPOOL_DIR/schema.DAT" ] && SPOOL_DIR="$SPOOL_DIR_OLD"

# define erl parameters
ERLANG_OPTS="+K $POLL +P $ERL_PROCESSES $ERL_OPTIONS"
if [ -n "$FIREWALL_WINDOW" ] ; then
    ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}"
fi
if [ -n "$INET_DIST_INTERFACE" ] ; then
    INET_DIST_INTERFACE2=$("$ERL" $ERLANG_OPTS -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt)
    if [ -n "$INET_DIST_INTERFACE2" ] ; then
        if [ "$(echo "$INET_DIST_INTERFACE2" | grep -o "," | wc -l)" -eq 7 ] ; then
            INET_DIST_INTERFACE2="$INET_DIST_INTERFACE2 -proto_dist inet6_tcp"
        fi
        ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2"
    fi
fi
[ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -erl_epmd_port $ERL_DIST_PORT -start_epmd false"
# if vm.args file exists in config directory, pass it to Erlang VM
[ -f "$VMARGS" ] && ERLANG_OPTS="$ERLANG_OPTS -args_file $VMARGS"
ERL_LIBS='/usr/local/lib/ejabberd'
ERL_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump
ERL_INETRC="$CONFIG_DIR"/inetrc

# define ejabberd parameters
EJABBERD_OPTS="\
$(sed '/^log_rotate_size/!d;s/:[ \t]*\([0-9]\{1,\}\).*/ \1/;s/:[ \t]*\(infinity\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\
$(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\
$(sed '/^log_burst_limit_count/!d;s/:[ \t]*\([0-9]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\
$(sed '/^log_burst_limit_window_time/!d;s/:[ \t]*\([0-9]*[a-z]*\).*/ \1 /;s/^/ /' "$EJABBERD_CONFIG_PATH")\
$EJABBERD_OPTS"
[ -n "$EJABBERD_OPTS" ] && EJABBERD_OPTS="-ejabberd $EJABBERD_OPTS"
EJABBERD_OPTS="-mnesia dir \"$SPOOL_DIR\" $MNESIA_OPTIONS $EJABBERD_OPTS -s ejabberd"

# export global variables
export EJABBERD_CONFIG_PATH
export EJABBERD_LOG_PATH
export EJABBERD_PID_PATH
export ERL_CRASH_DUMP
export ERL_EPMD_ADDRESS
export ERL_DIST_PORT
export ERL_INETRC
export ERL_MAX_PORTS
export ERL_MAX_ETS_TABLES
export CONTRIB_MODULES_PATH
export CONTRIB_MODULES_CONF_DIR
export ERL_LIBS
export SCRIPT_DIR

# Only required for Erlang/OTP 25:
export ERL_FLAGS="$ERL_FLAGS -enable-feature maybe_expr"

set_dist_client()
{
    [ -n "$ERL_DIST_PORT" ] && ERLANG_OPTS="$ERLANG_OPTS -dist_listen false"
}

# run command either directly or via su $INSTALLUSER
exec_cmd()
{
    case $EXEC_CMD,$(uname -s) in
        as_install_user,OpenBSD)
            su -s /bin/sh "$INSTALLUSER" -c 'exec "$0" "$@"' "$@"
            ;;
        as_install_user,NetBSD)
            su "$INSTALLUSER" -c 'exec "$0" "$@"' -- "$@"
            ;;
        as_install_user,*)
            su -s /bin/sh -c 'exec "$0" "$@"' "$INSTALLUSER" -- "$@"
            ;;
        as_current_user,*)
            "$@"
            ;;
    esac
}
exec_erl()
{
    NODE=$1; shift
    exec_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@"
}
exec_iex()
{
    NODE=$1; shift
    exec_cmd "$IEX" -${S:--}name "$NODE" --erl "$ERLANG_OPTS" "$@"
}

# usage
debugwarning()
{
    if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then
        echo "--------------------------------------------------------------------"
        echo ""
        echo "IMPORTANT: we will attempt to attach an INTERACTIVE shell"
        echo "to an already running ejabberd node."
        echo "If an ERROR is printed, it means the connection was not successful."
        echo "You can interact with the ejabberd node if you know how to use it."
        echo "Please be extremely cautious with your actions,"
        echo "and exit immediately if you are not completely sure."
        echo ""
        echo "To exit and detach this shell from ejabberd, press:"
        echo "  control+g and then q"
        echo ""
        #vt100 echo "Please do NOT use control+c in this debug shell !"
        #vt100 echo ""
        echo "--------------------------------------------------------------------"
        echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:"
        echo "  EJABBERD_BYPASS_WARNINGS=true"
        echo "Press return to continue"
        read -r _
        echo ""
    fi
}

livewarning()
{
    if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then
        echo "--------------------------------------------------------------------"
        echo ""
        echo "IMPORTANT: ejabberd is going to start in LIVE (interactive) mode."
        echo "All log messages will be shown in the command shell."
        echo "You can interact with the ejabberd node if you know how to use it."
        echo "Please be extremely cautious with your actions,"
        echo "and exit immediately if you are not completely sure."
        echo ""
        echo "To stop ejabberd gracefully:"
        echo "  ejabberd:stop()."
        echo "To quit erlang immediately, press:"
        echo "  control+g and then q"
        echo ""
        echo "--------------------------------------------------------------------"
        echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:"
        echo "  EJABBERD_BYPASS_WARNINGS=true"
        echo "Press return to continue"
        read -r _
        echo ""
    fi
}

check_etop_result()
{
    result=$?
    if [ $result -eq 1 ] ; then
        echo ""
        echo "It seems there was some problem running 'ejabberdctl etop'."
        echo "Is the error message something like this?"
        echo "  Failed to load module 'etop' because it cannot be found..."
        echo "Then probably ejabberd was compiled with development tools disabled."
        echo "To use 'etop', recompile ejabberd with: ./configure --enable-tools"
        echo ""
        exit $result
    fi
}

check_iex_result()
{
    result=$?
    if [ $result -eq 127 ] ; then
        echo ""
        echo "It seems there was some problem finding 'iex' binary from Elixir."
        echo "Probably ejabberd was compiled with Rebar3 and Elixir disabled, like:"
        echo "  ./configure"
        echo "which is equivalent to:"
        echo "  ./configure --with-rebar=rebar3 --disable-elixir"
        echo "To use 'iex', recompile ejabberd enabling Elixir or using Mix:"
        echo "  ./configure --enable-elixir"
        echo "  ./configure --with-rebar=mix"
        echo ""
        exit $result
    fi
}

help()
{
    echo ""
    echo "Commands to start an ejabberd node:"
    echo "  start            Start in server mode"
    echo "  foreground       Start in server mode (attached)"
    echo "  foreground-quiet Start in server mode (attached), show only critical messages"
    echo "  live             Start in interactive mode, with Erlang shell"
    echo "  iexlive          Start in interactive mode, with Elixir shell"
    echo ""
    echo "Commands to interact with a running ejabberd node:"
    echo "  debug            Attach an interactive Erlang shell to a running node"
    echo "  iexdebug         Attach an interactive Elixir shell to a running node"
    echo "  etop             Attach to a running node and start Erlang Top"
    echo "  ping             Send ping to the node, returns pong or pang"
    echo "  started|stopped  Wait for the node to fully start|stop"
    echo ""
    echo "Optional parameters when starting an ejabberd node:"
    echo "  --config-dir dir   Config ejabberd:    $CONFIG_DIR"
    echo "  --config file      Config ejabberd:    $EJABBERD_CONFIG_PATH"
    echo "  --ctl-config file  Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH"
    echo "  --logs dir         Directory for logs: $LOGS_DIR"
    echo "  --spool dir        Database spool dir: $SPOOL_DIR"
    echo "  --node nodename    ejabberd node name: $ERLANG_NODE"
    echo ""
}

# dynamic node name helper
uid() {
  ERTSVERSION="$("$ERL" -version 2>&1 | sed 's|.* \([0-9]*[0-9]\).*|\1|g')"
  if [ $ERTSVERSION -lt 11 ] ; then # otp 23.0 includes erts 11.0
    # Erlang/OTP lower than 23, which doesn's support dynamic node code
    N=1
    PF=$(( $$ % 97 ))
    while
      case $# in
        0) NN="${PF}-${N}-${ERLANG_NODE}"
          ;;
        1) NN="${PF}-${N}-${1}-${ERLANG_NODE}"
          ;;
        2) NN="${PF}-${N}-${1}@${2}"
          ;;
      esac
      N=$(( N + 1 + ( $$ % 5 ) ))
      "$EPMD" -names 2>/dev/null | grep -q " ${NN%@*} "
    do :; done
    echo $NN
  else
    # Erlang/OTP 23 or higher: use native dynamic node code
    # https://www.erlang.org/patches/otp-23.0#OTP-13812
    if [ "$ERLANG_NODE" != "${ERLANG_NODE%.*}" ]; then
      echo "undefined@${ERLANG_NODE#*@}"
    else
      echo "undefined"
    fi
  fi
}

# stop epmd if there is no other running node
stop_epmd()
{
    [ -n "$ERL_DIST_PORT" ] && return
    "$EPMD" -names 2>/dev/null | grep -q name || "$EPMD" -kill >/dev/null
}

# make sure node not already running and node name unregistered
# if all ok, ensure runtime directory exists and make it current directory
check_start()
{
    [ -n "$ERL_DIST_PORT" ] && {
        netstat -nl | grep ":$ERL_DIST_PORT" >/dev/null && {
            echo ""
            echo "NOTE:  The file ejabberdctl.cfg has configured ERL_DIST_PORT=$ERL_DIST_PORT"
            echo "       and the port 5210 is already in use."
            echo "       If ejabberd fails to start with listen error eaddrinsuse,"
            echo "       try configuring in ejabberdctl.cfg a different"
            echo "       INET_DIST_INTERFACE, or different ERL_DIST_PORT"
            echo ""
            netstat -nlp 2>/dev/null | grep ":5210"
            echo ""
        }
        return
    }
    "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && {
        pgrep -f "$ERLANG_NODE" >/dev/null && {
            echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running."
            exit 4
        }
        pgrep beam >/dev/null && {
            echo "ERROR: The ejabberd node '$ERLANG_NODE' is registered,"
            echo "       but no related beam process has been found."
            echo "Shutdown all other erlang nodes, and call 'epmd -kill'."
            exit 5
        }
        "$EPMD" -kill >/dev/null
    }
}

# allow sync calls
wait_status()
{
    wait_status_node "$ERLANG_NODE" $1 $2 $3
}

wait_status_node()
{
    CONNECT_NODE=$1
    shift
    # args: status try delay
    # return: 0 OK, 1 KO
    timeout="$2"
    status=4
    while [ "$status" -ne "$1" ] ; do
        sleep "$3"
        timeout=$((timeout - 1))
        if [ $timeout -eq 0 ] ; then
            status="$1"
        else
            exec_erl "$(uid ctl)" -hidden -noinput \
                     -eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \
                     -s ejabberd_ctl \
                     -extra "$CONNECT_NODE" $NO_TIMEOUT status > /dev/null
            status="$?"
        fi
    done
    [ $timeout -gt 0 ]
}

exec_other_command()
{
    exec_other_command_node $ERLANG_NODE "$@"
}

exec_other_command_node()
{
    CONNECT_NODE=$1
    shift
    if [ -z "$CTL_OVER_HTTP" ] || [ ! -S "$CTL_OVER_HTTP" ] \
        || [ ! -x "$(command -v curl)" ] || [ -z "$1" ] || [ "$1" = "help" ] \
        || [ "$1" = "mnesia_info_ctl" ]|| [ "$1" = "print_sql_schema" ] ; then
        exec_erl "$(uid ctl)" -hidden -noinput \
                 -eval 'net_kernel:connect_node('"'$CONNECT_NODE'"')' \
                 -s ejabberd_ctl \
                 -extra "$CONNECT_NODE" $NO_TIMEOUT "$@"
        result=$?
        case $result in
            3) help;;
            *) :;;
        esac
        return $result
    else
        exec_ctl_over_http_socket "$@"
    fi
}

exec_ctl_over_http_socket()
{
    COMMAND=${1}
    CARGS=""
    while [ $# -gt 0 ]; do
        [ -z "$CARGS" ] && CARGS="[" || CARGS="${CARGS}, "
        CARGS="${CARGS}\"$1\""
        shift
    done
    CARGS="${CARGS}]"
    TEMPHEADERS=temp-headers.log
    curl \
        --unix-socket ${CTL_OVER_HTTP} \
        --header "Content-Type: application/json" \
        --header "Accept: application/json" \
        --data "${CARGS}" \
        --dump-header ${TEMPHEADERS} \
        --no-progress-meter \
        "http://localhost/ctl/${COMMAND}"
    result=$(sed -n 's/.*status-code: \([0-9]*\).*/\1/p' < $TEMPHEADERS)
    rm ${TEMPHEADERS}
    case $result in
        2|3) exec_other_command help ${COMMAND};;
        *) :;;
    esac
    exit $result
}

# ensure we can change current directory to SPOOL_DIR
[ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR"
cd "$SPOOL_DIR" || {
    echo "ERROR: can not access directory $SPOOL_DIR"
    exit 6
}

printe()
{
    printf "\n"
    printf "\e[1;40;32m==> %s\e[0m\n" "$1"
}

## Function copied from tools/make-installers
user_agrees()
{
    question="$*"

    if [ -t 0 ]
    then
        printe "$question (y/n) [n]"
        read -r response
        case "$response" in
        [Yy]|[Yy][Ee][Ss])
            return 0
            ;;
        [Nn]|[Nn][Oo]|'')
            return 1
            ;;
        *)
            echo 'Please respond with "yes" or "no".'
            user_agrees "$question"
            ;;
        esac
    else # Assume 'yes' if not running interactively.
        return 0
    fi
}

mnesia_change()
{
    ERLANG_NODE_OLD="$1"
    [ "$ERLANG_NODE_OLD" = "" ] \
        && echo "Error: Please provide the old erlang node name, for example:" \
        && echo "  ejabberdctl mnesia_change ejabberd@oldmachine" \
        && exit 1

    SPOOL_DIR_BACKUP=$SPOOL_DIR/$ERLANG_NODE_OLD-backup/
    OLDFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE_OLD.backup
    NEWFILE=$SPOOL_DIR_BACKUP/$ERLANG_NODE.backup

    printe "This changes your mnesia database from node name '$ERLANG_NODE_OLD' to '$ERLANG_NODE'"

    [ -d "$SPOOL_DIR_BACKUP" ] && printe "WARNING! A backup of old node already exists in $SPOOL_DIR_BACKUP"

    if ! user_agrees "Do you want to proceed?"
    then
        echo 'Operation aborted.'
        exit 1
    fi

    printe "Starting ejabberd with old node name $ERLANG_NODE_OLD ..."
    exec_erl "$ERLANG_NODE_OLD" $EJABBERD_OPTS -detached
    wait_status_node $ERLANG_NODE_OLD 0 30 2
    result=$?
    case $result in
        1) echo "There was a problem starting ejabberd with the old erlang node name. " \
            && echo "Check for log errors in $EJABBERD_LOG_PATH" \
            && exit $result;;
        *) :;;
    esac
    exec_other_command_node $ERLANG_NODE_OLD "status"

    printe "Making backup of old database to file $OLDFILE ..."
    mkdir $SPOOL_DIR_BACKUP
    exec_other_command_node $ERLANG_NODE_OLD backup "$OLDFILE"

    printe "Changing node name in new backup file $NEWFILE ..."
    exec_other_command_node $ERLANG_NODE_OLD mnesia_change_nodename "$ERLANG_NODE_OLD" "$ERLANG_NODE" "$OLDFILE" "$NEWFILE"

    printe "Stopping old ejabberd..."
    exec_other_command_node $ERLANG_NODE_OLD "stop"
    wait_status_node $ERLANG_NODE_OLD 3 30 2 && stop_epmd

    printe "Moving old mnesia spool files to backup subdirectory $SPOOL_DIR_BACKUP ..."
    mv $SPOOL_DIR/*.DAT $SPOOL_DIR_BACKUP
    mv $SPOOL_DIR/*.DCD $SPOOL_DIR_BACKUP
    mv $SPOOL_DIR/*.LOG $SPOOL_DIR_BACKUP

    printe "Starting ejabberd with new node name $ERLANG_NODE ..."
    exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached
    wait_status 0 30 2
    exec_other_command "status"

    printe "Installing fallback of new mnesia..."
    exec_other_command install_fallback "$NEWFILE"

    printe "Stopping new ejabberd..."
    exec_other_command "stop"
    wait_status 3 30 2 && stop_epmd

    printe "Finished, now you can start ejabberd normally"
}

# main
case $1 in
    start)
        check_start
        exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached
        ;;
    foreground)
        check_start
        exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput
        ;;
    foreground-quiet)
        check_start
        exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput -ejabberd quiet true
        ;;
    live)
        livewarning
        check_start
        exec_erl "$ERLANG_NODE" $EJABBERD_OPTS
        ;;
    debug)
        debugwarning
        set_dist_client
        exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE"
        ;;
    etop)
        set_dist_client
        exec_erl "$(uid top)" -hidden -remsh "$ERLANG_NODE" -s etop \
                 -output text
        check_etop_result
        ;;
    iexdebug)
        debugwarning
        set_dist_client
        exec_iex "$(uid debug)" --remsh "$ERLANG_NODE"
        check_iex_result
        ;;
    iexlive)
        livewarning
        exec_iex "$ERLANG_NODE" --erl "$EJABBERD_OPTS"
        check_iex_result
        ;;
    ping)
        PEER=${2:-$ERLANG_NODE}
        [ "$PEER" = "${PEER%.*}" ] && PS="-s"
        set_dist_client
        exec_cmd "$ERL" ${PS:--}name "$(uid ping "$(hostname $PS)")" $ERLANG_OPTS \
                 -noinput -hidden \
                 -eval 'net_kernel:connect_node('"'$PEER'"')' \
                 -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \
                 -eval 'halt(case net_adm:ping('"'$PEER'"') of pong -> 0; pang -> 1 end).' \
                 -output text
        ;;
    started)
        set_dist_client
        wait_status 0 30 2 # wait 30x2s before timeout
        ;;
    stopped)
        set_dist_client
        wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout
        ;;
    mnesia_change)
        mnesia_change $2
        ;;
    *)
        set_dist_client
        exec_other_command "$@"
        ;;
esac
