"""Serial port reader for NMEA 0183 data streams."""

import asyncio
import logging
from collections.abc import AsyncIterator
from typing import Any

import serial_asyncio

from fishinfo.exceptions import ConnectionError, ValidationError
from fishinfo.types import TelemetryPoint

from .ais import AISDecoder
from .converter import NMEAConverter
from .parser import NMEAParser

logger = logging.getLogger(__name__)


class NMEASerialReader:
    """
    Asynchronous serial port reader for NMEA 0183 data.

    Reads NMEA sentences from a serial port, parses them, and converts
    them to TelemetryPoint objects for ingestion.

    Supports both standard NMEA sentences and AIS messages.
    """

    def __init__(
        self,
        port: str,
        baudrate: int = 4800,
        converter: NMEAConverter | None = None,
        validate_checksums: bool = True,
        skip_invalid: bool = True,
    ) -> None:
        """
        Initialize NMEA serial reader.

        Args:
            port: Serial port path (e.g., "/dev/ttyUSB0", "COM3")
            baudrate: Baud rate (default 4800 for NMEA 0183)
            converter: Optional NMEA converter (required for stream())
            validate_checksums: Whether to validate sentence checksums
            skip_invalid: Skip invalid sentences instead of raising errors

        Examples:
            >>> converter = NMEAConverter(vessel_id=UUID("..."), tenant_id=UUID("..."))
            >>> async with NMEASerialReader("/dev/ttyUSB0", converter=converter) as reader:
            ...     async for point in reader.stream():
            ...         print(point)
        """
        self.port = port
        self.baudrate = baudrate
        self.converter = converter
        self.validate_checksums = validate_checksums
        self.skip_invalid = skip_invalid

        self.parser = NMEAParser(validate_checksums=validate_checksums)
        self.ais_decoder = AISDecoder(validate_checksums=validate_checksums)

        self._reader: asyncio.StreamReader | None = None
        self._writer: asyncio.StreamWriter | None = None
        self._running = False

    async def __aenter__(self) -> "NMEASerialReader":
        """Open serial connection."""
        await self.connect()
        return self

    async def __aexit__(self, *args: Any) -> None:
        """Close serial connection."""
        await self.disconnect()

    async def connect(self) -> None:
        """
        Open serial port connection.

        Raises:
            ConnectionError: If unable to open serial port
        """
        try:
            self._reader, self._writer = await serial_asyncio.open_serial_connection(
                url=self.port,
                baudrate=self.baudrate,
                bytesize=serial_asyncio.serial.EIGHTBITS,
                parity=serial_asyncio.serial.PARITY_NONE,
                stopbits=serial_asyncio.serial.STOPBITS_ONE,
            )
            self._running = True
            logger.info(f"Connected to serial port {self.port} at {self.baudrate} baud")
        except Exception as e:
            raise ConnectionError(f"Failed to open serial port {self.port}: {e}") from e

    async def disconnect(self) -> None:
        """Close serial port connection."""
        self._running = False
        if self._writer:
            self._writer.close()
            await self._writer.wait_closed()
            logger.info(f"Disconnected from serial port {self.port}")

    async def read_sentences(self) -> AsyncIterator[str]:
        """
        Read raw NMEA sentences from serial port.

        Yields complete sentences (one per line) with whitespace stripped.

        Yields:
            NMEA sentence strings

        Raises:
            ConnectionError: If not connected or connection lost
        """
        if not self._reader or not self._running:
            raise ConnectionError("Not connected to serial port")

        while self._running:
            try:
                # Read line (NMEA sentences are line-terminated)
                line_bytes = await self._reader.readline()
                if not line_bytes:
                    # EOF - connection lost
                    raise ConnectionError("Serial port connection lost")

                # Decode and strip whitespace
                try:
                    line = line_bytes.decode("ascii", errors="ignore").strip()
                except UnicodeDecodeError:
                    logger.warning(f"Failed to decode line: {line_bytes!r}")
                    continue

                # Skip empty lines
                if not line:
                    continue

                yield line

            except asyncio.CancelledError:
                logger.info("Read operation cancelled")
                break
            except Exception as e:
                logger.error(f"Error reading from serial port: {e}")
                if not self.skip_invalid:
                    raise

    async def stream(
        self,
        converter: NMEAConverter | None = None,
    ) -> AsyncIterator[TelemetryPoint]:
        """
        Stream parsed telemetry points from serial port.

        Reads NMEA sentences, parses them, and converts to TelemetryPoint objects.
        Automatically handles both standard NMEA sentences and AIS messages.

        Args:
            converter: Optional converter (overrides instance converter)

        Yields:
            TelemetryPoint objects

        Raises:
            ValueError: If no converter provided
            ConnectionError: If not connected or connection lost

        Examples:
            >>> converter = NMEAConverter(vessel_id=UUID("..."), tenant_id=UUID("..."))
            >>> async with NMEASerialReader("/dev/ttyUSB0") as reader:
            ...     async for point in reader.stream(converter):
            ...         await client.send(point)
        """
        conv = converter or self.converter
        if not conv:
            raise ValueError("No converter provided")

        async for sentence in self.read_sentences():
            try:
                # Try parsing as AIS first (if starts with !)
                if sentence.startswith("!"):
                    try:
                        ais_message = self.ais_decoder.decode(sentence)
                        if ais_message is None:
                            # Multi-part message incomplete
                            continue

                        # Convert to telemetry point(s)
                        result = conv.to_telemetry_point(ais_message)
                        if isinstance(result, list):
                            for point in result:
                                yield point
                        else:
                            yield result
                        continue

                    except ValidationError as e:
                        if not self.skip_invalid:
                            raise
                        logger.debug(f"Invalid AIS message: {e}")
                        continue

                # Parse as standard NMEA sentence
                try:
                    parsed = self.parser.parse(sentence)

                    # Convert to telemetry point(s)
                    result = conv.to_telemetry_point(parsed)
                    if isinstance(result, list):
                        for point in result:
                            yield point
                    else:
                        yield result

                except ValidationError as e:
                    if not self.skip_invalid:
                        raise
                    logger.debug(f"Invalid NMEA sentence: {e}")
                    continue

            except Exception as e:
                logger.error(f"Error processing sentence '{sentence}': {e}")
                if not self.skip_invalid:
                    raise


