#!/bin/bash
# vim: shiftwidth=4 tabstop=4 expandtab

#
# Logging
#
declare -r RED='\033[31m'
declare -r YELLOW='\033[33m'
declare -r BLUE='\033[34m'
declare -r MAGENTA='\033[35m'
declare -r CYAN='\033[36m'

declare -rA LOG_LEVELS=( [TRACE]=1 [DEBUG]=2 [INFO]=3 [WARNING]=4 [ERROR]=5 [FATAL]=5 )
declare -rA LOG_LEVELS_COLOR=(
    [TRACE]=$MAGENTA [DEBUG]=$BLUE [INFO]=$CYAN [WARNING]=$YELLOW [ERROR]=$RED [FATAL]="\e[1m$RED"
)
DEFAULT_LOG_LEVEL=WARNING

function log_enable_quiet_mode() {
    if [[ -z "$LOG" ]]; then
        # No log file, just redirect stdout to /dev/null
        exec >> /dev/null
    else
        # Redirect stdout to log file and stderr to both log file and stderr
        exec >> >(sed "s/^/$(date '+%Y-%m-%d %H:%M:%S') - STDOUT - /" >> "$LOG")
        exec 2> >(sed "s/^/$(date '+%Y-%m-%d %H:%M:%S') - STDERR - /" >> "$LOG")
    fi
    QUIET_MODE=1
}

# Check if console log is active
function console_log() {
    { [[ -z "$LOG" ]] || [[ $CONSOLE_LOG -eq 1 ]]; } && [[ $QUIET_MODE -ne 1 ]] && return 0
    return 1
}

function log(){
    local level=${1^^}
    shift
    if [[ "${LOG_LEVELS[$level]:-null}" == "null" ]]; then
        log ERROR "Invalid log level '$level' supplied, used WARNING as default."
        log WARNING "$@"
        return
    fi
    [[ ${LOG_LEVELS[$level]} -lt ${LOG_LEVELS[$LOG_LEVEL]} ]] && return

    # Compute message
    local msg
    msg="$(date '+%Y-%m-%d %H:%M:%S') - $level - $( implode " " "$@" )"

    # Log to log file
    if [[ -n "$LOG" ]]; then
        if [[ -e "$LOG" ]] && [[ ! -w "$LOG" ]]; then
            echo "FATAL - Log file '$LOG' is not writable" >&2
            exit 3
        fi
        if [[ ! -e "$LOG" ]] && [[ ! -w "$(dirname "$LOG")" ]]; then
            echo "FATAL - Log directory '$( basename "$LOG")' is not writable" >&2
            exit 3
        fi
        # Log file exists
        echo -e "$msg" >> "$LOG"
    fi

    # Log to console (if no log file or console log and never if quiet mode is enabled)
    if console_log; then
        # Use colors if terminal support it and if we are in interactive mode
        [[ -n "$TERM" ]] && [[ -t 1 ]] && \
            msg="$(date '+%Y-%m-%d %H:%M:%S') - ${LOG_LEVELS_COLOR[$level]}$level\e[0m - " \
            msg+="$( implode " " "$@" )"

        # Displays on STDERR from warning messages
        if [[ ${LOG_LEVELS[$level]} -ge ${LOG_LEVELS["WARNING"]} ]]; then
            [[ "$level" == "FATAL" ]] && echo -e "\n\n" >&2
            echo -e "$msg" >&2
        else
            echo -e "$msg"
        fi
    fi

    # Exit on fatal error
    if [[ "$level" == "FATAL" ]]; then
        [[ -n "$TMP" ]] && [[ -e "$TMP" ]] && rm -f "$TMP"
        log_stop
        exit 3
    fi
}

function debug() { log DEBUG "$@"; }
function info() { log INFO "$@"; }
function warn() { log WARNING "$@"; }
function error() { log ERROR "$@"; }
function fatal_error() { log FATAL "$@"; }

function log_start() {
    log INFO "-------------------- Start $(_log_command_name) ----------------------"
    START_TIME=$(date +%s)
}

