import asyncio
import threading
from typing import Any

import pika
import pika.exceptions

from kurier.configurations.models.rabbitmq import RabbitMQConfig
from kurier.core.basekurier import BaseKurier
from kurier.core.bus_types import BusType
from kurier.core.station import Station
from kurier.hermes import Hermes
from kurier.utilities.compatibility import override
from kurier.utilities.serializers import Serializer


class RabbitMQLine(BaseKurier):
    """
    A Kurier line implementation for RabbitMQ.
    Manages connections, publishes passengers to exchanges/queues,
    and consumes passengers, dispatching them via the Station.
    """

    bus_type = BusType.RABBIT_MQ

    def __init__(self, config: RabbitMQConfig, serializer: Serializer | None = None):
        self._config: RabbitMQConfig = config

        super().__init__(self._config.name, serializer=serializer)

        self._publisher_connection: pika.BlockingConnection | None = None
        self._publisher_channel: pika.adapters.blocking_connection.BlockingChannel | None = None

        self._connect_publisher()

        self._consumer_thread: threading.Thread | None = None
        self._running: bool = False
        self._consumer_connection: pika.BlockingConnection | None = None
        self._consumer_channel: pika.adapters.blocking_connection.BlockingChannel | None = None
        self._consumer_loop: asyncio.AbstractEventLoop | None = None

        self._register()
        Hermes.info(f"RabbitMQLine '{self.name}' initialized and registered.")

    def _connect_publisher(self):
        try:
            if self._publisher_connection and self._publisher_connection.is_open:
                self._publisher_connection.close()

            credentials = pika.PlainCredentials(self._config.username, self._config.password)
            self._publisher_connection = pika.BlockingConnection(
                pika.ConnectionParameters(
                    host=self._config.host,
                    port=self._config.port,
                    virtual_host=self._config.virtual_host,
                    credentials=credentials,
                    **self._config.connection_properties,
                )
            )
            self._publisher_channel = self._publisher_connection.channel()

            if self._config.exchange_name:
                self._publisher_channel.exchange_declare(
                    exchange=self._config.exchange_name,
                    exchange_type=self._config.exchange_type,
                    durable=True,
                )

            if self._config.queue_name:
                self._publisher_channel.queue_declare(queue=self._config.queue_name, durable=True)

                if self._config.exchange_name and self._config.routing_key:
                    self._publisher_channel.queue_bind(
                        exchange=self._config.exchange_name,
                        queue=self._config.queue_name,
                        routing_key=self._config.routing_key,
                    )

            Hermes.info(f"RabbitMQLine '{self.name}': Publisher connected and ready on {self._config.host}.")

        except pika.exceptions.AMQPConnectionError as e:
            Hermes.error(f"RabbitMQLine '{self.name}': Could not connect publisher to RabbitMQ at {self._config.host}: {e}")
            self._publisher_connection = None
            self._publisher_channel = None

        except Exception as e:
            Hermes.error(f"RabbitMQLine '{self.name}': An unexpected error during publisher connection setup: {e}")
            self._publisher_connection = None
            self._publisher_channel = None
            raise

    @override
    async def _publish_to_bus(self, json_message: str, meta_data: dict[str, Any]):
        """
        Publishes a passenger to the configured RabbitMQ exchange or queue.
        """

        if not self._publisher_connection or not self._publisher_connection.is_open:
            Hermes.warn(f"RabbitMQLine '{self.name}': Publisher connection lost, attempting to reconnect...")
            self._connect_publisher()

        if not self._publisher_connection or not self._publisher_connection.is_open:
            Hermes.warn(f"RabbitMQLine '{self.name}': Failed to reconnect publisher. Cannot publish passenger.")
            return

        try:
            target_exchange = self._config.exchange_name or ""
            target_routing_key = self._config.routing_key or ""

            if not target_exchange and self._config.queue_name:
                target_routing_key = self._config.queue_name

            self._publisher_channel.basic_publish(
                exchange=target_exchange,
                routing_key=target_routing_key,
                body=json_message.encode("utf-8"),
                properties=pika.BasicProperties(delivery_mode=2),
            )

        except pika.exceptions.AMQPChannelError as e:
            Hermes.warn(f"RabbitMQLine '{self.name}': Channel error during publish: {e}")
            try:
                if self._publisher_connection and self._publisher_connection.is_open:
                    self._publisher_channel.close()
                    self._publisher_channel = self._publisher_connection.channel()
            except Exception:
                pass

        except pika.exceptions.AMQPConnectionError as e:
            Hermes.warn(f"RabbitMQLine '{self.name}': Connection error during publish: {e}. Reconnecting...")
            self._connect_publisher()

        except Exception as e:
            Hermes.fatal(f"RabbitMQLine '{self.name}': An unexpected error occurred during publish: {e}")
            raise

    @override
    def _run_consumer_loop(self):
        """
        The main loop for the RabbitMQ consumer thread.

        Runs in a dedicated daemon thread (not the main event loop) and uses
        pika's ``SelectConnection`` for non-blocking I/O. Dispatches messages
        to the Station via a thread-local asyncio event loop.

        Shutdown is triggered from ``close()`` by calling
        ``connection.add_callback_threadsafe(stop)`` which cleanly breaks the
        I/O loop without blocking the caller.
        """
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        self._consumer_loop = loop

        try:
            credentials = pika.PlainCredentials(self._config.username, self._config.password)
            params = pika.ConnectionParameters(
                host=self._config.host,
                port=self._config.port,
                virtual_host=self._config.virtual_host,
                credentials=credentials,
                **self._config.connection_properties,
            )

            consumer_queue_name = self._config.queue_name or f"Kurier_consumer_{self.name}"

            def on_connected(connection):
                connection.channel(on_open_callback=on_channel_open)

            def on_channel_open(channel):
                self._consumer_channel = channel
                channel.queue_declare(
                    queue=consumer_queue_name,
                    durable=True,
                    callback=lambda _frame: on_queue_declared(channel),
                )

            def on_queue_declared(channel):
                if self._config.exchange_name and self._config.routing_key:
                    channel.queue_bind(
                        exchange=self._config.exchange_name,
                        queue=consumer_queue_name,
                        routing_key=self._config.routing_key,
                        callback=lambda _frame: start_consuming(channel),
                    )
                elif self._config.exchange_name:
                    channel.queue_bind(
                        exchange=self._config.exchange_name,
                        queue=consumer_queue_name,
                        routing_key="",
                        callback=lambda _frame: start_consuming(channel),
                    )
                else:
                    start_consuming(channel)

            def start_consuming(channel):
                channel.basic_consume(
                    queue=consumer_queue_name,
                    on_message_callback=on_message,
                    auto_ack=False,
                )
                Hermes.info(f"RabbitMQLine '{self.name}': Consumer started for queue '{consumer_queue_name}'.")

            def on_message(ch, method, _properties, body):
                envelope_str = body.decode("utf-8")
                loop.run_until_complete(Station.dispatch(self, envelope_str))
                ch.basic_ack(delivery_tag=method.delivery_tag)

            self._consumer_connection = pika.SelectConnection(
                parameters=params,
                on_open_callback=on_connected,
                on_open_error_callback=lambda _conn, err: Hermes.error(f"RabbitMQLine '{self.name}': Failed to connect consumer: {err}"),
                on_close_callback=lambda _conn, reason: Hermes.info(f"RabbitMQLine '{self.name}': Consumer connection closed: {reason}"),
            )
            self._consumer_connection.ioloop.start()

        except Exception as e:
            Hermes.fatal(f"RabbitMQLine '{self.name}': An unhandled error occurred in consumer loop: {e}")

        finally:
            if self._consumer_loop and self._consumer_loop.is_running():
                self._consumer_loop.stop()
            if self._consumer_loop and not self._consumer_loop.is_closed():
                self._consumer_loop.close()

            Hermes.info(f"RabbitMQLine '{self.name}': Consumer loop stopped and connections closed.")

    @classmethod
    @override
    def start_consumer(cls, name: str | None = None) -> bool:
        """
        Starts consumers for specific or all RabbitMQLines.
        Refactored to use RegistryMixin logic.
        """
        lines: list[RabbitMQLine]

        if not name:
            lines = [line for line in cls.get_all_items() if isinstance(line, RabbitMQLine)]
        else:
            item = cls.get_item(name)
            lines = [item] if isinstance(item, RabbitMQLine) else []

        if not lines and name:
            Hermes.warning(f"No RabbitMQLine found with name '{name}' to start.")
            return False

        for line in lines:
            line._consumer_thread = threading.Thread(target=line._run_consumer_loop)
            line._consumer_thread.daemon = True
            line._consumer_thread.start()

            line._running = True

            Hermes.info(f"RabbitMQLine '{line.name}': Consumer thread started.")

        return True

    @override
    async def close(self):
        """
        Gracefully closes the RabbitMQLine, stopping the consumer thread and
        closing publisher and consumer connections.
        """

        Hermes.info(f"RabbitMQLine '{self.name}': Initiating shutdown...")
        self._running = False

        if self._publisher_connection and self._publisher_connection.is_open:
            try:
                self._publisher_connection.close()
                Hermes.info(f"RabbitMQLine '{self.name}': Publisher connection closed.")
            except Exception as e:
                Hermes.fatal(f"RabbitMQLine '{self.name}': Error closing publisher connection: {e}")

        if self._consumer_thread and self._consumer_thread.is_alive():
            if self._consumer_connection and self._consumer_connection.is_open:
                try:
                    self._consumer_connection.add_callback_threadsafe(lambda: self._consumer_connection.close())
                    Hermes.info(f"RabbitMQLine '{self.name}': Signaled consumer connection close.")
                except Exception as e:
                    Hermes.fatal(f"RabbitMQLine '{self.name}': signaling consumer connection close: {e}")

            self._consumer_thread.join(timeout=10)
            if self._consumer_thread.is_alive():
                Hermes.warn(f"RabbitMQLine '{self.name}': Consumer thread did not terminate gracefully within timeout.")

        Hermes.info(f"RabbitMQLine '{self.name}': Closed.")
