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

ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
RUNTIME_ROOT="${ROOT}/runtime/native"
OUT_ROOT="${ROOT}/runtime/native-smoke"
PORT="${Q4_NATIVE_PORT:-27666}"
TIMEOUT_SECONDS="${Q4_SMOKE_TIMEOUT:-90}"
# The late-join gate rejects shorter recordings. Keep the default above its
# 60-second minimum so the standard release sequence never falls back to a
# replay produced by an older, declaration-incompatible build.
RECORD_SECONDS="${Q4_SMOKE_RECORD_SECONDS:-70}"
MAX_PLAYERS=12
BOT_MINPLAYERS=8
SERVER_NAME="Q4 native smoke"
LOG_FILE="${OUT_ROOT}/dedicated.log"
RAW_LOG_FILE="${OUT_ROOT}/dedicated.raw.log"
STATUS_FILE="${OUT_ROOT}/status.txt"
BOT_STATUS_FILE="${OUT_ROOT}/bot-status.txt"
DECL_STATUS_FILE="${OUT_ROOT}/entitydef-status.txt"
IMPACT_STATUS_FILE="${OUT_ROOT}/impact-napalm-status.txt"
CONTROL_FIFO="${OUT_ROOT}/console.in"
REPLAY_ROOT="${OUT_ROOT}/replays"
EXPECTED_BOT_STATUS="botStatus slots=${BOT_MINPLAYERS} entities=${BOT_MINPLAYERS} active=${BOT_MINPLAYERS}"

[[ "${RECORD_SECONDS}" =~ ^[1-9][0-9]*$ ]] || {
    echo "Q4_SMOKE_RECORD_SECONDS must be a positive integer" >&2
    exit 2
}

[[ -x "${RUNTIME_ROOT}/openQ4-ded_x64" ]] || {
    echo "Native dedicated binary is missing; run tools/build-native.sh first" >&2
    exit 1
}
[[ -f "${RUNTIME_ROOT}/q4base/pak001.pk4" ]] || {
    echo "Retail q4base data is not visible from the native runtime" >&2
    exit 1
}
for command in pgrep script ss; do
    command -v "${command}" >/dev/null 2>&1 || {
        echo "Required smoke command is missing: ${command}" >&2
        exit 1
    }
done
if ss -H -lun "sport = :${PORT}" | grep -q .; then
    echo "UDP port ${PORT} is already in use" >&2
    exit 1
fi

rm -rf -- "${OUT_ROOT}"
mkdir -p -- "${OUT_ROOT}" "${REPLAY_ROOT}" "${RUNTIME_ROOT}/home"
mkfifo -- "${CONTROL_FIFO}"
exec 3<>"${CONTROL_FIFO}"

server_pid=""
stop_server() {
    local child_pids=""

    if [[ -z "${server_pid}" ]]; then
        return
    fi
    child_pids="$(pgrep -P "${server_pid}" 2>/dev/null || true)"
    if [[ -n "${child_pids}" ]]; then
        kill ${child_pids} 2>/dev/null || true
    fi
    if kill -0 "${server_pid}" 2>/dev/null; then
        kill "${server_pid}" 2>/dev/null || true
        sleep 1
        if [[ -n "${child_pids}" ]]; then
            kill -KILL ${child_pids} 2>/dev/null || true
        fi
        kill -KILL "${server_pid}" 2>/dev/null || true
        wait "${server_pid}" 2>/dev/null || true
    fi
    server_pid=""
}

cleanup() {
    stop_server
    exec 3>&-
    rm -f -- "${CONTROL_FIFO}"
}
trap cleanup EXIT INT TERM

wait_for_log() {
    local pattern="$1"
    local timeout="$2"
    local started=${SECONDS}

    until grep -Fq -- "${pattern}" "${RAW_LOG_FILE}" 2>/dev/null; do
        if ! kill -0 "${server_pid}" 2>/dev/null; then
            echo "Dedicated server exited while waiting for: ${pattern}" >&2
            return 1
        fi
        if (( SECONDS - started >= timeout )); then
            echo "Timed out waiting for dedicated evidence: ${pattern}" >&2
            return 1
        fi
        sleep 0.25
    done
}

