"""
Defines the ``BaseKurier`` abstract base class — the foundation for all Kurier
line implementations. It provides the common publish / consume interface and
serializes messages into the standardized ``KurierMessage`` envelope format.
"""

import asyncio
import datetime
from abc import ABC, abstractmethod
from collections.abc import Generator
from dataclasses import asdict, is_dataclass
from typing import Any

from pydantic import BaseModel

from ..hermes import Hermes
from ..utilities.compatibility import override
from ..utilities.helpers import is_not_builtin_type
from ..utilities.registrymixin import RegistryMixin
from ..utilities.serializers import JsonSerializer, Serializer
from ..utilities.uuidv7 import UUIDv7
from .exceptions import KurierPublishError
from .kurier_message import KurierMessage


class BaseKurier(RegistryMixin[str, "BaseKurier"], ABC):
    """
    Abstract base class for all Kurier line implementations.

    Subclasses must implement ``_publish_to_bus``, ``_run_consumer_loop``,
    ``close``, and ``start_consumer``. Concrete publish logic, envelope
    serialization, and registry management are handled here.

    Every instance is registered globally by *name* (must be unique across
    all transport types). The global registry lives on ``BaseKurier._registry``
    while each subclass also maintains its own scoped registry via
    ``RegistryMixin.__init_subclass__``.
    """

    def __init__(self, name: str, serializer: Serializer | None = None):
        """
        Initialize and register a Kurier line.

        Args:
            name: A globally unique name for this line instance.
            serializer: Optional serializer for wire-format conversion.
                        Defaults to ``JsonSerializer``.

        Raises:
            ValueError: If a line with the same *name* is already registered.
        """
        self.name = name
        self.serializer: Serializer = serializer or JsonSerializer()
        existing = BaseKurier._registry.get(name)
        if existing is not None:
            raise ValueError(f"A line with name '{name}' is already registered ({type(existing).__name__})")

    def _register(self) -> None:
        """Register this instance in both the global and subclass registries.

        Subclasses should call this at the end of their ``__init__`` after all
        setup is complete, so that partially-initialized instances are never
        left in the registry on error.
        """
        BaseKurier._registry[self.name] = self
        self.add_to_registry(self.get_id(), self)

    @override
    def get_id(self) -> str:
        return self.name

    @classmethod
    @override
    def from_registry(cls, identifier: str | Any) -> Any:
        """
        Resolve an identifier to a registered ``BaseKurier`` instance.

        If *identifier* is already a ``BaseKurier`` instance it is returned
        directly. Otherwise it is looked up by name in the subclass registry
        first, then in the global ``BaseKurier._registry``.

        Args:
            identifier: A line name (str) or an existing ``BaseKurier`` instance.

        Raises:
            ValueError: If no line with the given name is found.
        """
        if isinstance(identifier, cls):
            return identifier

        item = cls.get_item(str(identifier)) or BaseKurier._registry.get(str(identifier))
        if item is None:
            raise ValueError(f"No {cls.__name__} registered with name: '{identifier}'")
        return item

    @classmethod
    @override
    def unregister(cls, item: "BaseKurier") -> None:
        super().unregister(item)
        BaseKurier._registry.pop(item.get_id(), None)

    async def publish_async(self, message: Any, meta_data: dict[str, Any] | None = None):
        """
        Serialize *message* into a ``KurierMessage`` envelope and send it
        via the transport-specific ``_publish_to_bus`` method.

        This is the async variant — use :meth:`publish` for synchronous code.

        Args:
            message: The message object (Pydantic model, dataclass, or primitive).
            meta_data: Optional dictionary of extra metadata attached to the envelope.

        Raises:
            KurierPublishError: If publishing fails for any reason.
        """
        if meta_data is None:
            meta_data = {}
        try:
            envelope_msg: KurierMessage = self._serialize_to_envelope(message, meta_data)
            json_payload = self._dump_envelope(envelope_msg)
            Hermes.trace(f"Serialized message payload: {json_payload}")

            result = self._publish_to_bus(json_payload, meta_data)
            if asyncio.iscoroutine(result):
                await result

            cid = envelope_msg.correlation_id or "N/A"

            Hermes.info(f"Published message {type(message).__name__} (CorrelationID: {cid})")
        except Exception as e:
            Hermes.error(f"Error publishing message: {e}")
            raise KurierPublishError(f"Failed to publish message: {e}") from e

    async def publish_batch_async(
        self,
        messages: list[Any],
        meta_data: dict[str, Any],
        batch_size: int = 25,
    ):
        """
        Publish a list of messages in chunked batches sharing a single correlation ID.

        This is the async variant — use :meth:`publish_batch` for synchronous code.

        Args:
            messages: The list of message objects to publish.
            meta_data: Metadata dictionary attached to every envelope in the batch.
            batch_size: Maximum number of messages per chunk (default 25).

        Raises:
            KurierPublishError: If any chunk fails to publish.
        """

        correlation_id = UUIDv7.get_as_str()
        Hermes.info(f"Starting batch publish of {len(messages)} messages (Batch CorrelationID: {correlation_id})")

        count = 0
        try:
            for chunk in self.chunk_messages(messages, batch_size):
                for message in chunk:
                    envelope_msg: KurierMessage = self._serialize_to_envelope(message, meta_data, correlation_id)
                    result = self._publish_to_bus(self._dump_envelope(envelope_msg), meta_data)
                    if asyncio.iscoroutine(result):
                        await result
                    count += 1

                # Yield between chunks so large batches do not monopolize the event loop.
                await asyncio.sleep(0)
            Hermes.info(f"Successfully published batch of {len(messages)} messages (Batch CorrelationID: {correlation_id})")
        except Exception as e:
            Hermes.error(f"Error during batch publish (Batch CorrelationID: {correlation_id}): {e}")
            raise KurierPublishError(f"Failed to publish batch: {e}") from e

    def publish(self, message: Any, meta_data: dict[str, Any] | None = None):
        """
        Publish a message synchronously.

        If an event loop is already running (e.g. inside an async application),
        this creates a new thread to run the coroutine.  Otherwise it simply
        calls ``asyncio.run``.

        For async code, use :meth:`publish_async` instead.

        Args:
            message: The message object (Pydantic model, dataclass, or primitive).
            meta_data: Optional dictionary of extra metadata attached to the envelope.

        Raises:
            KurierPublishError: If publishing fails for any reason.
        """
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = None

        if loop and loop.is_running():
            import concurrent.futures

            with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
                future = pool.submit(asyncio.run, self.publish_async(message, meta_data))
                return future.result()
        else:
            return asyncio.run(self.publish_async(message, meta_data))

    def publish_batch(
        self,
        messages: list[Any],
        meta_data: dict[str, Any],
        batch_size: int = 25,
    ):
        """
        Publish a list of messages in batches, synchronously.

        For async code, use :meth:`publish_batch_async` instead.

        Args:
            messages: The list of message objects to publish.
            meta_data: Metadata dictionary attached to every envelope in the batch.
            batch_size: Maximum number of messages per chunk (default 25).

        Raises:
            KurierPublishError: If any chunk fails to publish.
        """
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = None

        if loop and loop.is_running():
            import concurrent.futures

            with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
                future = pool.submit(asyncio.run, self.publish_batch_async(messages, meta_data, batch_size))
                return future.result()
        else:
            return asyncio.run(self.publish_batch_async(messages, meta_data, batch_size))

    @abstractmethod
    def _publish_to_bus(self, json_message: str, meta_data: dict[str, Any]):
        """Send the serialized JSON envelope to the underlying broker."""
        raise NotImplementedError()

    @staticmethod
    def chunk_messages(messages: list[Any], batch_size: int) -> Generator[list[Any], Any, None]:
        """Yield successive *batch_size*-length slices from *messages*."""
        length = len(messages)
        for ndx in range(0, length, batch_size):
            yield messages[ndx : min(ndx + batch_size, length)]

    @abstractmethod
    def _run_consumer_loop(self):
        """
        Abstract method for the internal passenger consumption loop of the line.
        This typically runs in a separate thread/task and dispatches messages via the Station.
        """
        raise NotImplementedError("Subclasses must implement the '_run_consumer_loop' method for consuming passengers.")

    @abstractmethod
    def close(self):
        """
        Abstract method to gracefully close the line connection and clean up resources.
        """
        raise NotImplementedError("Subclasses must implement the 'close' method for proper shutdown.")

    def _serialize_to_envelope(
        self,
        message: Any,
        meta_data: dict[str, Any],
        correlation_id: str | None = None,
    ) -> KurierMessage:  # type: ignore
        """
        Wrap a Python object in a ``KurierMessage`` envelope.

        Custom objects (Pydantic models, dataclasses) are converted to dicts,
        then serialized into the ``message_payload`` field using the line's
        ``serializer``. Primitive types are serialized directly.

        Args:
            message: The message object to serialize.
            meta_data: Metadata dictionary to attach to the envelope.
            correlation_id: Optional shared correlation ID (e.g. for batches).

        Returns:
            A ``KurierMessage`` instance ready to be dumped to JSON.
        """
        if is_not_builtin_type(message):
            if is_dataclass(message):
                message_payload = asdict(message)

            elif isinstance(message, BaseModel):
                message_payload = message.model_dump()

            else:
                message_payload = message.__dict__

        else:
            message_payload = message

        def serialize_datetimes(data: Any) -> Any:
            if isinstance(data, dict):
                return {k: serialize_datetimes(v) for k, v in data.items()}
            if isinstance(data, list):
                return [serialize_datetimes(v) for v in data]
            if isinstance(data, datetime.datetime):
                return data.isoformat()
            return data

        message_payload = serialize_datetimes(message_payload)

        message_payload = self.serializer.serialize(message_payload)

        if correlation_id is None:
            envelope = KurierMessage(
                object_type_name=type(message).__name__,
                qualified_object_type_name=f"{type(message).__module__}.{type(message).__qualname__}",
                message_payload=message_payload,
                metadata=meta_data,
            )

        else:
            envelope = KurierMessage(
                correlation_id=correlation_id,
                object_type_name=type(message).__name__,
                qualified_object_type_name=f"{type(message).__module__}.{type(message).__qualname__}",
                message_payload=message_payload,
                metadata=meta_data,
            )

        return envelope

    def _dump_envelope(self, envelope: KurierMessage) -> str | bytes:
        """Serialize an envelope using the line serializer's envelope-aware API."""
        return self.serializer.serialize_envelope(envelope)

    @classmethod
    @abstractmethod
    def start_consumer(cls, name: str) -> bool:
        """
        Abstract class method to start the passenger consumer for a named line instance.

        Args:
            name (str): The name of the line instance.

        Returns:
            bool: True if the consumer started successfully.
        """
        raise NotImplementedError("Subclasses must implement the 'start_consumer' class method.")
