from __future__ import annotations

import asyncio
import os
import re
import signal
import ssl
import sys
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 TestData
from proto_socket.tcp_server import TcpServer
from proto_socket.ws_server import WsServer

HOST = "127.0.0.1"
PYTHON_TYPESCRIPT_TCP_PORT = 29798
PYTHON_TYPESCRIPT_WS_PORT = 29800
TLS_TCP_PORT = 29802
WSS_PORT = 29804
WS_PATH = "/"
PROCESS_TIMEOUT = 20.0
SERVER_OBSERVATION_WINDOW = 0.2
CERT_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.crt"
KEY_PATH = Path(__file__).resolve().parents[1] / "test" / "certs" / "server.key"


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


async def main() -> None:
    print(f"INFO typeName python={type_name_of(TestData)}")
    selected_transport = os.environ.get("PROTO_SOCKET_CROSSTEST_TRANSPORT")
    try:
        if selected_transport is None or selected_transport == "tcp":
            await run_tcp_send_push()
            await asyncio.sleep(0.15)
            await run_tcp_requests()
            await asyncio.sleep(0.15)
            await run_tcp_legacy_simple_receive()
            await asyncio.sleep(0.15)
        if selected_transport is None or selected_transport == "ws":
            await run_ws_send_push()
            await asyncio.sleep(0.15)
            await run_ws_requests()
            await asyncio.sleep(0.15)
        if selected_transport is None or selected_transport in {"tls", "wss"}:
            await run_tls(selected_transport)
    except Exception as exc:
        print(f"FAIL crosstest error={exc}", file=sys.stderr)
        raise SystemExit(1) from exc
    print("PASS all python-server/typescript-client crosstests passed")


async def run_tcp_send_push() -> None:
    received = asyncio.get_running_loop().create_future()
    server = TcpServer(HOST, PYTHON_TYPESCRIPT_TCP_PORT, interval_sec=0, wait_sec=0, parser_map=parser_map())

    def on_connected(client):
        def on_message(data):
            print(f"SERVER_RECEIVED index={data.index} message={data.message}")
            valid = data.index == 101 and data.message == "fire from typescript client"
            if not received.done():
                received.set_result(valid)
            if valid:
                asyncio.create_task(
                    client.communicator.send(TestData(index=200, message="push from python server"))
                )

        client.communicator.add_listener(type_name_of(TestData), on_message)

    server.on_client_connected = on_connected
    await server.start()
    try:
        await run_typescript_client("tcp", PYTHON_TYPESCRIPT_TCP_PORT, "send-push", {"1", "2"})
        ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
        if not ok:
            raise RuntimeError("TCP send-push server received unexpected data")
    finally:
        await server.stop()


async def run_tcp_requests() -> None:
    server = TcpServer(HOST, PYTHON_TYPESCRIPT_TCP_PORT, interval_sec=0, wait_sec=0, parser_map=parser_map())
    server.on_client_connected = lambda client: client.communicator.add_request_listener(
        type_name_of(TestData),
        lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
    )
    await server.start()
    try:
        await run_typescript_client("tcp", PYTHON_TYPESCRIPT_TCP_PORT, "requests", {"3", "4"})
    finally:
        await server.stop()


async def run_tcp_legacy_simple_receive() -> None:
    received: asyncio.Future[bool] = asyncio.get_running_loop().create_future()
    server = TcpServer(HOST, PYTHON_TYPESCRIPT_TCP_PORT, interval_sec=0, wait_sec=0, parser_map=parser_map())

    def on_connected(client):
        def on_message(data):
            if not received.done():
                received.set_result(data.index == 101)
        client.communicator.add_listener(type_name_of(TestData), on_message)

    server.on_client_connected = on_connected
    await server.start()
    try:
        await run_typescript_client("tcp", PYTHON_TYPESCRIPT_TCP_PORT, "legacy-receive", set())
        try:
            ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
        except asyncio.TimeoutError:
            raise RuntimeError("TCP legacy simple-name server did not receive packet")
        if not ok:
            raise RuntimeError("TCP legacy simple-name server received unexpected data")
        print("PASS scenario=tcp-legacy-simple-receive detail=server-received-legacy-alias")
    finally:
        await server.stop()


