"""
TCP <-> WebSocket Bridge for Minecraft Java Edition

Runs a WebSocket server. The client keeps a persistent WebSocket and
sends framed messages:
  0x01 + data = new TCP session to MC (+ first chunk)
  0x02 + data = data for current session
  0x03        = close current session

The bridge opens/closes TCP connections to the MC server accordingly.

Flow:
  MC Java Client -> [Client Tunnel] -> persistent WS -> [This Bridge] -> TCP -> MC Server
"""

import asyncio
import logging
import argparse

try:
    import websockets
except ImportError:
    raise SystemExit("Install websockets: pip install websockets")

logger = logging.getLogger("tcp-ws-bridge")

MSG_NEW   = b'\x01'
MSG_DATA  = b'\x02'
MSG_CLOSE = b'\x03'


async def handle_connection(websocket, mc_host: str, mc_port: int):
    """Handle persistent WS from client tunnel, manage TCP sessions to MC."""
    peer = websocket.remote_address
    logger.info("Client tunnel connected: %s", peer)

    reader = None
    writer = None
    tcp_read_task = None

    async def _close_tcp():
        nonlocal reader, writer, tcp_read_task
        if tcp_read_task and not tcp_read_task.done():
            tcp_read_task.cancel()
            tcp_read_task = None
        if writer:
            try:
                writer.close()
            except OSError:
                pass
        reader = None
        writer = None

    async def _tcp_reader():
        """Read from MC server TCP and send back over WS."""
        nonlocal reader
        try:
            while reader:
                data = await reader.read(65536)
                if not data:
                    break
                await websocket.send(MSG_DATA + data)
        except (ConnectionResetError, asyncio.CancelledError, OSError):
            pass
        except websockets.ConnectionClosed:
            pass

    try:
        async for message in websocket:
            if isinstance(message, str):
                message = message.encode("utf-8")
            if not message:
                continue

            msg_type = message[0:1]
            payload = message[1:]

            if msg_type == MSG_NEW:
                # Close any existing session
                await _close_tcp()

                # Open new TCP to MC server
                try:
                    reader, writer = await asyncio.open_connection(mc_host, mc_port)
                    logger.info("New TCP session to %s:%s for %s", mc_host, mc_port, peer)
                except (ConnectionRefusedError, OSError) as exc:
                    logger.error("Cannot reach MC: %s", exc)
                    await websocket.send(MSG_CLOSE)
                    continue

                # Start reading TCP responses
                tcp_read_task = asyncio.ensure_future(_tcp_reader())

                # Forward the first chunk
                if payload:
                    writer.write(payload)
                    await writer.drain()

            elif msg_type == MSG_DATA:
                if writer:
                    try:
                        writer.write(payload)
                        await writer.drain()
                    except (ConnectionResetError, OSError):
                        await _close_tcp()
                        await websocket.send(MSG_CLOSE)

            elif msg_type == MSG_CLOSE:
                logger.info("Client closed TCP session for %s", peer)
                await _close_tcp()

    except websockets.ConnectionClosed:
        logger.info("Client tunnel disconnected: %s", peer)
    except Exception as exc:
        logger.error("Bridge error for %s: %s", peer, exc)
    finally:
        await _close_tcp()
        logger.info("Session ended for %s", peer)


async def run_bridge(ws_host: str, ws_port: int, mc_host: str, mc_port: int):
    """Main bridge coroutine."""
    logger.info(
        "TCP<->WS Bridge: ws://%s:%s <-> tcp://%s:%s",
        ws_host, ws_port, mc_host, mc_port,
    )

    async with websockets.serve(
        lambda ws: handle_connection(ws, mc_host, mc_port),
        ws_host,
        ws_port,
        max_size=None,
        ping_interval=20,
        ping_timeout=30,
    ):
        logger.info("Bridge ready – waiting for connections…")
        await asyncio.Future()  # run forever


def main():
    """CLI entry point."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
    )
    
    parser = argparse.ArgumentParser(description="TCP<->WS Bridge for MC Java")
    parser.add_argument("--ws-host", default="127.0.0.1")
    parser.add_argument("--ws-port", type=int, default=8764)
    parser.add_argument("--mc-host", default="127.0.0.1")
    parser.add_argument("--mc-port", type=int, default=25565)
    args = parser.parse_args()

    try:
        asyncio.run(run_bridge(args.ws_host, args.ws_port, args.mc_host, args.mc_port))
    except KeyboardInterrupt:
        logger.info("Shutting down…")


if __name__ == "__main__":
    main()
