"""FIFO consumer - pulls from queue, routes to parsers, stores to database.

The consumer runs in a separate thread, pulling lines from the FIFO queue,
routing them to appropriate parsers, and storing parsed data to the database.
"""

import contextlib
import logging
import threading
import time
from queue import Empty, Queue
from typing import Any, Protocol, runtime_checkable

import duckdb

from adcp_recorder.core.nmea import (
    compute_checksum,
    extract_prefix,
    is_binary_data,
    validate_checksum,
)
from adcp_recorder.db import (
    DatabaseManager,
    insert_header_data,
    insert_parse_error,
    insert_pnora_data,
    insert_pnorb_data,
    insert_pnore_data,
    insert_pnorf_data,
    insert_pnori_configuration,
    insert_pnorw_data,
    insert_pnorwd_data,
    insert_raw_line,
    insert_sensor_data,
    insert_velocity_data,
)
from adcp_recorder.export.binary_writer import BinaryBlobWriter
from adcp_recorder.export.file_writer import FileWriter
from adcp_recorder.serial.binary_chunk import BinaryChunk

logger = logging.getLogger(__name__)


@runtime_checkable
class NMEAParser(Protocol):
    """Protocol for NMEA message parsers."""

    @classmethod
    def from_nmea(cls, sentence: str) -> Any:
        """Parse NMEA sentence into a message object."""
        ...

    def to_dict(self) -> dict[str, Any]:
        """Convert message object to dictionary."""
        ...


class MessageRouter:
    """Routes NMEA sentences to appropriate parsers.

    Maps NMEA prefixes (e.g., 'PNORI', 'PNORS') to parser classes.
    """

    def __init__(self):
        """Initialize message router with empty registry."""
        self._parsers: dict[str, type[NMEAParser]] = {}

    def register_parser(self, prefix: str, parser_class: type[NMEAParser]) -> None:
        """Register a parser for a message type.

        Args:
            prefix: NMEA prefix (e.g., 'PNORI')
            parser_class: Parser class with from_nmea() classmethod

        Example:
            >>> from adcp_recorder.parsers import PNORI
            >>> router = MessageRouter()
            >>> router.register_parser('PNORI', PNORI)

        """
        self._parsers[prefix.upper()] = parser_class
        logger.debug(f"Registered parser for {prefix}")

    def route(self, sentence: str) -> Any | None:
        """Route sentence to appropriate parser.

        Args:
            sentence: NMEA sentence string

        Returns:
            Parsed message object or None if parser not found

        Raises:
            ValueError: If parsing fails

        """
        prefix = extract_prefix(sentence)
        if prefix is None:
            return None

        prefix = prefix.upper()
        if prefix not in self._parsers:
            return None

        parser_class = self._parsers[prefix]
        return parser_class.from_nmea(sentence)


