#!/usr/bin/env bash
set -Eeuo pipefail

ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
RUNTIME_ROOT="${Q4_CAPACITY_RUNTIME_ROOT:-${ROOT}/runtime/native}"
OUT_ROOT="${ROOT}/runtime/native-capacity-smoke"
PORT="${Q4_CAPACITY_PORT:-27670}"
TIMEOUT_SECONDS="${Q4_CAPACITY_TIMEOUT:-240}"
EXIT_TIMEOUT_SECONDS="${Q4_CAPACITY_EXIT_TIMEOUT:-20}"
SUSTAIN_SECONDS="${Q4_CAPACITY_SUSTAIN_SECONDS:-30}"
HUMAN_ONE="Q4NativeHuman1"
HUMAN_TWO="Q4NativeHuman2"

SERVER_RAW="${OUT_ROOT}/server.raw.log"
SERVER_LOG="${OUT_ROOT}/server.log"
CLIENT1_RAW="${OUT_ROOT}/client1.raw.log"
CLIENT1_LOG="${OUT_ROOT}/client1.log"
CLIENT2_RAW="${OUT_ROOT}/client2.raw.log"
CLIENT2_LOG="${OUT_ROOT}/client2.log"

die() {
    echo "capacity smoke: $*" >&2
    exit 1
}

require_integer() {
    local name="$1"
    local value="$2"
    local minimum="$3"
    local maximum="$4"

    [[ "${value}" =~ ^[0-9]+$ ]] || die "${name} must be an integer"
    (( value >= minimum && value <= maximum )) \
        || die "${name} must be between ${minimum} and ${maximum}"
}

require_integer Q4_CAPACITY_PORT "${PORT}" 1 65535
require_integer Q4_CAPACITY_TIMEOUT "${TIMEOUT_SECONDS}" 30 1800
require_integer Q4_CAPACITY_EXIT_TIMEOUT "${EXIT_TIMEOUT_SECONDS}" 5 120
require_integer Q4_CAPACITY_SUSTAIN_SECONDS "${SUSTAIN_SECONDS}" 30 600

if (( PORT == 28004 || ( PORT >= 28040 && PORT <= 28047 ) )); then
    die "UDP port ${PORT} is reserved for the production server or replay pool"
fi

for command in awk flock grep pgrep ps script setsid sha256sum ss tr xvfb-run; do
    command -v "${command}" >/dev/null 2>&1 \
        || die "required command is missing: ${command}"
done

[[ -x "${RUNTIME_ROOT}/openQ4-ded_x64" ]] \
    || die "native dedicated binary is missing; run tools/build-native.sh first"
[[ -x "${RUNTIME_ROOT}/openQ4-client_x64" ]] \
    || die "native client binary is missing; run tools/build-native.sh first"
[[ -f "${RUNTIME_ROOT}/q4base/pak001.pk4" ]] \
    || die "retail q4base data is not visible from the native runtime"
[[ -f "${RUNTIME_ROOT}/build-info/SHA256SUMS" ]] \
    || die "native build checksums are missing"
compgen -G "${RUNTIME_ROOT}/baseoq4/q4web_mp_q4web_arena-*.pk4" >/dev/null \
    || die "the staged q4web arena addon is missing"

(
    cd -- "${RUNTIME_ROOT}"
    sha256sum -c build-info/SHA256SUMS >/dev/null
) || die "native build checksum verification failed"

mkdir -p -- "${ROOT}/runtime"
exec 9>"${ROOT}/runtime/.native-capacity-smoke.lock"
flock -n 9 || die "another native capacity smoke is already running"

if ss -H -lun "sport = :${PORT}" | grep -q .; then
    die "UDP port ${PORT} is already in use"
fi

rm -rf -- "${OUT_ROOT}"
mkdir -p -- \
    "${OUT_ROOT}/server-home" \
    "${OUT_ROOT}/client1-home" \
    "${OUT_ROOT}/client2-home"
