#!/usr/bin/env python3
"""Static contract checks for the OpenQ4 browser datagram transport."""

from pathlib import Path
import subprocess


ROOT = Path(__file__).resolve().parents[1]
OPENQ4 = ROOT / "vendor" / "openq4"
SOURCE = OPENQ4 / "src" / "sys" / "emscripten" / "wasm_net.cpp"
CALLBACK_PATCH_NAME = "0023-defer-websocket-messages-during-asyncify.patch"
CALLBACK_PATCH = ROOT / "port" / "native" / "patches" / CALLBACK_PATCH_NAME


def require(text: str, needle: str, context: str) -> None:
    if needle not in text:
        raise AssertionError(f"missing {needle!r} in {context}")


def reject(text: str, needle: str, context: str) -> None:
    if needle in text:
        raise AssertionError(f"unexpected {needle!r} in {context}")


def main() -> None:
    callback_patch = CALLBACK_PATCH.read_text(encoding="utf-8")
    source = subprocess.run(
        [
            "patch",
            "--silent",
            "--output=-",
            str(SOURCE),
            str(CALLBACK_PATCH),
        ],
        check=True,
        capture_output=True,
        text=True,
    ).stdout
    meson = (OPENQ4 / "meson.build").read_text(encoding="utf-8")
    selected = subprocess.run(
        [
            "python3",
            "tools/build/meson_sources.py",
            "--host-system",
            "emscripten",
            "--platform-backend",
            "sdl3",
            "--include-game",
            "false",
        ],
        cwd=OPENQ4,
        check=True,
        capture_output=True,
        text=True,
    ).stdout.splitlines()

    assert selected.count("src/sys/emscripten/wasm_net.cpp") == 1
    assert "src/sys/posix/posix_net.cpp" not in selected

    require(source, "WEB_MAX_DATAGRAM_BYTES = 65507", "UDP payload bound")
    require(source, "WEB_QUEUE_CAPACITY = 64", "queue count bound")
    require(source, "WEB_QUEUE_BYTE_LIMIT = 1024 * 1024", "queue memory bound")
    require(source, "WEB_BACKPRESSURE_BYTES", "browser send backpressure")
    require(source, "Web_ScheduleReconnect", "reconnect state machine")
    require(source, "Web_ReportPending", "deferred callback reporting")
    require(source, "WEB_NOTICE_INTERVAL_MSEC = 1000.0", "notice consolidation interval")
    require(source, "counter < UINT_MAX", "bounded pending counters")
    reject(source, "Web_DeferMessagesDuringAsyncify", "shared text/binary JS deferral")
    reject(source, "Asyncify.sleepCallbacks", "Asyncify runtime hooks")
    require(callback_patch, "Web_ReportPending", "tracked callback-purity patch")

    for relative_path in (
        "tools/build-native.sh",
        "tools/build-wasm.sh",
        "tools/stage-source-offer.sh",
    ):
        script = (ROOT / relative_path).read_text(encoding="utf-8")
        openq4_patches, gamelibs_patches = script.split("GAMELIBS_PATCH_FILES=", 1)
        require(openq4_patches, CALLBACK_PATCH_NAME, f"{relative_path} OpenQ4 patch list")
        reject(gamelibs_patches, CALLBACK_PATCH_NAME, f"{relative_path} GameLib patch list")

    send_call = "emscripten_websocket_send_binary( state->socket, const_cast<void *>( data ), size )"
    require(source, send_call, "one-frame-per-datagram send")
    assert source.count("emscripten_websocket_send_binary(") == 1
    reject(source, "PORT_ANNOUNCEMENT", "binary datagram transport")

    callback_sections = {
        "open": source[source.index("static bool Web_OnOpen"):source.index("static bool Web_OnMessage")],
        "message": source[source.index("static bool Web_OnMessage"):source.index("static bool Web_OnError")],
        "error": source[source.index("static bool Web_OnError"):source.index("static bool Web_OnClose")],
        "close": source[source.index("static bool Web_OnClose"):source.index("static bool Web_OpenSocket")],
    }
    for name, body in callback_sections.items():
        reject(body, "common->", f"{name} callback engine output")
        reject(body, "Web_ReportPending", f"{name} callback recursive reporting")

    require(callback_sections["open"], "pendingConnectedNotices", "open callback notice")
    message_handler = callback_sections["message"]
    require(message_handler, "if ( event->isText )", "voice signaling split")
    require(message_handler, "Web_DispatchText", "voice signaling callback")
    require(message_handler, "state->incoming.Push( state->remote, event->data", "binary frame queue")
    require(message_handler, "pendingIncomingDrops", "message callback drop notice")
    require(callback_sections["error"], "pendingErrorNotices", "error callback notice")
    require(callback_sections["close"], "pendingCloseNotices", "close callback notice")
    require(callback_sections["close"], "lastCloseReason", "bounded close reason copy")

    reporter = source[source.index("static void Web_ReportPending"):source.index("static void Web_DeleteSocket")]
    require(reporter, "common->Printf", "deferred open report")
    require(reporter, "common->DPrintf", "deferred lifecycle report")
    require(reporter, "common->Warning", "deferred drop report")
    first_output = reporter.index("common->")
    for field in (
        "pendingConnectedNotices",
        "pendingErrorNotices",
        "pendingCloseNotices",
        "pendingIncomingDrops",
    ):
        cleared = reporter.index(f"state->{field} = 0;")
        assert cleared < first_output, f"{field} must clear before engine output"

    get_packet = source[source.index("bool idPort::GetPacket("):source.index("bool idPort::GetPacketBlocking")]
    require(get_packet, "Web_ReportPending( state )", "normal engine poll reporting")
    assert get_packet.index("Web_ReportPending( state )") < get_packet.index("Web_EnsureSocket( state )")
    require(source, "Module['q4Network']", "public signaling API")
    require(source, "network['sendText']", "public signaling sender")
    require(source, "network['onText']", "public signaling receiver")

    require(source, "value = '/game/' + port", "same-origin gateway route")
    require(source, "Module['q4WebSocketUrl']", "gateway URL override")
    require(meson, "'-lwebsocket'", "Emscripten WebSocket C API link dependency")

    print("wasm_network: ok")


if __name__ == "__main__":
    main()