function log_stop() {
    [[ -z "$START_TIME" ]] && return
    local duration
    (( duration=$(date +%s)-START_TIME ))
    log INFO "Total duration: $(format_duration $duration)"
    log INFO "-------------------- End of $(_log_command_name) command -----------------------"
}

function log_sigint() {
    log WARNING "SIGINT detected"
    log_stop
    exit 1
}
trap log_sigint SIGINT

function init_logging() {
    [[ "${CONSOLE_LOG:-null}" == "null" ]] && CONSOLE_LOG=0
    [[ "${QUIET_MODE:-null}" == "null" ]] && QUIET_MODE=0
    if [[ "${LOG_LEVEL:-null}" == "null" ]] || [[ "${LOG_LEVELS[$LOG_LEVEL]:-null}" == "null" ]]; then
        local bad_log_level=$LOG_LEVEL
        LOG_LEVEL=$DEFAULT_LOG_LEVEL
        [[ -n "$bad_log_level" ]] && \
            warn "Bad log level '$bad_log_level' configured, use default: $DEFAULT_LOG_LEVEL"
    fi
    mkdir -p "$LOG_DIRECTORY_PATH" || log FATAL "Failed to create log directory ($LOG_DIRECTORY_PATH)."
    LOG="$LOG_DIRECTORY_PATH/ee-postfix-tools.log"
}

