from __future__ import annotations

import argparse
import os
import asyncio
import ssl
import sys
from contextlib import suppress
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from proto_socket.communicator import type_name_of
from proto_socket.packets.message_common_pb2 import PacketBase, TestData
from proto_socket.tcp_client import connect_tcp, connect_tcp_tls
from proto_socket.ws_client import connect_ws, connect_wss

HOST = "127.0.0.1"
WS_PATH = "/"
CONNECT_WINDOW = 3.0
REQUEST_WINDOW = 10.0
SERVER_READY_DELAY = 0.05


def parser_map():
    return {type_name_of(TestData): TestData.FromString}


async def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--mode", choices=["tcp", "ws", "tls", "wss"], default="tcp")
    parser.add_argument("--port", type=int, required=True)
    parser.add_argument("--phase", choices=["send-push", "requests", "legacy-receive"], default="send-push")
    parser.add_argument("--cert", default=None)
    args = parser.parse_args()

    print(f"INFO typeName python={type_name_of(TestData)}")

    try:
        client = await dial_with_retry(args.mode, args.port, args.cert)
    except Exception as exc:
        fail("setup", str(exc))
        raise SystemExit(1) from exc
    await asyncio.sleep(SERVER_READY_DELAY)

    try:
        if args.phase == "send-push":
            ok = await run_send_push(client)
        elif args.phase == "legacy-receive":
            await run_legacy_receive(client)
            ok = True
        else:
            ok = await run_requests(client)
    finally:
        with suppress(Exception):
            await asyncio.wait_for(client.close(), 1.0)

    if not ok:
        raise SystemExit(1)


async def dial_with_retry(mode: str, port: int, cert: str | None):
    deadline = asyncio.get_running_loop().time() + CONNECT_WINDOW
    last_error: Exception | None = None
    while asyncio.get_running_loop().time() < deadline:
        try:
            return await asyncio.wait_for(dial(mode, port, cert), 0.3)
        except Exception as exc:
            last_error = exc
            await asyncio.sleep(0.1)
    raise TimeoutError(f"connect {mode}:{port} timed out: {last_error}")


async def dial(mode: str, port: int, cert: str | None):
    if mode == "tcp":
        return await connect_tcp(HOST, port, 0, 0, parser_map())
    if mode == "ws":
        return await connect_ws(HOST, port, WS_PATH, 0, 0, parser_map())
    if mode == "tls":
        if cert is None:
            raise ValueError("--cert is required for tls mode")
        ssl_ctx = ssl.create_default_context(cafile=cert)
        ssl_ctx.check_hostname = False
        return await connect_tcp_tls(HOST, port, ssl_ctx, 0, 0, parser_map())
    if mode == "wss":
        if cert is None:
            raise ValueError("--cert is required for wss mode")
        ssl_ctx = ssl.create_default_context(cafile=cert)
        ssl_ctx.check_hostname = False
        return await connect_wss(HOST, port, WS_PATH, ssl_ctx, 0, 0, parser_map())
    raise ValueError(f"unknown mode {mode!r}")


async def run_legacy_receive(client) -> None:
    data = TestData(index=101, message="legacy fire from python").SerializeToString()
    await client.communicator.queue_packet(PacketBase(typeName="TestData", nonce=1, data=data))
    await asyncio.sleep(0.15)


async def run_send_push(client) -> bool:
    loop = asyncio.get_running_loop()
    push = loop.create_future()
    client.communicator.add_listener(
        type_name_of(TestData),
        lambda message: push.set_result(message) if not push.done() else None,
    )

    try:
        await client.communicator.send(TestData(index=101, message="fire from python client"))
        passed("1", "fire-and-forget sent")

        message = await asyncio.wait_for(push, REQUEST_WINDOW)
        if message.index != 200 or message.message != "push from go server":
            fail("2", f"unexpected push index={message.index} message={message.message!r}")
            return False
        passed("2", "received push from go server")
        return True
    except Exception as exc:
        fail("2", str(exc))
        return False


async def run_requests(client) -> bool:
    try:
        single = await client.communicator.send_request(
            TestData(index=21, message="single request from python"),
            TestData,
            timeout=REQUEST_WINDOW,
        )
        if single.index != 42 or single.message != "echo: single request from python":
            fail("3", f"unexpected response index={single.index} message={single.message!r}")
            return False
        passed("3", "single request response matched")
    except Exception as exc:
        fail("3", str(exc))
        return False

    async def request_one(i: int) -> None:
        index = 30 + i
        message = f"multi request {i} from python"
        res = await client.communicator.send_request(
            TestData(index=index, message=message),
            TestData,
            timeout=REQUEST_WINDOW,
        )
        if res.index != index * 2 or res.message != "echo: " + message:
            raise AssertionError(f"request {i} got index={res.index} message={res.message!r}")

    try:
        await asyncio.gather(*(request_one(i) for i in range(concurrency_count())))
        passed("4", "concurrent request responses matched")
        return True
    except Exception as exc:
        fail("4", str(exc))
        return False


def concurrency_count() -> int:
    raw = os.environ.get("PROTO_SOCKET_CROSSTEST_CONCURRENCY", "5")
    try:
        value = int(raw)
    except ValueError:
        return 5
    return value if value > 0 else 5

def passed(scenario: str, detail: str) -> None:
    print(f"PASS scenario={scenario} detail={detail}")


def fail(scenario: str, error: str) -> None:
    print(f"FAIL scenario={scenario} error={error}")


if __name__ == "__main__":
    asyncio.run(main())