async def run_ws_send_push() -> None:
    received = asyncio.get_running_loop().create_future()
    server = WsServer(HOST, PYTHON_TYPESCRIPT_WS_PORT, WS_PATH, interval_sec=0, wait_sec=0, parser_map=parser_map())

    def on_connected(client):
        def on_message(data):
            print(f"SERVER_RECEIVED index={data.index} message={data.message}")
            valid = data.index == 101 and data.message == "fire from typescript client"
            if not received.done():
                received.set_result(valid)
            if valid:
                asyncio.create_task(
                    client.communicator.send(TestData(index=200, message="push from python server"))
                )

        client.communicator.add_listener(type_name_of(TestData), on_message)

    server.on_client_connected = on_connected
    await server.start()
    try:
        await run_typescript_client("ws", PYTHON_TYPESCRIPT_WS_PORT, "send-push", {"1", "2"})
        ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
        if not ok:
            raise RuntimeError("WS send-push server received unexpected data")
    finally:
        await server.stop()


async def run_ws_requests() -> None:
    server = WsServer(HOST, PYTHON_TYPESCRIPT_WS_PORT, WS_PATH, interval_sec=0, wait_sec=0, parser_map=parser_map())
    server.on_client_connected = lambda client: client.communicator.add_request_listener(
        type_name_of(TestData),
        lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
    )
    await server.start()
    try:
        await run_typescript_client("ws", PYTHON_TYPESCRIPT_WS_PORT, "requests", {"3", "4"})
    finally:
        await server.stop()


def _make_server_ssl_context() -> ssl.SSLContext:
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(CERT_PATH, KEY_PATH)
    return ctx


async def run_tls(selected_transport: str | None) -> None:
    ssl_context = _make_server_ssl_context()
    if selected_transport is None or selected_transport == "tls":
        await run_tls_tcp_send_push(ssl_context)
        await asyncio.sleep(0.15)
        await run_tls_tcp_requests(ssl_context)
        await asyncio.sleep(0.15)
    if selected_transport is None or selected_transport == "wss":
        await run_wss_send_push(ssl_context)
        await asyncio.sleep(0.15)
        await run_wss_requests(ssl_context)


async def run_tls_tcp_send_push(ssl_context: ssl.SSLContext) -> None:
    received = asyncio.get_running_loop().create_future()
    server = TcpServer(
        HOST,
        TLS_TCP_PORT,
        interval_sec=0,
        wait_sec=0,
        parser_map=parser_map(),
        ssl_context=ssl_context,
    )

    def on_connected(client):
        def on_message(data):
            print(f"SERVER_RECEIVED index={data.index} message={data.message}")
            valid = data.index == 101 and data.message == "fire from typescript client"
            if not received.done():
                received.set_result(valid)
            if valid:
                asyncio.create_task(
                    client.communicator.send(TestData(index=200, message="push from python server"))
                )

        client.communicator.add_listener(type_name_of(TestData), on_message)

    server.on_client_connected = on_connected
    await server.start()
    try:
        await run_typescript_client("tls", TLS_TCP_PORT, "send-push", {"1", "2"}, CERT_PATH)
        ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
        if not ok:
            raise RuntimeError("TLS TCP send-push server received unexpected data")
    finally:
        await server.stop()


async def run_tls_tcp_requests(ssl_context: ssl.SSLContext) -> None:
    server = TcpServer(
        HOST,
        TLS_TCP_PORT,
        interval_sec=0,
        wait_sec=0,
        parser_map=parser_map(),
        ssl_context=ssl_context,
    )
    server.on_client_connected = lambda client: client.communicator.add_request_listener(
        type_name_of(TestData),
        lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
    )
    await server.start()
    try:
        await run_typescript_client("tls", TLS_TCP_PORT, "requests", {"3", "4"}, CERT_PATH)
    finally:
        await server.stop()