mkfifo -- "${OUT_ROOT}/server.in" "${OUT_ROOT}/client1.in" "${OUT_ROOT}/client2.in"
exec 3<>"${OUT_ROOT}/server.in"
exec 4<>"${OUT_ROOT}/client1.in"
exec 5<>"${OUT_ROOT}/client2.in"

server_pid=""
client1_pid=""
client2_pid=""

collect_tree() {
    local pid="$1"
    local child=""

    while read -r child; do
        [[ -n "${child}" ]] && collect_tree "${child}"
    done < <(pgrep -P "${pid}" 2>/dev/null || true)
    printf '%s\n' "${pid}"
}

stop_tree() {
    local pid="$1"
    local -a pids=()

    [[ -n "${pid}" ]] || return 0
    mapfile -t pids < <(collect_tree "${pid}")
    kill -TERM -- "-${pid}" 2>/dev/null || true
    kill -TERM -- "${pids[@]}" 2>/dev/null || true
    for (( i = 0; i < 20; ++i )); do
        local alive=0
        local member=""
        for member in "${pids[@]}"; do
            kill -0 "${member}" 2>/dev/null && alive=1
        done
        (( alive == 0 )) && return 0
        sleep 0.1
    done
    kill -KILL -- "-${pid}" 2>/dev/null || true
    kill -KILL -- "${pids[@]}" 2>/dev/null || true
}