wait_for_log_count() {
    local pattern="$1"
    local expected="$2"
    local timeout="$3"
    local started=${SECONDS}
    local count=0

    while :; do
        count="$(grep -Fc -- "${pattern}" "${RAW_LOG_FILE}" 2>/dev/null || true)"
        (( count >= expected )) && return 0
        if ! kill -0 "${server_pid}" 2>/dev/null; then
            echo "Dedicated server exited while waiting for ${expected} occurrences of: ${pattern}" >&2
            return 1
        fi
        if (( SECONDS - started >= timeout )); then
            echo "Timed out waiting for ${expected} occurrences of: ${pattern} (saw ${count})" >&2
            return 1
        fi
        sleep 0.25
    done
}

count_latest_bot_slots_since() {
    local after_line="$1"

    tail -n "+$(( after_line + 1 ))" "${RAW_LOG_FILE}" 2>/dev/null \
        | tr -d '\r' \
        | awk -v server_name="${SERVER_NAME}" '
            index($0, "server \047" server_name "\047 IP = ") == 1 {
                slots = " "
                count = 0
                capture = 1
                next
            }
            capture && /^client +[0-9]+: / {
                line = $0
                sub(/^client +/, "", line)
                slot = line
                sub(/:.*/, "", slot)
                name = line
                sub(/^[0-9]+: /, "", name)
                sub(/,.*/, "", name)
                if (name ~ /^major_*$/ && index(slots, " " slot " ") == 0) {
                    slots = slots slot " "
                    count++
                }
                next
            }
            capture && /^[0-9]+: server / {
                capture = 0
            }
            END { print count + 0 }
        '
}

latest_bot_status_since() {
    local after_line="$1"

    tail -n "+$(( after_line + 1 ))" "${RAW_LOG_FILE}" 2>/dev/null \
        | tr -d '\r' \
        | grep -F -- 'botStatus slots=' \
        | tail -n 1 \
        || true
}

wait_for_bot_population() {
    local after_line="$1"
    local timeout="$2"
    local phase="$3"
    local started=${SECONDS}
    local count=0
    local status_line=""

    while :; do
        printf 'serverInfo\nbotStatus\n' >&3
        sleep 0.5
        count="$(count_latest_bot_slots_since "${after_line}")"
        status_line="$(latest_bot_status_since "${after_line}")"
        if (( count == BOT_MINPLAYERS )) && [[ "${status_line}" == "${EXPECTED_BOT_STATUS}" ]]; then
            bot_population_count="${count}"
            bot_status_evidence="${status_line}"
            return 0
        fi
        if (( count > BOT_MINPLAYERS )); then
            echo "Dedicated reported ${count} major bot slots during ${phase}; expected exactly ${BOT_MINPLAYERS}" >&2
            return 1
        fi
        if ! kill -0 "${server_pid}" 2>/dev/null; then
            echo "Dedicated server exited while waiting for ${BOT_MINPLAYERS} bot slots during ${phase}" >&2
            return 1
        fi
        if (( SECONDS - started >= timeout )); then
            echo "Timed out waiting for ${EXPECTED_BOT_STATUS} during ${phase} (named slots=${count}, latest status='${status_line}')" >&2
            return 1
        fi
    done
}

server_command=(
    "${RUNTIME_ROOT}/openQ4-ded_x64"
    +set fs_basepath "${RUNTIME_ROOT}"
    +set fs_savepath "${RUNTIME_ROOT}/home"
    +set s_noSound 1
    +set net_port "${PORT}"
    +set net_LANServer 1
    +set net_serverAllowDataChecksumMismatch 1
    +set net_serverRemoteConsolePassword q4-native-smoke
    +set net_serverDemo 1
    +set net_serverDemoPath "${REPLAY_ROOT}"
    +set si_name "${SERVER_NAME}"
    +set si_map mp/q4web_arena
    +set si_gameType DM
    +set si_maxPlayers "${MAX_PLAYERS}"
    +set bot_minplayers "${BOT_MINPLAYERS}"
    +set si_warmup 0
    +spawnServer
)
printf -v server_command_line '%q ' "${server_command[@]}"

set +e
TERM=xterm script -qefc "${server_command_line}" "${RAW_LOG_FILE}" \
    <&3 >/dev/null 2>&1 &
server_pid=$!
set -e

wait_for_log "msec to load mp/q4web_arena" "${TIMEOUT_SECONDS}"
wait_for_log "loading maps/mp/q4web_arena.aas32" 10
wait_for_log "Server spawned on port ${PORT}." 10
wait_for_log "recording full-state server demo to ${REPLAY_ROOT}/" 10