async def run_wss_send_push(ssl_context: ssl.SSLContext) -> None:
    received = asyncio.get_running_loop().create_future()
    server = WsServer(
        HOST,
        WSS_PORT,
        WS_PATH,
        interval_sec=0,
        wait_sec=0,
        parser_map=parser_map(),
        ssl_context=ssl_context,
    )

    def on_connected(client):
        def on_message(data):
            print(f"SERVER_RECEIVED index={data.index} message={data.message}")
            valid = data.index == 101 and data.message == "fire from typescript client"
            if not received.done():
                received.set_result(valid)
            if valid:
                asyncio.create_task(
                    client.communicator.send(TestData(index=200, message="push from python server"))
                )

        client.communicator.add_listener(type_name_of(TestData), on_message)

    server.on_client_connected = on_connected
    await server.start()
    try:
        await run_typescript_client("wss", WSS_PORT, "send-push", {"1", "2"}, CERT_PATH)
        ok = await asyncio.wait_for(received, SERVER_OBSERVATION_WINDOW)
        if not ok:
            raise RuntimeError("WSS send-push server received unexpected data")
    finally:
        await server.stop()


async def run_wss_requests(ssl_context: ssl.SSLContext) -> None:
    server = WsServer(
        HOST,
        WSS_PORT,
        WS_PATH,
        interval_sec=0,
        wait_sec=0,
        parser_map=parser_map(),
        ssl_context=ssl_context,
    )
    server.on_client_connected = lambda client: client.communicator.add_request_listener(
        type_name_of(TestData),
        lambda req: TestData(index=req.index * 2, message="echo: " + req.message),
    )
    await server.start()
    try:
        await run_typescript_client("wss", WSS_PORT, "requests", {"3", "4"}, CERT_PATH)
    finally:
        await server.stop()


async def run_typescript_client(
    mode: str,
    port: int,
    phase: str,
    expected: set[str],
    cert: Path | None = None,
) -> None:
    ts_dir = typescript_package_dir()
    tsx_bin = ts_dir / "node_modules" / ".bin" / "tsx"
    args = [
        str(tsx_bin),
        "crosstest/python_typescript_client.ts",
        f"--mode={mode}",
        f"--port={port}",
        f"--phase={phase}",
    ]
    if cert is not None:
        args.append(f"--cert={cert}")
    process = await asyncio.create_subprocess_exec(
        *args,
        cwd=ts_dir,
        start_new_session=True,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    try:
        stdout, stderr = await asyncio.wait_for(process.communicate(), PROCESS_TIMEOUT)
    except TimeoutError as exc:
        try:
            os.killpg(process.pid, signal.SIGKILL)
        except ProcessLookupError:
            pass
        process.kill()
        await process.wait()
        raise TimeoutError(f"typescript client {mode}/{phase} timed out") from exc

    result_lines: list[str] = []
    for line in stdout.decode().splitlines():
        print(line)
        if line.startswith(("PASS ", "FAIL ")):
            result_lines.append(line)
    for line in stderr.decode().splitlines():
        print(line, file=sys.stderr)

    validate_result_lines(f"typescript-client {mode}/{phase}", process.returncode, result_lines, expected)


def validate_result_lines(label: str, return_code: int | None, lines: list[str], expected: set[str]) -> None:
    failed = [line for line in lines if line.startswith("FAIL ")]
    passed: set[str] = set()
    for line in lines:
        if line.startswith("PASS "):
            match = re.search(r"scenario=([^ ]+)", line)
            if match:
                passed.add(match.group(1))
    missing = expected - passed
    if return_code or failed or missing:
        raise RuntimeError(f"{label} failed returnCode={return_code} failed={failed} missing={sorted(missing)}")


def typescript_package_dir() -> Path:
    repo_root = Path(__file__).resolve().parents[2]
    candidate = repo_root / "typescript"
    if (candidate / "package.json").is_file():
        return candidate
    raise RuntimeError(f"cannot resolve typescript package directory from {candidate}")


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