class NMEASerialBatcher:
    """
    NMEA serial reader with automatic batching.

    Combines NMEASerialReader with a batching client for efficient
    telemetry ingestion.
    """

    def __init__(
        self,
        reader: NMEASerialReader,
        batch_size: int = 100,
        flush_interval: float = 5.0,
    ) -> None:
        """
        Initialize NMEA serial batcher.

        Args:
            reader: NMEASerialReader instance
            batch_size: Number of points to batch before sending
            flush_interval: Maximum seconds to wait before flushing batch

        Examples:
            >>> from fishinfo import Client, Config
            >>> from fishinfo.nmea import NMEASerialReader, NMEAConverter, NMEASerialBatcher
            >>>
            >>> client = Client(Config(...))
            >>> converter = NMEAConverter(...)
            >>> reader = NMEASerialReader("/dev/ttyUSB0", converter=converter)
            >>>
            >>> batcher = NMEASerialBatcher(reader, batch_size=100, flush_interval=5.0)
            >>> await batcher.run(client)
        """
        self.reader = reader
        self.batch_size = batch_size
        self.flush_interval = flush_interval

    async def run(self, client: Any) -> None:
        """
        Run the serial reader with batching.

        Continuously reads from serial port and sends batched telemetry
        to the client.

        Args:
            client: Fishinfo client with send_batch() method

        Raises:
            ConnectionError: If serial connection fails
        """
        batch: list[TelemetryPoint] = []
        last_flush = asyncio.get_event_loop().time()

        async with self.reader:
            async for point in self.reader.stream():
                batch.append(point)

                # Check if we should flush
                current_time = asyncio.get_event_loop().time()
                should_flush = (
                    len(batch) >= self.batch_size
                    or (current_time - last_flush) >= self.flush_interval
                )

                if should_flush and batch:
                    try:
                        result = await client.send_batch(batch)
                        logger.info(
                            f"Sent batch: {result.points_sent} sent, "
                            f"{result.points_buffered} buffered, "
                            f"{result.points_dropped} dropped"
                        )
                        batch.clear()
                        last_flush = current_time
                    except Exception as e:
                        logger.error(f"Failed to send batch: {e}")
                        # Keep points in batch for retry