class SerialConsumer:
    """Consumes NMEA sentences from queue, parses, and stores to database.

    The consumer runs in a background thread, pulling lines from the queue,
    routing them to parsers, and inserting parsed data to the database.

    Attributes:
        queue: FIFO queue to consume from
        db_manager: Database manager
        router: Message router
        heartbeat_interval: Seconds between heartbeat updates

    Example:
        >>> from adcp_recorder.parsers import PNORI
        >>> queue = Queue(maxsize=1000)
        >>> db = DatabaseManager(':memory:')
        >>> router = MessageRouter()
        >>> router.register_parser('PNORI', PNORI)
        >>> consumer = SerialConsumer(queue, db, router)
        >>> consumer.start()
        >>> # ... later ...
        >>> consumer.stop()

    """

    def __init__(
        self,
        queue: Queue,
        db_manager: DatabaseManager,
        router: MessageRouter,
        heartbeat_interval: float = 5.0,
        file_writer: FileWriter | None = None,
    ):
        """Initialize serial consumer.

        Args:
            queue: Queue to consume from
            db_manager: Database manager
            router: Message router
            heartbeat_interval: Seconds between heartbeat updates

        """
        self._queue = queue
        self._db_manager = db_manager
        self._router = router
        self._heartbeat_interval = heartbeat_interval
        self._file_writer = file_writer
        if file_writer and isinstance(file_writer.base_path, str):
            bp = file_writer.base_path
        else:
            bp = "."
        self._binary_writer = BinaryBlobWriter(bp)

        self._running = False
        self._thread: threading.Thread | None = None
        self._last_heartbeat = time.time()

    @property
    def is_running(self) -> bool:
        """Check if consumer is running."""
        return self._running

    @property
    def last_heartbeat(self) -> float:
        """Get timestamp of last heartbeat update."""
        return self._last_heartbeat

    def start(self) -> None:
        """Start the consumer thread."""
        if self._running:
            logger.warning("Consumer already running")
            return

        self._running = True
        self._thread = threading.Thread(target=self._consume_loop, daemon=True)
        self._thread.start()
        logger.info("Serial consumer started")

    def stop(self) -> None:
        """Stop the consumer thread gracefully."""
        if not self._running:
            return

        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=10.0)
            if self._thread.is_alive():
                logger.warning("Consumer thread did not exit cleanly within timeout")
        logger.info("Serial consumer stopped")

    def _update_heartbeat(self) -> None:
        """Update heartbeat timestamp."""
        self._last_heartbeat = time.time()

    def _consume_loop(self) -> None:
        """Main consume loop (runs in thread)."""
        logger.info("Consumer loop starting")
        conn = self._db_manager.get_connection()

        try:
            while True:
                try:
                    # Pull from queue - use short timeout if stopping to drain quickly
                    limit = 1.0 if self._running else 0.1
                    item = self._queue.get(timeout=limit)
                except Empty:
                    # Queue empty - if we are stopping, we are done
                    if not self._running:
                        logger.debug("Queue empty and stopping, exiting loop")
                        break

                    # Periodic flush of stale buffers
                    if self._file_writer:
                        try:
                            self._file_writer.flush_stale(max_age_seconds=300)
                        except Exception as e:
                            logger.error(f"Failed to flush stale Parquet records: {e}")
                    continue
                except Exception as e:
                    logger.error(f"Unexpected error getting from queue: {e}", exc_info=True)
                    continue
                # Binary chunk streaming handling
                try:
                    if isinstance(item, BinaryChunk):
                        # Start of blob
                        if item.start:
                            # record parse error/marker once at blob start
                            insert_parse_error(
                                conn,
                                "<BINARY_BLOB>",
                                error_type="BINARY_DATA",
                                error_message="Binary blob captured",
                            )
                            insert_raw_line(
                                conn,
                                "<BINARY_BLOB>",
                                parse_status="FAIL",
                                error_message="Binary blob captured",
                            )
                            # start file
                            self._binary_writer.start_blob(item.data)
                            if self._file_writer:
                                self._file_writer.write_invalid_record(
                                    "BINARY", item.data.decode("ascii", errors="replace")
                                )
                                # Add to parquet
                                self._file_writer.write_record(
                                    "errors",
                                    {
                                        "raw_content": item.data.decode("ascii", errors="replace"),
                                        "error_type": "BINARY_DATA",
                                        "error_message": "Binary blob captured",
                                        "attempted_prefix": "BINARY",
                                    },
                                )
                                self._file_writer.write_record(
                                    "raw_lines",
                                    {
                                        "content": item.data.decode("ascii", errors="replace"),
                                        "parse_status": "FAIL",
                                        "record_type": "BINARY",
                                        "error_message": "Binary blob captured",
                                    },
                                )
                        elif item.end:
                            path = self._binary_writer.finish_blob()
                            logger.info(f"Binary blob saved to {path}")
                        else:
                            # middle chunk
                            self._binary_writer.append_chunk(item.data)

                        self._update_heartbeat()
                        continue

                    # Otherwise it's a normal line (bytes)
                    line_bytes = item

                    # Process line
                    try:
                        self._process_line(conn, line_bytes)
                    except Exception as e:
                        logger.error(f"Error processing line: {e}", exc_info=True)
                        # Try to keep going
                except Exception as e:
                    logger.error(f"Consumer loop processing error: {e}", exc_info=True)

                try:
                    self._update_heartbeat()
                except Exception as e:
                    logger.error(f"Heartbeat update failed: {e}")
        finally:
            with contextlib.suppress(Exception):
                self._binary_writer.finish_blob()

            # Flush to disk for visibility on Windows before closing
            try:
                conn.execute("CHECKPOINT;")
            except Exception as e:
                logger.warning(f"Failed to checkpoint on exit: {e}")

            self._db_manager.close()
            logger.info("Consumer loop exiting")

    def _record_error(
        self,
        conn: duckdb.DuckDBPyConnection,
        raw_content: str | bytes,
        error_type: str,
        error_message: str,
        prefix: str = "UNKNOWN",
        parse_status: str = "FAIL",
        checksum_expected: str | None = None,
        checksum_actual: str | None = None,
        measurement_date: str | None = None,
        measurement_time: str | None = None,
        measurement_datetime: Any | None = None,
    ) -> None:
        """Centralized error recording for parsers and processing."""
        if isinstance(raw_content, bytes):
            raw_str = raw_content.decode("ascii", errors="replace")
        else:
            raw_str = raw_content

        logger.warning(f"{error_type} for {prefix}: {error_message}")

        # Insert to database
        insert_parse_error(
            conn,
            raw_str,
            error_type=error_type,
            error_message=error_message,
            attempted_prefix=prefix,
            checksum_expected=checksum_expected,
            checksum_actual=checksum_actual,
            measurement_date=measurement_date,
            measurement_time=measurement_time,
            measurement_datetime=measurement_datetime,
        )
        insert_raw_line(
            conn,
            raw_str,
            parse_status=parse_status,
            record_type=prefix,
            error_message=error_message,
            checksum_valid=False if error_type == "CHECKSUM_ERROR" else None,
            measurement_date=measurement_date,
            measurement_time=measurement_time,
            measurement_datetime=measurement_datetime,
        )

        # Write to files
        if self._file_writer:
            with contextlib.suppress(Exception):
                # Legacy text logs
                if error_type == "BINARY_DATA":
                    self._file_writer.write_invalid_record("BINARY", raw_str)
                elif error_type == "DECODE_ERROR":
                    self._file_writer.write_error(f"Decode error: {raw_str} - {error_message}")
                else:
                    self._file_writer.write_invalid_record(prefix, raw_str)

                # Parquet export
                self._file_writer.write_record(
                    "errors",
                    {
                        "raw_content": raw_str,
                        "error_type": error_type,
                        "error_message": error_message,
                        "attempted_prefix": prefix,
                        "measurement_date": measurement_date,
                        "measurement_time": measurement_time,
                        "measurement_datetime": measurement_datetime,
                    },
                )
                self._file_writer.write_record(
                    "raw_lines",
                    {
                        "content": raw_str,
                        "parse_status": parse_status,
                        "record_type": prefix,
                        "error_message": error_message,
                        "checksum_valid": False if error_type == "CHECKSUM_ERROR" else None,
                        "measurement_date": measurement_date,
                        "measurement_time": measurement_time,
                        "measurement_datetime": measurement_datetime,
                    },
                )

    def _process_line(self, conn: duckdb.DuckDBPyConnection, line_bytes: bytes) -> None:
        """Process a single line from the queue.

        Args:
            conn: Database connection
            line_bytes: Line data as bytes

        """
        # 1. Check for binary data
        if is_binary_data(line_bytes):
            self._record_error(
                conn, line_bytes, "BINARY_DATA", "Binary data detected", prefix="BINARY"
            )
            return

        # 2. Decode to string
        try:
            sentence = line_bytes.decode("ascii").strip()
        except UnicodeDecodeError as e:
            self._record_error(conn, line_bytes, "DECODE_ERROR", str(e))
            return

        if not sentence:
            return

        # 3. Basic NMEA validation and Routing
        prefix = "UNKNOWN"
        try:
            # Extract prefix carefully
            try:
                prefix = extract_prefix(sentence)
            except ValueError:
                # Still try to route/handle if prefix extraction fails (e.g. malformed start)
                pass

            # Checksum validation if present
            if "*" in sentence:
                try:
                    if not validate_checksum(sentence):
                        data_part, provided = sentence.rsplit("*", 1)
                        computed = compute_checksum(sentence)
                        self._record_error(
                            conn,
                            sentence,
                            "CHECKSUM_ERROR",
                            f"Checksum mismatch: expected {computed}, got {provided}",
                            prefix=prefix,
                            checksum_expected=computed,
                            checksum_actual=provided,
                        )
                        return
                except ValueError as e:
                    self._record_error(conn, sentence, "CHECKSUM_ERROR", str(e), prefix=prefix)
                    return

            # Route to parser
            parsed = self._router.route(sentence)
            logger.debug(f"Routed {prefix} to {parsed.__class__.__name__ if parsed else 'None'}")

            # Extract date/time for raw_lines if available
            m_date = None
            m_time = None
            m_dt = None
            if parsed and hasattr(parsed, "to_dict"):
                try:
                    p_dict = parsed.to_dict()
                    m_date = p_dict.get("date")
                    m_time = p_dict.get("time")
                    if m_date and m_time:
                        from datetime import datetime

                        # Try common MMDDYY/HHMMSS format
                        try:
                            m_dt = datetime.strptime(f"{m_date}{m_time}", "%m%d%y%H%M%S")
                        except ValueError:
                            # Try YYMMDD
                            try:
                                m_dt = datetime.strptime(f"{m_date}{m_time}", "%y%m%d%H%M%S")
                            except ValueError:
                                pass
                except Exception:
                    pass

            if parsed is None:
                # Unknown message type - log and store as PENDING
                logger.debug(f"Unknown message type: {prefix}")
                insert_raw_line(
                    conn,
                    sentence,
                    parse_status="PENDING",
                    record_type=prefix,
                    checksum_valid=True if "*" in sentence else None,
                    error_message=f"No parser for {prefix}",
                )
                if self._file_writer:
                    with contextlib.suppress(Exception):
                        self._file_writer.write(prefix, sentence)
                    with contextlib.suppress(Exception):
                        self._file_writer.write_record(
                            "raw_lines",
                            {
                                "content": sentence,
                                "parse_status": "PENDING",
                                "record_type": prefix,
                                "error_message": f"No parser for {prefix}",
                                "checksum_valid": True if "*" in sentence else None,
                                "measurement_date": m_date,
                                "measurement_time": m_time,
                                "measurement_datetime": m_dt,
                            },
                        )
                return

            # 4. Successfully parsed - insert to database
            self._store_parsed_message(conn, sentence, prefix, parsed)

            # Also insert to raw_lines
            insert_raw_line(
                conn,
                sentence,
                parse_status="OK",
                record_type=prefix,
                checksum_valid=True if "*" in sentence else None,
                measurement_date=m_date,
                measurement_time=m_time,
                measurement_datetime=m_dt,
            )

            if self._file_writer:
                with contextlib.suppress(Exception):
                    self._file_writer.write(prefix, sentence)
                with contextlib.suppress(Exception):
                    self._file_writer.write_record(
                        "raw_lines",
                        {
                            "content": sentence,
                            "parse_status": "OK",
                            "record_type": prefix,
                            "checksum_valid": True if "*" in sentence else None,
                            "measurement_date": m_date,
                            "measurement_time": m_time,
                            "measurement_datetime": m_dt,
                        },
                    )

        except ValueError as e:
            # Parse failed (ValidationError from from_nmea)
            self._record_error(conn, sentence, "PARSE_ERROR", str(e), prefix=prefix)

    def _store_parsed_message(
        self, conn: duckdb.DuckDBPyConnection, sentence: str, prefix: str, parsed: Any
    ) -> None:
        """Store parsed message to appropriate table and export to Parquet."""
        try:
            data = parsed.to_dict()
        except Exception as e:
            logger.error(f"Failed to convert parsed message to dict for {prefix}: {e}")
            return

        # 1. Export structured data to DuckLake (Parquet) - ISOLATED
        if self._file_writer:
            try:
                self._file_writer.write_record(prefix, data)
            except Exception as e:
                logger.error(f"Parquet export failed for {prefix}: {e}")

        # 2. Insert into database - ISOLATED
        try:
            if prefix in ("PNORI", "PNORI1", "PNORI2"):
                insert_pnori_configuration(conn, data, sentence)
            elif prefix in ("PNORS", "PNORS1", "PNORS2", "PNORS3", "PNORS4"):
                insert_sensor_data(conn, sentence, data)
            elif prefix in ("PNORC", "PNORC1", "PNORC2", "PNORC3", "PNORC4"):
                insert_velocity_data(conn, sentence, data)
            elif prefix in ("PNORH3", "PNORH4"):
                insert_header_data(conn, sentence, data)
            elif prefix == "PNORW":
                insert_pnorw_data(conn, sentence, data)
            elif prefix == "PNORB":
                insert_pnorb_data(conn, sentence, data)
            elif prefix == "PNORE":
                insert_pnore_data(conn, sentence, data)
            elif prefix == "PNORF":
                insert_pnorf_data(conn, sentence, data)
            elif prefix == "PNORWD":
                insert_pnorwd_data(conn, sentence, data)
            elif prefix == "PNORA":
                insert_pnora_data(conn, sentence, data)
            else:
                logger.warning(f"No database insert for {prefix}")
        except Exception as e:
            logger.error(f"Database insertion failed for {prefix}: {e}", exc_info=True)