# The CC0 anchor must reserve the historical replay index while the installed
# retail declaration remains the live projectile body, including after reload.
decl_evidence_line="$(wc -l <"${RAW_LOG_FILE}")"
printf '%s\n' \
    'reloadFile def/weapons/00_napalmgun_openq4.def' \
    'reloadFile def/weapons/napalmgun.def' \
    'listEntityDefs all' \
    'printEntityDef impact_napalm_projectile' >&3
wait_for_log '1806 of 1806 entityDef' "${TIMEOUT_SECONDS}"
wait_for_log 'source: def/weapons/napalmgun.def:' 10
wait_for_log '"damage_napalmGlobEmit"' 10
tail -n "+$(( decl_evidence_line + 1 ))" "${RAW_LOG_FILE}" \
    | tr -d '\r' >"${DECL_STATUS_FILE}"
awk '
    /^entityDef impact_napalm_projectile:/ {
        capture = 1
        separators = 0
    }
    capture {
        print
        if ($0 == "----------" && ++separators == 2) {
            exit
        }
    }
' "${DECL_STATUS_FILE}" >"${IMPACT_STATUS_FILE}"

initial_population_line="$(wc -l <"${RAW_LOG_FILE}")"
wait_for_bot_population "${initial_population_line}" "${TIMEOUT_SECONDS}" "initial map load"
initial_bot_population_count="${bot_population_count}"
initial_bot_status="${bot_status_evidence}"

# Let all eight bots advance before requiring a fresh population response. This
# catches failures that happen only after the last bot entity begins thinking.
sleep "${RECORD_SECONDS}"
if ! kill -0 "${server_pid}" 2>/dev/null; then
    echo "Dedicated server exited while exercising ${BOT_MINPLAYERS} bots" >&2
    exit 1
fi
survival_population_line="$(wc -l <"${RAW_LOG_FILE}")"
wait_for_bot_population "${survival_population_line}" 20 "post-survival check"
surviving_bot_population_count="${bot_population_count}"
surviving_bot_status="${bot_status_evidence}"

# Reloading the bot-ready map must atomically finalize the first demo and start
# another. Stock Quake 4 MP maps do not ship AAS data, so they are not valid bot
# smoke targets for the OpenQ4 web addon.
rotation_population_line="$(wc -l <"${RAW_LOG_FILE}")"
printf 'set si_map mp/q4web_arena\nnextMap\n' >&3
wait_for_log "finished server demo" "${TIMEOUT_SECONDS}"
wait_for_log_count "msec to load mp/q4web_arena" 2 "${TIMEOUT_SECONDS}"
wait_for_log_count "recording full-state server demo to ${REPLAY_ROOT}/q4-" 2 10
wait_for_bot_population "${rotation_population_line}" "${TIMEOUT_SECONDS}" "map rotation"
rotated_bot_population_count="${bot_population_count}"
rotated_bot_status="${bot_status_evidence}"
printf 'quit\n' >&3

set +e
for (( i = 0; i < 80; i++ )); do
    kill -0 "${server_pid}" 2>/dev/null || break
    sleep 0.25
done
if kill -0 "${server_pid}" 2>/dev/null; then
    stop_server
    server_rc=143
else
    wait "${server_pid}"
    server_rc=$?
    server_pid=""
fi
set -e

tr -d '\r' <"${RAW_LOG_FILE}" >"${LOG_FILE}"
printf '%s\n%s\n%s\n' \
    "${initial_bot_status}" \
    "${surviving_bot_status}" \
    "${rotated_bot_status}" >"${BOT_STATUS_FILE}"

awk '
    /^server / { capture = 1 }
    capture { print }
    /^-----/ && capture { exit }
' "${LOG_FILE}" >"${STATUS_FILE}"

failure=0
for pattern in \
    'mp/q4web_arena' \
    'loading maps/mp/q4web_arena.aas32' \
    "Opening IPv4 UDP socket: localhost:${PORT}" \
    "Server spawned on port ${PORT}." \
    "recording full-state server demo to ${REPLAY_ROOT}/" \
    'finished server demo' \
    "server '${SERVER_NAME}'" \
    'si_maxPlayers = 12'; do
    if ! grep -Fq "${pattern}" "${LOG_FILE}"; then
        echo "Missing smoke evidence: ${pattern}" >&2
        failure=1
    fi
