#!/usr/bin/env bash

# AzerothCore Starter Script
# This script handles the execution of AzerothCore binaries with optional GDB support
#
# Usage: starter <binpath> <binfile> [gdb_file] [config] [syslog] [syserr] [gdb_enabled] [crashes_path]
#
# Parameters:
#   $1 - Binary path (required)
#   $2 - Binary file name (required)
#   $3 - GDB configuration file (optional)
#   $4 - Configuration file path (optional)
#   $5 - System log file (optional)
#   $6 - System error file (optional)
#   $7 - GDB enabled flag (0/1, optional)
#   $8 - Crashes directory path (optional)

BINPATH="$1"
BINFILE="$2"
GDB_FILE="$3"
CONFIG="$4"
SYSLOG="$5"
SYSERR="$6"
GDB_ENABLED="${7:-0}"
CRASHES_PATH="$8"

# Default values
CURRENT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_CRASHES_PATH=$(realpath "$BINPATH/crashes")
[ -n "$CONFIG" ] && CONFIG_ABS=$(realpath "$CONFIG")

# Set defaults if not provided
CRASHES_PATH="${CRASHES_PATH:-$DEFAULT_CRASHES_PATH}"

# Validate binary
if [ -z "$BINPATH" ] || [ -z "$BINFILE" ]; then
    echo "Error: Binary path and file are required"
    echo "Usage: $0 <binpath> <binfile> [gdb_file] [config] [syslog] [syserr] [gdb_enabled] [crashes_path]"
    exit 1
fi

BINARY="$BINPATH/$BINFILE"
if [ ! -f "$BINARY" ]; then
    echo "Error: Binary '$BINARY' not found"
    exit 1
fi

function config_value() {
    local key="$1"

    if [ -z "${CONFIG_ABS:-}" ] || [ ! -f "$CONFIG_ABS" ]; then
        return 1
    fi

    awk -F= -v key="$key" '
        {
            configKey = $1
            gsub(/^[[:space:]]+|[[:space:]]+$/, "", configKey)
        }
        configKey == key {
            value = $0
            sub(/^[^=]*=/, "", value)
            sub(/[[:space:]]*[#;].*$/, "", value)
            gsub(/"/, "", value)
            gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
            print value
            exit
        }
    ' "$CONFIG_ABS"
}

function command_contains_config() {
    local command_line="$1"

    if [ -z "${CONFIG_ABS:-}" ]; then
        return 0
    fi

    case "$command_line" in
        *" -c $CONFIG_ABS"*|*" -c \"$CONFIG_ABS\""*)
            return 0
            ;;
    esac

    return 1
}

function find_existing_instances() {
    local line pid command_line

    while IFS= read -r line; do
        pid="${line%% *}"
        command_line="${line#* }"

        [ -z "$pid" ] && continue
        [ "$pid" = "$$" ] && continue
        [ "$pid" = "$PPID" ] && continue

        case "$command_line" in
            *"/starter "*|*"pgrep "*)
                continue
                ;;
        esac

        if command_contains_config "$command_line"; then
            printf '%s\n' "$line"
        fi
    done < <(pgrep -fa "$EXECPATH" 2>/dev/null || true)
}

function listening_on_port() {
    local port="$1"

    command -v ss >/dev/null 2>&1 || return 1
    ss -H -ltn "sport = :$port" 2>/dev/null | grep -q .
}

function guarded_ports() {
    local port soap_enabled soap_port

    if [ "$BINFILE" != "worldserver" ]; then
        return 0
    fi

    port="$(config_value "WorldServerPort" || true)"
    if [ -n "$port" ]; then
        printf '%s\n' "$port"
    fi

    soap_enabled="$(config_value "SOAP.Enabled" || true)"
    soap_port="$(config_value "SOAP.Port" || true)"
    if [ "$soap_enabled" = "1" ] && [ -n "$soap_port" ]; then
        printf '%s\n' "$soap_port"
    fi
}

function find_listening_guard_ports() {
    local port

    while IFS= read -r port; do
        [ -z "$port" ] && continue
        if listening_on_port "$port"; then
            printf '%s\n' "$port"
        fi
    done < <(guarded_ports)
}

function wait_for_startup_guard() {
    local timeout="${AC_STARTUP_GUARD_TIMEOUT:-90}"
    local interval="${AC_STARTUP_GUARD_INTERVAL:-2}"
    local elapsed=0
    local instances ports
    local sleep_for
    local warned=0

    if [ "${AC_STARTUP_SKIP_GUARD:-0}" = "1" ]; then
        return 0
    fi

    case "$timeout" in
        ''|*[!0-9]*)
            echo "Startup guard: invalid AC_STARTUP_GUARD_TIMEOUT '$timeout', using 90."
            timeout=90
            ;;
    esac

    case "$interval" in
        ''|*[!0-9]*)
            echo "Startup guard: invalid AC_STARTUP_GUARD_INTERVAL '$interval', using 2."
            interval=2
            ;;
    esac

    if [ "$interval" -lt 1 ]; then
        echo "Startup guard: AC_STARTUP_GUARD_INTERVAL must be at least 1, using 1."
        interval=1
    fi

    while true; do
        instances="$(find_existing_instances)"
        ports="$(find_listening_guard_ports)"

        if [ -z "$instances" ] && [ -z "$ports" ]; then
            return 0
        fi

        if [ "$elapsed" -ge "$timeout" ]; then
            echo "Startup guard: refusing to start duplicate $BINFILE after waiting ${timeout}s."
            if [ -n "$instances" ]; then
                echo "Startup guard: existing matching process(es):"
                echo "$instances"
            fi
            if [ -n "$ports" ]; then
                echo "Startup guard: configured port(s) still listening: $(echo "$ports" | paste -sd ',' -)"
            fi
            exit 0
        fi

        if [ "$warned" -eq 0 ]; then
            echo "Startup guard: waiting for existing $BINFILE instance or configured port(s) to clear..."
            warned=1
        fi

        sleep_for="$interval"
        if [ $((elapsed + sleep_for)) -gt "$timeout" ]; then
            sleep_for=$((timeout - elapsed))
        fi

        [ "$sleep_for" -le 0 ] && continue

        sleep "$sleep_for"
        elapsed=$((elapsed + sleep_for))
    done
}

