from __future__ import annotations

import logging
import socket
import struct
import time
from dataclasses import dataclass
from typing import Optional

logger = logging.getLogger("mcping.bedrock")


MAGIC = bytes([
    0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE,
    0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78,
])

PROTOCOL_VERSION_MAP: dict[int, str] = {
    944: "1.21.4",
    940: "1.21.0",
    927: "1.20.80",
    922: "1.20.60",
    919: "1.20.50",
    909: "1.20.30",
    865: "1.20.0",
    861: "1.19.80",
    851: "1.19.70",
    840: "1.19.60",
    822: "1.19.30",
    818: "1.19.20",
    814: "1.19.0",
    785: "1.18.30",
    779: "1.18.0",
    761: "1.17.40",
    755: "1.17.30",
    736: "1.17.0",
    721: "1.16.230",
    719: "1.16.220",
    714: "1.16.210",
    707: "1.16.100",
    702: "1.16.0",
    628: "1.14.0",
    575: "1.13.0",
    546: "1.12.0",
    518: "1.11.0",
    503: "1.10.0",
    489: "1.9.0",
    471: "1.8.0",
}

GAMEMODE_MAP: dict[str, str] = {
    "0": "Survival",
    "1": "Creative",
    "2": "Adventure",
    "3": "Spectator",
    "c": "Survival",
    "s": "Creative",
    "a": "Adventure",
    "v": "Spectator",
}


@dataclass
class BedrockPingResult:
    success: bool
    error: Optional[str] = None
    host: str = ""
    port: int = 19132
    ip: Optional[str] = None
    edition: str = "MCPE"
    version_name: str = ""
    protocol: int = 0
    motd: str = ""
    motd_raw: str = ""
    players_online: int = 0
    players_max: int = 0
    sub_motd: str = ""
    gamemode: str = ""
    latency_ms: float = 0.0
    srv_resolved: bool = False
    resolved_host: str = ""
    resolved_port: int = 19132

    def to_dict(self) -> dict:
        return {
            "success": self.success,
            "error": self.error,
            "host": self.host,
            "port": self.port,
            "ip": self.ip,
            "edition": self.edition,
            "version_name": self.version_name,
            "protocol": self.protocol,
            "motd": self.motd,
            "motd_raw": self.motd_raw,
            "players_online": self.players_online,
            "players_max": self.players_max,
            "sub_motd": self.sub_motd,
            "gamemode": self.gamemode,
            "latency_ms": self.latency_ms,
            "srv_resolved": self.srv_resolved,
            "resolved_host": self.resolved_host,
            "resolved_port": self.resolved_port,
        }


async def _resolve_srv_bedrock(host: str) -> tuple[str, int, bool]:
    if not host:
        return host, 19132, False

    try:
        import dns.resolver
    except ImportError:
        return host, 19132, False

    srv_record = f"_minecraft._tcp.{host}"
    try:
        answers = await asyncio.get_event_loop().run_in_executor(
            None,
            lambda: dns.resolver.resolve(srv_record, "SRV"),
        )
        if answers:
            srv = answers[0]
            target = str(srv.target).rstrip(".")
            port = int(srv.port)
            if target and target != host:
                return target, port, True
    except Exception:
        pass

    return host, 19132, False


async def mcping_bedrock(
    host: str,
    port: int = 19132,
    timeout: float = 10.0,
    resolve_srv: bool = True,
) -> BedrockPingResult:
    import asyncio
    import os

    result = BedrockPingResult(success=False, host=host, port=port)

    if resolve_srv:
        resolved_host, resolved_port, was_srv = await _resolve_srv_bedrock(host)
        result.resolved_host = resolved_host
        result.resolved_port = resolved_port
        result.srv_resolved = was_srv
        connect_host = resolved_host
        connect_port = resolved_port
    else:
        connect_host = host
        connect_port = port
        result.resolved_host = host
        result.resolved_port = port

    timestamp = int(time.time() * 1000)
    client_guid = int.from_bytes(os.urandom(8), "big")

    packet = (
        struct.pack(">B", 0x01)
        + struct.pack(">q", timestamp)
        + MAGIC
        + struct.pack(">Q", client_guid)
    )

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(timeout)

    try:
        ping_start = time.monotonic()
        addr_info = socket.getaddrinfo(connect_host, connect_port, socket.AF_INET, socket.SOCK_DGRAM)
        if addr_info:
            result.ip = addr_info[0][4][0]

        sock.sendto(packet, (connect_host, connect_port))
        data, _ = sock.recvfrom(65535)
        ping_end = time.monotonic()
        result.latency_ms = round((ping_end - ping_start) * 1000, 2)

        if len(data) < 35:
            result.error = "Pong data too short"
            return result

        offset = 0
        packet_type = data[offset]
        offset += 1
        offset += 8
        offset += 8
        offset += 16

        str_len = struct.unpack(">H", data[offset:offset + 2])[0]
        offset += 2
        server_string = data[offset:offset + str_len].decode("utf-8", errors="replace")

        parts = server_string.split(";")

        if len(parts) >= 6:
            result.edition = parts[0] if parts else "MCPE"
            result.motd_raw = parts[1] if len(parts) > 1 else ""
            result.motd = result.motd_raw
            result.protocol = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 0
            result.version_name = PROTOCOL_VERSION_MAP.get(result.protocol, parts[3] if len(parts) > 3 else "Unknown")
            result.players_online = int(parts[4]) if len(parts) > 4 and parts[4].isdigit() else 0
            result.players_max = int(parts[5]) if len(parts) > 5 and parts[5].isdigit() else 0

        if len(parts) >= 7:
            result.sub_motd = parts[6]
        if len(parts) >= 8:
            raw_gm = parts[7].lower()
            result.gamemode = GAMEMODE_MAP.get(raw_gm, parts[7])

        result.success = True

    except socket.timeout:
        result.error = f"Connection timeout ({timeout}s)"
    except OSError as e:
        result.error = f"Network error: {e}"
    except Exception as e:
        result.error = f"Ping failed: {e}"
        logger.error("Bedrock Ping error: %s", e, exc_info=True)
    finally:
        sock.close()

    return result
