from __future__ import annotations

import asyncio
import json
import logging
import struct
import time
from dataclasses import dataclass, field
from typing import List, Optional

from .varint import pack_varint, unpack_varint, VarIntDecodeError
from .colors import strip_all, to_rich, to_legacy

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

DEFAULT_PROTOCOL = 765


@dataclass
class PingResult:
    success: bool
    error: Optional[str] = None
    host: str = ""
    port: int = 25565
    ip: Optional[str] = None
    version_name: str = ""
    protocol: int = 0
    motd: str = ""
    players_online: int = 0
    players_max: int = 0
    player_list: List[str] = field(default_factory=list)
    latency_ms: float = 0.0
    favicon: Optional[str] = None
    srv_resolved: bool = False
    resolved_host: str = ""
    resolved_port: int = 25565

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


def _build_handshake(host: str, port: int, protocol: int = DEFAULT_PROTOCOL) -> bytes:
    payload = (
        pack_varint(0x00)
        + pack_varint(protocol)
        + pack_varint(len(host.encode("utf-8")))
        + host.encode("utf-8")
        + struct.pack(">H", port)
        + pack_varint(1)
    )
    return pack_varint(len(payload)) + payload


def _build_status_request() -> bytes:
    payload = pack_varint(0x00)
    return pack_varint(len(payload)) + payload


def _build_ping() -> bytes:
    payload = pack_varint(0x01) + struct.pack(">q", int(time.time() * 1000))
    return pack_varint(len(payload)) + payload


async def _resolve_srv(host: str, default_port: int = 25565) -> tuple[str, int, bool]:
    if not host:
        return host, default_port, False

    try:
        import dns.resolver
    except ImportError:
        return host, default_port, 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 as e:
        logger.debug("SRV resolution failed (%s): %s", srv_record, e)

    return host, default_port, False


async def mcping(
    host: str,
    port: int = 25565,
    timeout: float = 10.0,
    protocol: int = DEFAULT_PROTOCOL,
    resolve_srv: bool = True,
) -> PingResult:
    result = PingResult(success=False, host=host, port=port)

    if resolve_srv:
        resolved_host, resolved_port, was_srv = await _resolve_srv(host, port)
        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

    reader = None
    writer = None

    try:
        start = time.monotonic()
        reader, writer = await asyncio.wait_for(
            asyncio.open_connection(connect_host, connect_port),
            timeout=timeout,
        )
        connect_time = time.monotonic()

        try:
            peername = writer.get_extra_info("peername")
            if peername:
                result.ip = peername[0]
        except Exception:
            pass

        writer.write(_build_handshake(connect_host, connect_port, protocol))
        writer.write(_build_status_request())
        await writer.drain()

        raw_data = b""
        deadline = time.monotonic() + timeout
        while True:
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise asyncio.TimeoutError()
            chunk = await asyncio.wait_for(reader.read(4096), timeout=remaining)
            if not chunk:
                break
            raw_data += chunk
            try:
                packet_len, offset = unpack_varint(raw_data)
                if len(raw_data) >= offset + packet_len:
                    break
            except VarIntDecodeError:
                continue

        if not raw_data:
            result.error = "No response data received"
            return result

        packet_len, offset = unpack_varint(raw_data)
        packet_id, offset = unpack_varint(raw_data, offset)

        if packet_id != 0x00:
            result.error = f"Unexpected Packet ID: {packet_id}"
            return result

        json_len, offset = unpack_varint(raw_data, offset)
        json_str = raw_data[offset:offset + json_len].decode("utf-8")
        status = json.loads(json_str)

        ping_start = time.monotonic()
        writer.write(_build_ping())
        await writer.drain()

        try:
            pong_data = await asyncio.wait_for(reader.read(256), timeout=timeout)
            ping_end = time.monotonic()
            latency = (ping_end - ping_start) * 1000
        except asyncio.TimeoutError:
            latency = (connect_time - start) * 1000

        version = status.get("version", {})
        result.version_name = version.get("name", "Unknown")
        result.protocol = version.get("protocol", 0)

        players = status.get("players", {})
        result.players_online = players.get("online", 0)
        result.players_max = players.get("max", 0)
        sample = players.get("sample", [])
        result.player_list = [p.get("name", "") for p in sample if isinstance(p, dict)]

        raw_motd = status.get("description", "")
        result.motd = to_legacy(raw_motd)
        result.favicon = status.get("favicon")
        result.latency_ms = round(latency, 2)
        result.success = True

    except asyncio.TimeoutError:
        result.error = f"Connection timeout ({timeout}s)"
    except ConnectionRefusedError:
        result.error = "Connection refused"
    except OSError as e:
        result.error = f"Network error: {e}"
    except Exception as e:
        result.error = f"Ping failed: {e}"
        logger.error("MCPing error: %s", e, exc_info=True)
    finally:
        if writer:
            try:
                writer.close()
                await writer.wait_closed()
            except Exception:
                pass

    return result
