import json
import queue
import random
import socket
import string
import threading
import time

UDP_PORT = 52740
BROADCAST_INTERVAL = 1.0
BUFFER_SIZE = 4096


def get_local_ip() -> str:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(0.5)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        return "127.0.0.1"


class Connection:
    """Manages a TCP connection for game communication.

    Messages are newline-delimited JSON over TCP.
    A background thread continuously reads incoming messages
    and places them in a thread-safe queue.
    """

    def __init__(self, sock: socket.socket):
        self.sock = sock
        self.connected = True
        self._queue: queue.Queue = queue.Queue()
        self._thread = threading.Thread(target=self._recv_loop, daemon=True)
        self._thread.start()

    def _recv_loop(self):
        buf = ""
        while self.connected:
            try:
                data = self.sock.recv(BUFFER_SIZE)
                if not data:
                    self.connected = False
                    self._queue.put({"type": "disconnected"})
                    break
                buf += data.decode("utf-8", errors="replace")
                while "\n" in buf:
                    line, buf = buf.split("\n", 1)
                    line = line.strip()
                    if line:
                        self._queue.put(json.loads(line))
            except (ConnectionResetError, ConnectionAbortedError, OSError):
                if self.connected:
                    self.connected = False
                    self._queue.put({"type": "disconnected"})
                break
            except json.JSONDecodeError:
                continue

    def send(self, msg: dict):
        if not self.connected:
            return
        try:
            payload = json.dumps(msg, ensure_ascii=False) + "\n"
            self.sock.sendall(payload.encode("utf-8"))
        except OSError:
            self.connected = False

    def receive(self, timeout: float | None = None) -> dict | None:
        try:
            return self._queue.get(timeout=timeout)
        except queue.Empty:
            return None

    def has_message(self) -> bool:
        return not self._queue.empty()

    def close(self):
        self.connected = False
        try:
            self.sock.shutdown(socket.SHUT_RDWR)
        except OSError:
            pass
        try:
            self.sock.close()
        except OSError:
            pass


class RoomHost:
    """Creates a game room: starts a TCP server and broadcasts presence via UDP."""

    def __init__(self, host_name: str = "Host"):
        self.host_name = host_name
        self.room_id = self._generate_room_id()
        self._tcp_server: socket.socket | None = None
        self._tcp_port = 0
        self._broadcasting = False
        self._broadcast_thread: threading.Thread | None = None

    def start(self) -> tuple[str, str, int]:
        """Start the room. Returns (room_id, local_ip, tcp_port)."""
        self._tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self._tcp_server.bind(("0.0.0.0", 0))
        self._tcp_port = self._tcp_server.getsockname()[1]
        self._tcp_server.listen(1)

        self._broadcasting = True
        self._broadcast_thread = threading.Thread(
            target=self._broadcast_loop, daemon=True
        )
        self._broadcast_thread.start()

        local_ip = get_local_ip()
        return self.room_id, local_ip, self._tcp_port

    def wait_for_player(self, timeout: float = 300) -> Connection | None:
        """Block until a player connects or timeout."""
        if not self._tcp_server:
            return None
        self._tcp_server.settimeout(timeout)
        try:
            client_sock, _addr = self._tcp_server.accept()
            self._broadcasting = False
            return Connection(client_sock)
        except socket.timeout:
            self._broadcasting = False
            return None

    def stop(self):
        self._broadcasting = False
        if self._tcp_server:
            try:
                self._tcp_server.close()
            except OSError:
                pass

    def _broadcast_loop(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        sock.settimeout(1.0)

        msg = json.dumps(
            {
                "type": "room_announce",
                "room_id": self.room_id,
                "host_name": self.host_name,
                "tcp_port": self._tcp_port,
            },
            ensure_ascii=False,
        ).encode("utf-8")

        while self._broadcasting:
            try:
                sock.sendto(msg, ("<broadcast>", UDP_PORT))
            except OSError:
                pass
            time.sleep(BROADCAST_INTERVAL)

        sock.close()

    @staticmethod
    def _generate_room_id() -> str:
        chars = string.ascii_uppercase + string.digits
        return "".join(random.choices(chars, k=4))


class RoomClient:
    """Joins an existing game room by discovering it via UDP or connecting directly."""

    def find_and_join(
        self, room_id: str, timeout: float = 30
    ) -> Connection | None:
        """Listen for UDP broadcasts matching room_id and connect."""
        room_id = room_id.upper().strip()
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.bind(("", UDP_PORT))
        except OSError:
            sock.bind(("", 0))
        sock.settimeout(2.0)

        deadline = time.time() + timeout

        while time.time() < deadline:
            try:
                data, addr = sock.recvfrom(1024)
                msg = json.loads(data.decode("utf-8", errors="replace"))
                if (
                    msg.get("type") == "room_announce"
                    and msg.get("room_id") == room_id
                ):
                    host_ip = addr[0]
                    tcp_port = msg["tcp_port"]
                    sock.close()
                    return self._connect_tcp(host_ip, tcp_port)
            except socket.timeout:
                continue
            except (json.JSONDecodeError, KeyError):
                continue

        sock.close()
        return None

    def connect_direct(self, ip: str, port: int) -> Connection | None:
        """Connect directly via IP:port (fallback when UDP broadcast is blocked)."""
        return self._connect_tcp(ip, port)

    @staticmethod
    def _connect_tcp(host: str, port: int) -> Connection | None:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(10)
            sock.connect((host, port))
            sock.settimeout(None)
            return Connection(sock)
        except OSError:
            return None