# Create crashes directory if it doesn't exist
mkdir -p "$CRASHES_PATH"

cd "$BINPATH" || {
    echo "Error: Could not change to binary path '$BINPATH'"
    exit 1
}

EXECPATH=$(realpath "$BINFILE")

wait_for_startup_guard

if [ "$GDB_ENABLED" -eq 1 ]; then
    echo "Starting $EXECPATH with GDB enabled"

    # Generate GDB configuration on the fly
    TIMESTAMP=$(date +%Y-%m-%d-%H-%M-%S)
    GDB_TEMP_FILE="$CRASHES_PATH/gdb-$TIMESTAMP.conf"
    GDB_OUTPUT_FILE="$CRASHES_PATH/gdb-$TIMESTAMP.txt"

    # Create GDB configuration file if it is not defined
    if [ -z "$GDB_FILE" ]; then

        # Create GDB configuration
        cat > "$GDB_TEMP_FILE" << EOF
set logging file $GDB_OUTPUT_FILE
set logging enabled on
set debug timestamp
EOF

    # Add run command with config if specified
    if [ -n "$CONFIG_ABS" ]; then
        echo "run -c $CONFIG_ABS" >> "$GDB_TEMP_FILE"
    else
        echo "run" >> "$GDB_TEMP_FILE"
    fi
    
    cat >> "$GDB_TEMP_FILE" << EOF
bt
bt full
info thread
thread apply all backtrace full
EOF


        GDB_FILE="$GDB_TEMP_FILE"
    fi



    # Create log files if specified
    if [ -n "$SYSLOG" ]; then
        [ ! -f "$SYSLOG" ] && touch "$SYSLOG"
    fi

    if [ -n "$SYSERR" ]; then
        [ ! -f "$SYSERR" ] && touch "$SYSERR"
    fi

    # Execute with GDB
    if [ "${WITH_CONSOLE:-0}" -eq 0 ] && [ -n "$SYSLOG" ] && [ -n "$SYSERR" ]; then
        gdb -x "$GDB_FILE" --batch "$EXECPATH" >> "$SYSLOG" 2>> "$SYSERR"
    else
        echo "> Console enabled"
        if [ -n "$SYSLOG" ] && [ -n "$SYSERR" ]; then
            gdb -x "$GDB_FILE" --batch "$EXECPATH" > >(tee "$SYSLOG") 2> >(tee "$SYSERR" >&2)
        else
            gdb -x "$GDB_FILE" --batch "$EXECPATH"
        fi
    fi


    # clean up temporary GDB file if it exists
    if [ -n "$GDB_TEMP_FILE" ]; then
        # Clean up temporary GDB file
        rm -f "$GDB_TEMP_FILE"
    fi
else
    echo "Starting $BINFILE without GDB"
    # Determine if PM2 is active
    is_pm2_active="0"
    [ "$AC_LAUNCHED_BY_PM2" == "1" ] && is_pm2_active="1"
    
    # Determine if interactive mode is enabled
    is_interactive_enabled="1"
    [ "$AC_DISABLE_INTERACTIVE" == "1" ] && is_interactive_enabled="0"
    
    # use normal execution if we are running the binary under PM2 
    # or when interactive mode is enabled
    if [[ "$is_pm2_active" == "1" || "$is_interactive_enabled" == "1" ]]; then
        echo "Running AC"
        "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
    else
        # When AC_DISABLE_INTERACTIVE is set to 1 and we are not in PM2 
        # This means we are using systemd without interactive mode and no session managers
        # in this case we need to run AC with unbuffer for line-buffered output 
        # NOTE unbuffer doesn't fully support interactive mode
        if command -v unbuffer >/dev/null 2>&1; then
            echo "Running AC with unbuffer for line-buffered output"
            unbuffer "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
        else
            echo "⚠️  unbuffer not found, the output may not be line-buffered. Try installing expect."
            exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"}
        fi
    fi
fi
