"""Async NTRIP client for RTK correction streaming.

Auto-detects NTRIP protocol version per mount point:
  - Tries NTRIP 2.0 (HTTP/1.1 + Ntrip-Version header) first
  - Falls back to NTRIP 1.0 (HTTP/1.0, ICY response) on rejection

Protocol details from QGNSS.exe decompilation:
  - User-Agent: "NTRIP lc29h" (QGNSS used "NTRIP QGNSS")
  - Source table: GET / HTTP/1.1 with Host header
  - Mount point: GET /mount with Basic auth + User-Agent
  - GGA position sent to caster for VRS (Virtual Reference Station)
"""

import asyncio
import base64
import logging
from collections.abc import AsyncIterator

from .models import NtripConfig, NtripMountPoint, Position

logger = logging.getLogger(__name__)

USER_AGENT = "NTRIP lc29h"
SOURCE_TABLE_END = b"ENDSOURCETABLE"


class NtripClient:
    """Async NTRIP client for RTK correction data."""

    def __init__(self, config: NtripConfig):
        self.config = config
        self._reader: asyncio.StreamReader | None = None
        self._writer: asyncio.StreamWriter | None = None
        self._connected = False
        self._running = False

    @property
    def connected(self) -> bool:
        return self._connected

    async def get_source_table(self) -> list[NtripMountPoint]:
        """Fetch the source table from the NTRIP caster.

        Returns a list of available mount points.
        """
        reader, writer = await asyncio.open_connection(
            self.config.host, self.config.port
        )
        try:
            request = (
                f"GET / HTTP/1.1\r\n"
                f"Host: {self.config.host}\r\n"
                f"User-Agent: {USER_AGENT}\r\n"
                f"\r\n"
            )
            writer.write(request.encode())
            await writer.drain()

            # Read full response — source tables can be large (rtk2go.com has hundreds of entries)
            chunks = []
            try:
                while True:
                    chunk = await asyncio.wait_for(reader.read(65536), timeout=10.0)
                    if not chunk:
                        break
                    chunks.append(chunk)
                    if SOURCE_TABLE_END in chunk:
                        break
            except TimeoutError:
                pass  # Use whatever we got
            writer.close()

            response = b"".join(chunks)
            return self._parse_source_table(response.decode("latin-1", errors="replace"))
        except Exception:
            writer.close()
            raise

    def _parse_source_table(self, response: str) -> list[NtripMountPoint]:
        """Parse NTRIP source table response into mount point objects."""
        mount_points = []
        in_body = False

        for line in response.split("\r\n"):
            if not in_body:
                if line == "":
                    in_body = True
                continue

            if line.startswith("ENDSOURCETABLE"):
                break

            if line.startswith("STR;"):
                parts = line.split(";")
                if len(parts) >= 19:
                    try:
                        mp = NtripMountPoint(
                            name=parts[1],
                            identifier=parts[2],
                            format=parts[3],
                            format_details=parts[4],
                            carrier=int(parts[5]) if parts[5] else 0,
                            nav_system=parts[6],
                            network=parts[7],
                            country=parts[8],
                            latitude=float(parts[9]) if parts[9] else 0.0,
                            longitude=float(parts[10]) if parts[10] else 0.0,
                            nmea_required=parts[11] == "1",
                            solution=parts[12],
                            generator=parts[13],
                            compression=parts[14],
                            authentication=parts[15],
                            fee=parts[16],
                            bitrate=int(parts[17]) if parts[17] else 0,
                        )
                        mount_points.append(mp)
                    except (ValueError, IndexError) as e:
                        logger.debug("Skipping malformed STR entry: %s", e)

        return mount_points

    async def connect(self) -> None:
        """Connect to NTRIP mount point and start receiving RTCM3 data.

        Auto-detects protocol version: tries NTRIP 2.0 (HTTP/1.1) first,
        falls back to NTRIP 1.0 (HTTP/1.0) if the caster rejects it.
        """
        auth = base64.b64encode(
            f"{self.config.username}:{self.config.password}".encode()
        ).decode()

        # Try NTRIP 2.0 first, fall back to 1.0
        for attempt, (http_ver, ntrip_hdr) in enumerate([
            ("HTTP/1.1", "Ntrip-Version: Ntrip/2.0\r\n"),
            ("HTTP/1.0", ""),
        ]):
            self._reader, self._writer = await asyncio.open_connection(
                self.config.host, self.config.port
            )

            request = (
                f"GET /{self.config.mountpoint} {http_ver}\r\n"
                f"Host: {self.config.host}\r\n"
                f"User-Agent: {USER_AGENT}\r\n"
                f"{ntrip_hdr}"
                f"Authorization: Basic {auth}\r\n"
                f"\r\n"
            )
            self._writer.write(request.encode())
            await self._writer.drain()

            response_line = await asyncio.wait_for(
                self._reader.readline(), timeout=10.0
            )
            response_str = response_line.decode("latin-1").strip()
            logger.info("NTRIP %s response: %s", http_ver, response_str)

            if "SOURCETABLE" in response_str:
                self._writer.close()
                raise ConnectionError(
                    "NTRIP caster rejected credentials (returned source table)"
                )

            if "200" in response_str or "ICY" in response_str:
                break  # Success

            # First attempt failed — close and try fallback
            self._writer.close()
            if attempt == 0:
                logger.info("NTRIP 2.0 rejected, falling back to 1.0")
                continue

            raise ConnectionError(f"NTRIP connection failed: {response_str}")

        # Read headers — check Content-Type to detect "mount exists but no
        # base station" condition (NTRIP 2.0 returns 200 OK with sourcetable)
        while True:
            header = await self._reader.readline()
            header_str = header.decode("latin-1").strip()
            if header_str == "":
                break
            header_lower = header_str.lower()
            if header_lower.startswith("content-type:") and "sourcetable" in header_lower:
                self._writer.close()
                raise ConnectionError(
                    f"Mount point '{self.config.mountpoint}' has no active "
                    f"base station (caster returned source table)"
                )

        self._connected = True
        self._running = True
        logger.info("Connected to NTRIP %s/%s", self.config.host, self.config.mountpoint)

    async def disconnect(self) -> None:
        """Disconnect from NTRIP caster."""
        self._running = False
        self._connected = False
        if self._writer:
            self._writer.close()
            logger.info("NTRIP disconnected")

    async def stream_rtcm(self, chunk_size: int = 1024) -> AsyncIterator[bytes]:
        """Yield RTCM3 binary data chunks from the caster.

        Each yielded chunk can be forwarded directly to the LC29H
        serial port for RTK correction.
        """
        if not self._reader or not self._connected:
            raise ConnectionError("Not connected to NTRIP caster")

        while self._running:
            try:
                data = await asyncio.wait_for(
                    self._reader.read(chunk_size), timeout=30.0
                )
                if not data:
                    logger.warning("NTRIP stream ended")
                    self._connected = False
                    break
                yield data
            except TimeoutError:
                logger.debug("NTRIP read timeout (keepalive)")
                continue
            except ConnectionError:
                logger.error("NTRIP connection lost")
                self._connected = False
                break

    def format_gga_for_vrs(self, position: Position) -> str:
        """Format a GGA sentence to send to the caster for VRS.

        Some NTRIP casters require the client to send its position
        so they can compute area-specific corrections (VRS/MAC).
        """
        if position.latitude == 0.0 and position.longitude == 0.0:
            return ""

        lat = abs(position.latitude)
        lat_deg = int(lat)
        lat_min = (lat - lat_deg) * 60
        lat_dir = "N" if position.latitude >= 0 else "S"

        lon = abs(position.longitude)
        lon_deg = int(lon)
        lon_min = (lon - lon_deg) * 60
        lon_dir = "E" if position.longitude >= 0 else "W"

        # Simplified GGA for VRS — time and checksum computed fresh
        gga_body = (
            f"GPGGA,000000.00,"
            f"{lat_deg:02d}{lat_min:010.7f},{lat_dir},"
            f"{lon_deg:03d}{lon_min:010.7f},{lon_dir},"
            f"{position.fix_type.value},"
            f"{position.satellites:02d},"
            f"{position.hdop:.1f},"
            f"{position.altitude:.1f},M,"
            f"0.0,M,,"
        )
        from .protocol import compute_checksum

        cs = compute_checksum(gga_body)
        return f"${gga_body}*{cs}\r\n"

    async def send_gga(self, position: Position) -> None:
        """Send GGA position to caster for VRS support."""
        if not self._writer or not self._connected:
            return
        gga = self.format_gga_for_vrs(position)
        if gga:
            self._writer.write(gga.encode())
            await self._writer.drain()
            logger.debug("Sent GGA to caster for VRS")