#
# Loading configuration
#
[ "$LIB_DIR" != "/usr/lib/ee-postfix-tools" ] && CONFIG="$LIB_DIR/../../..$CONFIG"
CONFIG_DIR="/etc/ee-postfix-tools"
if [[ -d "$CONFIG_DIR" ]] && [[ -n "$(ls "$CONFIG_DIR"/*.conf 2>/dev/null)" ]]; then
    for file in "$CONFIG_DIR"/*.conf; do
        # shellcheck source=/dev/null
        source "$file"
    done
fi

# Default configuration values
[ "${BACKUP_DIRECTORY_PATH:-null}" == "null" ] && \
    BACKUP_DIRECTORY_PATH="/var/backups/ee-postfix-tools"
[ "${LOG_DIRECTORY_PATH:-null}" == "null" ] && LOG_DIRECTORY_PATH=/var/log/ee-postfix-tools
[ "${TMP_DIRECTORY_PATH:-null}" == "null" ] && TMP_DIRECTORY_PATH=/tmp
[ "${POSTFIX_GENERAL_LOG_PATH:-null}" == "null" ] && POSTFIX_GENERAL_LOG_PATH=/var/log/mail.log
[ "${POSTFIX_SPOOL_PATH:-null}" == "null" ] && POSTFIX_SPOOL_PATH=/var/spool/postfix
[[ "${POSTFIX_INSTANCE:-null}" == "null" ]] && POSTFIX_INSTANCE="-"

# Init logging
init_logging

#
# Bash helpers
#

function check_regex() {
  [[ $(grep -Ec "$2" <<< "$1") -eq 1 ]] && return 0
  return 1
}

function check_int() {
    check_regex "$1" '^-?[0-9]+$' || return 1
    [[ -n "$2" ]] && [[ $1 -lt $2 ]] && return 1
    [[ -n "$3" ]] && [[ $1 -gt $3 ]] && return 1
    return 0
}

function function_exists() {
    LC_ALL=C type $1 2> /dev/null | grep -q 'function'
    return $?
}

function in_array() {
    local needle=$1 el
    shift
    for el in "$@"; do
        [[ "$el" = "$needle" ]] && return 0
    done
    return 1
}

function implode() {
    local d=${1-} f=${2-}
    if shift 2; then
        printf %s "$f" "${@/#/$d}"
    fi
}

#
# Handle command arguments
#
function handle_args() {
    local idx=1 opt show_usage=0
    local -a extra_args
    while [[ $idx -le $# ]]; do
        opt=${!idx}
        if [[ "$COMMAND" == "complete" ]]; then
            COMMAND_ARGS+=("$opt")
        else
            case $opt in
                -v|--verbose)
                    LOG_LEVEL=INFO
                    CONSOLE_LOG=1
                ;;
                -d|--debug)
                    LOG_LEVEL=DEBUG
                    CONSOLE_LOG=1
                ;;
                -x|--trace)
                    set -x
                    LOG_LEVEL=TRACE
                    CONSOLE_LOG=1
                ;;
                -L|--log-level)
                    ((idx++))
                    LOG_LEVEL="${!idx}"
                    in_array "$LOG_LEVEL" "${!LOG_LEVELS[@]}" || \
                        usage "Invalid log level '$LOG_LEVEL'"
                    log DEBUG "Log level set to '$LOG_LEVEL'"
                ;;
                -C|--console)
                    CONSOLE_LOG=1
                    log DEBUG "Console logging enabled"
                ;;
                -q|--quiet)
                    log_enable_quiet_mode
                    log DEBUG "Quiet mode enabled"
                ;;
                -j|--just-try)
                    JUST_TRY=1
                    log DEBUG "Just-try mode enabled"
                ;;
                -i|--postfix-instance)
                    ((idx++))
                    POSTFIX_INSTANCE="${!idx}"
                ;;
                -h|--help|help)
                    show_usage=1
                ;;
                *)
                    if function_exists handle_extra_args; then
                        extra_args+=( "$opt" )
                    else
                        usage "Invalid argument '$opt'"
                    fi
            esac
        fi
        ((idx++))
    done
    if function_exists handle_extra_args; then
        log DEBUG "Extra args: '$( implode "' '" "${extra_args[@]}" )'"
        handle_extra_args "${extra_args[@]}"
    fi

    [[ $show_usage -eq 1 ]] && usage
}

EXTRA_SHORT_USAGE=""
function usage() {
    local error="$1"
    [[ -n "$error" ]] && echo -e "$error\n" >&2
    cat << EOF
Usage : $(basename "$0") [-dvxQCjh] [-i instance] $EXTRA_SHORT_USAGE
    -v|--verbose                 Verbose mode (log level=INFO & console logging)
    -d|--debug                   Debug mode (log level=DEBUG & console logging)
    -x|--trace                   Enable bash tracing (=set -x & log level=TRACE & console logging)
    -L|--log-level               Specify log level (default: $DEFAULT_LOG_LEVEL)
                                 Possible values: ${!LOG_LEVELS[@]}
    -C|--console                 Enable logging to console
    -q|--quiet                   Enable quiet mode: redirect all output to log file if provided or
                                 redirect all non-warning messages to /dev/null otherwise.
    -j|--just-try                Enable just-try mode: do not really run action that could change
                                 anything
    -i|--postfix-instance        Select postfix instance (default: ${POSTFIX_INSTANCE:-system default})
    -h|--help|help               Show this message

EOF

    function_exists extra_usage && echo && extra_usage

    [[ -n "$error" ]] && exit 1
    exit 0
}

#
# Helpers to run external commands
#

function run_external() {
    local cmd=( "${@:1}" )
    [[ ${JUST_TRY:-0} -eq 1 ]] && \
        log DEBUG "Just-try mode: do not really run '$( implode "', '" "${cmd[@]}" )'" && \
        return 0
    JUST_TRY="$JUST_TRY" POSTFIX_INSTANCE="$POSTFIX_INSTANCE" \
        LOG_LEVEL="$LOG_LEVEL" CONSOLE="$CONSOLE" QUIET_MODE="$QUIET_MODE" \
        "${cmd[@]}"
    return $?
}

function run_postfix_command() {
    local cmd=( "${@:1}" )
    run_external /usr/sbin/postmulti -i "$POSTFIX_INSTANCE" -x "${cmd[@]}"
    return $?
}

BIN_DIR="$( realpath "$( dirname "$( realpath "$0" )" )/../sbin" )"
function run_other_tool() {
    local tool=$1
    local args=( -i "$POSTFIX_INSTANCE" )
    [[ ${JUST_TRY:-0} -eq 1 ]] && args+=( -j )
    args+=( "${@:2}" )
    run_external "$BIN_DIR/$tool" "${args[@]}"
    return $?
}