cleanup() {
    set +e
    stop_tree "${client2_pid}"
    stop_tree "${client1_pid}"
    stop_tree "${server_pid}"
    [[ -z "${client2_pid}" ]] || wait "${client2_pid}" 2>/dev/null || true
    [[ -z "${client1_pid}" ]] || wait "${client1_pid}" 2>/dev/null || true
    [[ -z "${server_pid}" ]] || wait "${server_pid}" 2>/dev/null || true
    exec 5>&-
    exec 4>&-
    exec 3>&-
    rm -f -- "${OUT_ROOT}"/*.in
}
trap 'exit 130' INT
trap 'exit 143' TERM
trap cleanup EXIT

process_is_running() {
    local pid="$1"
    local log="$2"

    kill -0 "${pid}" 2>/dev/null \
        && ! grep -Eq 'COMMAND_EXIT_CODE="[0-9]+"' "${log}" 2>/dev/null
}

wait_for_log() {
    local pid="$1"
    local log="$2"
    local pattern="$3"
    local timeout="$4"
    local started=${SECONDS}

    until grep -Fq -- "${pattern}" "${log}" 2>/dev/null; do
        process_is_running "${pid}" "${log}" \
            || die "process exited while waiting for: ${pattern}"
        (( SECONDS - started < timeout )) \
            || die "timed out waiting for: ${pattern}"
        sleep 0.25
    done
}

wait_for_exact_line() {
    local pid="$1"
    local log="$2"
    local expected="$3"
    local timeout="$4"
    local started=${SECONDS}

    until [[ -r "${log}" ]] \
        && awk -v expected="${expected}" '
            { sub(/\r$/, ""); sub(/[[:space:]]+$/, "", $0) }
            $0 == expected { found = 1 }
            END { exit !found }
        ' "${log}"; do
        process_is_running "${pid}" "${log}" \
            || die "process exited while waiting for exact marker: ${expected}"
        (( SECONDS - started < timeout )) \
            || die "timed out waiting for exact marker: ${expected}"
        sleep 0.25
    done
}

capture_server_info() {
    local tag="$1"
    local destination="$2"
    local begin="Q4_CAPACITY_${tag}_BEGIN"
    local end="Q4_CAPACITY_${tag}_END"
    local clean="${OUT_ROOT}/.server-current.log"

    printf 'echo %s\nserverInfo\nbotStatus\necho %s\n' "${begin}" "${end}" >&3
    wait_for_exact_line "${server_pid}" "${SERVER_RAW}" "${end}" "${TIMEOUT_SECONDS}"
    tr -d '\r' <"${SERVER_RAW}" >"${clean}"
    awk -v begin="${begin}" -v end="${end}" '
        { sub(/[[:space:]]+$/, "", $0) }
        $0 == begin { capture = 1; next }
        $0 == end && capture { complete = 1; exit }
        capture { print }
        END { if (!complete) exit 2 }
    ' "${clean}" >"${destination}" \
        || die "could not extract marked serverInfo block ${tag}"
}

bot_count() {
    awk '/^client +[0-9]+: major_*, ping =/ { count++ } END { print count + 0 }' "$1"
}

client_count() {
    awk '/^client +[0-9]+:/ { count++ } END { print count + 0 }' "$1"
}

population_matches() {
    local evidence="$1"
    local expected_bots="$2"
    local expected_clients="$3"
    local require_humans="$4"
    local expected_status="botStatus slots=${expected_bots} entities=${expected_bots} active=${expected_clients}"

    [[ "$(bot_count "${evidence}")" -eq "${expected_bots}" ]] || return 1
    [[ "$(client_count "${evidence}")" -eq "${expected_clients}" ]] || return 1
    grep -Fxq -- "${expected_status}" "${evidence}" || return 1
    grep -Fxq 'bot_minplayers = 8' "${evidence}" || return 1
    grep -Fxq 'si_maxPlayers = 12' "${evidence}" || return 1
    if [[ "${require_humans}" == 1 ]]; then
        grep -Eq "^client +[0-9]+: ${HUMAN_ONE}, ping =" "${evidence}" || return 1
        grep -Eq "^client +[0-9]+: ${HUMAN_TWO}, ping =" "${evidence}" || return 1
    fi
}

assert_population() {
    local evidence="$1"
    local expected_bots="$2"
    local expected_clients="$3"
    local require_humans="$4"
    local bots=""
    local clients=""
    local status_line=""

    bots="$(bot_count "${evidence}")"
    clients="$(client_count "${evidence}")"
    status_line="$(awk '/^botStatus slots=/ { status = $0 } END { print status }' "${evidence}")"
    if ! population_matches "${evidence}" "${expected_bots}" "${expected_clients}" "${require_humans}"; then
        cat -- "${evidence}" >&2
        die "population mismatch in $(basename -- "${evidence}"): bots=${bots}/${expected_bots}, clients=${clients}/${expected_clients}, status='${status_line}'"
    fi
    printf '%s: bots=%s clients=%s %s\n' "$(basename -- "${evidence}")" "${bots}" "${clients}" "${status_line}"
}

wait_for_population() {
    local label="$1"
    local destination="$2"
    local expected_bots="$3"
    local expected_clients="$4"
    local require_humans="$5"
    local started=${SECONDS}
    local attempt=0
    local probe="${OUT_ROOT}/.${label}.probe.serverinfo"

    while (( SECONDS - started < TIMEOUT_SECONDS )); do
        attempt=$(( attempt + 1 ))
        capture_server_info "${label}_${attempt}" "${probe}"
        if population_matches "${probe}" "${expected_bots}" "${expected_clients}" "${require_humans}"; then
            mv -- "${probe}" "${destination}"
            assert_population "${destination}" "${expected_bots}" "${expected_clients}" "${require_humans}"
            return 0
        fi
        sleep 1
    done
    [[ ! -f "${probe}" ]] || cat -- "${probe}" >&2
    die "timed out waiting for ${expected_bots} bots and ${expected_clients} total clients"
}

CRASH_PATTERN='Fatal Error|fatal signal|segmentation fault|core dumped|free\(\): invalid pointer|double free|terminate called|assertion .*failed|reliable messages overflow|snapshot .* contains too many|couldn.t apply snapshot'

assert_logs_clean() {
    local log=""
    local failure=0

    for log in "${SERVER_RAW}" "${CLIENT1_RAW}" "${CLIENT2_RAW}"; do
        [[ -f "${log}" ]] || continue
        if grep -Eiq "${CRASH_PATTERN}|COMMAND_EXIT_CODE=\"[1-9][0-9]*\"" "${log}"; then
            echo "capacity smoke: runtime failure signature in ${log}" >&2
            grep -Ein "${CRASH_PATTERN}|COMMAND_EXIT_CODE=\"[1-9][0-9]*\"" "${log}" | tail -n 20 >&2 || true
            failure=1
        fi
    done
    (( failure == 0 )) || exit 1
}

wait_for_clean_exit() {
    local pid="$1"
    local label="$2"
    local started=${SECONDS}
    local state=""
    local status=0

    while kill -0 "${pid}" 2>/dev/null; do
        state="$(ps -o stat= -p "${pid}" 2>/dev/null || true)"
        [[ "${state}" == Z* ]] && break
        (( SECONDS - started < EXIT_TIMEOUT_SECONDS )) \
            || die "${label} did not exit within ${EXIT_TIMEOUT_SECONDS}s"
        sleep 0.25
    done
    if wait "${pid}"; then
        return 0
    else
        status=$?
        die "${label} exited with status ${status}"
    fi
}

server_command=(
    "${RUNTIME_ROOT}/openQ4-ded_x64"
    +set fs_basepath "${RUNTIME_ROOT}"
    +set fs_savepath "${OUT_ROOT}/server-home"
    +set fs_caseSensitiveOS 0
    +set net_ip 127.0.0.1
    +set net_port "${PORT}"
    +set net_LANServer 1
    +set si_pure 0
    +set net_serverAllowDataChecksumMismatch 1
    +set net_serverDemo 0
    +set si_name Q4NativeCapacityGate
    +set si_maxPlayers 12
    +set si_warmup 0
    +set bot_debug 0
    +set bot_minplayers 8
    +set si_map mp/q4web_arena
    +set si_gameType DM
    +spawnServer
)
printf -v server_command_line '%q ' "${server_command[@]}"
TERM=xterm setsid script -qefc "${server_command_line}" "${SERVER_RAW}" <&3 >/dev/null 2>&1 &
server_pid=$!

wait_for_log "${server_pid}" "${SERVER_RAW}" "Server spawned on port ${PORT}." "${TIMEOUT_SECONDS}"
wait_for_log "${server_pid}" "${SERVER_RAW}" 'loading maps/mp/q4web_arena.aas32' "${TIMEOUT_SECONDS}"
wait_for_log "${server_pid}" "${SERVER_RAW}" 'msec to load mp/q4web_arena' "${TIMEOUT_SECONDS}"
wait_for_population BOTS_READY "${OUT_ROOT}/bots-initial.serverinfo" 8 8 0

sleep "${SUSTAIN_SECONDS}"
process_is_running "${server_pid}" "${SERVER_RAW}" \
    || die "dedicated server exited during the bot-only sustain interval"
capture_server_info BOTS_SUSTAINED "${OUT_ROOT}/bots-sustained.serverinfo"
assert_population "${OUT_ROOT}/bots-sustained.serverinfo" 8 8 0
assert_logs_clean

client1_command=(
    "${RUNTIME_ROOT}/openQ4-client_x64"
    +set fs_basepath "${RUNTIME_ROOT}"
    +set fs_savepath "${OUT_ROOT}/client1-home"
    +set fs_caseSensitiveOS 0
    +set com_skipLogoVideos 1
    +set com_skipRenderer 1
    +set r_fullscreen 0
    +set r_mode 3
    +set s_noSound 1
    +set sys_allowMultipleInstances 1
    +set net_LANServer 1
    +set net_clientDownload 0
    +set net_verbose 1
    +set si_gameType DM
)
printf -v client1_command_line '%q ' "${client1_command[@]}"
ALSOFT_DRIVERS=null TERM=xterm setsid xvfb-run -a -s '-screen 0 1280x720x24' \
    script -qefc "${client1_command_line}" "${CLIENT1_RAW}" <&4 >/dev/null 2>&1 &
client1_pid=$!
wait_for_log "${client1_pid}" "${CLIENT1_RAW}" 'Common Initialization Complete' "${TIMEOUT_SECONDS}"
printf 'set ui_name "%s"\nconnect 127.0.0.1:%s\n' "${HUMAN_ONE}" "${PORT}" >&4
wait_for_log "${client1_pid}" "${CLIENT1_RAW}" 'received first snapshot' "${TIMEOUT_SECONDS}"

client2_command=(
    "${RUNTIME_ROOT}/openQ4-client_x64"
    +set fs_basepath "${RUNTIME_ROOT}"
    +set fs_savepath "${OUT_ROOT}/client2-home"
    +set fs_caseSensitiveOS 0
    +set com_skipLogoVideos 1
    +set com_skipRenderer 1
    +set r_fullscreen 0
    +set r_mode 3
    +set s_noSound 1
    +set sys_allowMultipleInstances 1
    +set net_LANServer 1
    +set net_clientDownload 0
    +set net_verbose 1
    +set si_gameType DM
)
printf -v client2_command_line '%q ' "${client2_command[@]}"
ALSOFT_DRIVERS=null TERM=xterm setsid xvfb-run -a -s '-screen 0 1280x720x24' \
    script -qefc "${client2_command_line}" "${CLIENT2_RAW}" <&5 >/dev/null 2>&1 &
client2_pid=$!
wait_for_log "${client2_pid}" "${CLIENT2_RAW}" 'Common Initialization Complete' "${TIMEOUT_SECONDS}"
printf 'set ui_name "%s"\nconnect 127.0.0.1:%s\n' "${HUMAN_TWO}" "${PORT}" >&5
wait_for_log "${client2_pid}" "${CLIENT2_RAW}" 'received first snapshot' "${TIMEOUT_SECONDS}"

wait_for_population HUMANS_READY "${OUT_ROOT}/humans-initial.serverinfo" 8 10 1

sleep "${SUSTAIN_SECONDS}"
process_is_running "${server_pid}" "${SERVER_RAW}" \
    || die "dedicated server exited during the two-human sustain interval"
process_is_running "${client1_pid}" "${CLIENT1_RAW}" \
    || die "first native client exited during the sustain interval"
process_is_running "${client2_pid}" "${CLIENT2_RAW}" \
    || die "second native client exited during the sustain interval"
capture_server_info HUMANS_SUSTAINED "${OUT_ROOT}/humans-sustained.serverinfo"
assert_population "${OUT_ROOT}/humans-sustained.serverinfo" 8 10 1
assert_logs_clean

printf 'quit\n' >&5
printf 'quit\n' >&4
wait_for_clean_exit "${client2_pid}" 'second native client'
client2_pid=""
wait_for_clean_exit "${client1_pid}" 'first native client'
client1_pid=""
printf 'quit\n' >&3
wait_for_clean_exit "${server_pid}" 'dedicated server'
server_pid=""

tr -d '\r' <"${SERVER_RAW}" >"${SERVER_LOG}"
tr -d '\r' <"${CLIENT1_RAW}" >"${CLIENT1_LOG}"
tr -d '\r' <"${CLIENT2_RAW}" >"${CLIENT2_LOG}"
assert_logs_clean

if ss -H -lun "sport = :${PORT}" | grep -q .; then
    die "UDP port ${PORT} remained in use after shutdown"
fi

trap - EXIT
cleanup

echo "Native capacity smoke passed: 8 bots, then 8 bots plus 2 native clients."
echo "Sustain interval per stage: ${SUSTAIN_SECONDS}s"
echo "Evidence directory: ${OUT_ROOT}"
printf 'Population evidence: %s\n' \
    "${OUT_ROOT}/bots-initial.serverinfo" \
    "${OUT_ROOT}/bots-sustained.serverinfo" \
    "${OUT_ROOT}/humans-initial.serverinfo" \
    "${OUT_ROOT}/humans-sustained.serverinfo"