done

for pattern in \
    '1647: damage_openq4_napalmglobresidueburn' \
    '1648: impact_napalm_projectile' \
    '1687: weapon_hyperblaster' \
    '1769: weapon_rocketlauncher' \
    '1785: weapon_shotgun' \
    '1806 of 1806 entityDef'; do
    if ! grep -Fq "${pattern}" "${DECL_STATUS_FILE}"; then
        echo "Missing replay entityDef evidence: ${pattern}" >&2
        failure=1
    fi
done
for pattern in \
    'entityDef impact_napalm_projectile:' \
    'source: def/weapons/napalmgun.def:' \
    '"def_emit_damage"' \
    '"damage_napalmGlobEmit"' \
    '"net_syncPhysics"'; do
    if ! grep -Fq "${pattern}" "${IMPACT_STATUS_FILE}"; then
        echo "Missing live retail projectile evidence: ${pattern}" >&2
        failure=1
    fi
done
if grep -Fq 'q4web_retail_override' "${IMPACT_STATUS_FILE}"; then
    echo "Replay anchor metadata leaked into the live retail projectile declaration" >&2
    failure=1
fi

for population in \
    "${initial_bot_population_count}" \
    "${surviving_bot_population_count}" \
    "${rotated_bot_population_count}"; do
    if [[ "${population}" -ne "${BOT_MINPLAYERS}" ]]; then
        echo "Expected ${BOT_MINPLAYERS} major bot slots, found ${population}" >&2
        failure=1
    fi
done

for status_line in \
    "${initial_bot_status}" \
    "${surviving_bot_status}" \
    "${rotated_bot_status}"; do
    if [[ "${status_line}" != "${EXPECTED_BOT_STATUS}" ]]; then
        echo "Expected exact bot entity evidence '${EXPECTED_BOT_STATUS}', found '${status_line}'" >&2
        failure=1
    fi
done

mapfile -t replay_files < <(find "${REPLAY_ROOT}" -maxdepth 1 -type f -name '*.q4sd' -print | sort)
mapfile -t replay_parts < <(find "${REPLAY_ROOT}" -maxdepth 1 -type f -name '*.part' -print)
if [[ ${#replay_files[@]} -ne 2 ]]; then
    echo "Expected two finalized server demos after map rotation, found ${#replay_files[@]}" >&2
    failure=1
else
    for replay_file in "${replay_files[@]}"; do
        if [[ "$(head -c 4 -- "${replay_file}")" != 'Q4SD' ]]; then
            echo "Server demo has an invalid Q4SD header: ${replay_file}" >&2
            failure=1
            continue
        fi
        if ! grep -Fq '"complete": true' "${replay_file}.meta.json"; then
            echo "Server demo metadata is missing or incomplete: ${replay_file}.meta.json" >&2
            failure=1
            continue
        fi
        snapshot_count="$(sed -n 's/.*"snapshots": \([0-9][0-9]*\).*/\1/p' "${replay_file}.meta.json")"
        if [[ ! "${snapshot_count}" =~ ^[1-9][0-9]*$ ]]; then
            echo "Server demo contains no snapshots: ${replay_file}" >&2
            failure=1
        fi
    done
    for replay_file in "${replay_files[@]}"; do
        if [[ "$(basename -- "${replay_file}")" != *mp_q4web_arena* ]]; then
            echo "Server demo rotation did not preserve the bot-ready map name: ${replay_file}" >&2
            failure=1
        fi
    done
fi
if [[ ${#replay_parts[@]} -ne 0 ]]; then
    echo "Server demo left partial files behind" >&2
    failure=1
fi

if grep -Fq 'no valid AAS is loaded' "${LOG_FILE}"; then
    echo "Bot AAS validation failed" >&2
    failure=1
fi

if [[ ${server_rc} -ne 0 ]]; then
    echo "Dedicated server exited with unexpected code ${server_rc}" >&2
    failure=1
fi

echo "Dedicated log: ${LOG_FILE}"
echo "Server status: ${STATUS_FILE}"
echo "Bot status: ${BOT_STATUS_FILE}"
echo "EntityDef status: ${DECL_STATUS_FILE}"
printf 'Server demo: %s\n' "${replay_files[@]}"
echo "Dedicated exit code: ${server_rc}"
exit "${failure}"
