import logging

import aio_pika
from aio_pika.abc import AbstractRobustExchange, AbstractRobustQueue
from pika.exchange_type import ExchangeType

from amqp.adapter.data_message_factory import DataMessageFactory
from amqp.adapter.logging_utils import (
    logging_debug,
    logging_error,
    logging_info,
    logging_warning,
)
from amqp.model.model import AMQRoute, DataMessage
from amqp.router.router_base import (
    AMQ_REPLY_TO_EXCHANGE,
    AMQ_RPC_EXCHANGE,
    DLQ_EXCHANGE,
)
from amqp.router.utils import sanitize_as_rabbitmq_name


class RabbitMQClient:
    PREFETCH_COUNT = 1
    DLQ_ARGS = {"x-dead-letter-exchange": DLQ_EXCHANGE}

    def __init__(self, configuration, router):
        self.channel: aio_pika.RobustChannel | None = None
        self.connection: aio_pika.RobustConnection | None = None
        self.rpc_exchange: aio_pika.RobustExchange | None = None
        self.reply_exchange: aio_pika.RobustExchange | None = None
        self.amq_configuration = configuration
        logging.info("*******************************************")
        logging.info(
            "******** RabbitMQClient.init(): %s",
            self.amq_configuration.dispatch.amq_host,
        )
        logging.info("*******************************************")
        self.router = router
        # this.meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);

        self.reply_to_exchange: AbstractRobustExchange | None = (
            None  # when sending response to RPC request
        )
        self.reply_to_queue: AbstractRobustQueue | None = (
            None  # when receiving response to RPC request
        )

    async def init(self, loop) -> AbstractRobustExchange:
        await self.setup_amq_channel(loop)
        await self.router.init_routing_paths_consumer(self.router.channel)
        cm_reply_to_queue = self.router.get_reply_to_queue_name()
        self.reply_to_queue = await self.channel.declare_queue(
            name=cm_reply_to_queue, durable=True, exclusive=False, auto_delete=False
        )
        self.reply_to_exchange = await self.channel.declare_exchange(
            AMQ_REPLY_TO_EXCHANGE, type=ExchangeType.direct
        )
        await self.reply_to_queue.bind(
            exchange=self.reply_to_exchange, routing_key=cm_reply_to_queue
        )
        return self.reply_to_exchange

    def cleanup(self):
        logging_info("RabbitMQClient.cleanup()")
        self.router.cleanup()  # de-register route(s)
        if self.router.is_open(self.channel):
            try:
                self.channel.close()
                if not self.connection.is_closed:
                    self.connection.close()
            except Exception as e:
                logging_error(
                    "RabbitMQClient.PreDestroy cleanup - connection close error: %s",
                    e,
                )

    async def register_inbound_routes(self, route_mapping: str, callback):
        """
        Register inbound routes for the AMQ adapter. The route mapping is a comma-separated string that contains
        list of routes. A route is the mapping of domain and path to the exchange and queue name.
        This method declare (and thus create) the queue and exchange, bind them together using the
        regex-like routing expression given in the route.
        After this the Service is listening on (consuming from) the queue for incoming messages.
        """
        if self.router.is_open(self.channel):
            queue_args = self.DLQ_ARGS if self.amq_configuration.dispatch.use_dlq else None
            logging_info("RabbitMQClient register routes, cfg mapping: [%s]", route_mapping)
            # convert the comma-separated string into list of AMQRoute objects
            requested_routes = self.router.unwrap_route_list(route_mapping)
            for route in requested_routes:
                try:
                    # Note: AMQRoute allows one of the exchange or queue to be None, in which case the default is used
                    queue_name = (
                        route.queue if route.queue else self.router.get_consume_queue_name()
                    )
                    logging_info("RabbitMQClient register route: [%s]", route)
                    queue: AbstractRobustQueue = await self.channel.declare_queue(
                        name=queue_name,
                        durable=True,
                        exclusive=False,
                        auto_delete=False,
                        arguments=queue_args,
                    )
                    _exchange_name: str = route.exchange if route.exchange else AMQ_RPC_EXCHANGE
                    exchange: AbstractRobustExchange = await self.channel.declare_exchange(
                        name=_exchange_name, type=ExchangeType.topic
                    )
                    await queue.bind(exchange, route.key)
                    _c_tag = await queue.consume(callback, no_ack=False)
                    # Register the route with the router and publish its detail to other adapters
                    await self.router.register_route(
                        AMQRoute(
                            route.component_name,
                            _exchange_name,
                            queue_name,
                            sanitize_as_rabbitmq_name(route.key),
                            route.timeout,
                            route.valid_until,
                        )
                    )
                    logging_info(
                        " [%s] AMQ connection initialized for route: %s, listens on: %s/%s",
                        _c_tag,
                        route,
                        exchange,
                        queue_name,
                    )
                except Exception as err:
                    logging_error("Callback registration INCOMPLETE, %s", err)
        else:
            logging_warning("Channel is null, cannot register routes.")

    async def register_data_reply_callback(self, rpc_callback):
        _c_tag = await self.reply_to_queue.consume(callback=rpc_callback, no_ack=False)
        logging_info(
            " [%s] RabbitMQClient register data reply callback on queue: %s",
            _c_tag,
            self.reply_to_queue.name,
        )

    async def setup_amq_channel(self, loop):
        """
        Set up the RabbitMQ connection
        :return: nothing, it applies changes to instance variables
        """
        self.connection = await aio_pika.connect_robust(
            host=self.amq_configuration.dispatch.amq_host,
            port=self.amq_configuration.dispatch.amq_port,
            login=self.amq_configuration.dispatch.rabbit_mq_user,
            password=self.amq_configuration.dispatch.rabbit_mq_password,
            loop=loop,
        )
        logging_debug(
            "RabbitMQClient.setupAMQ, got connection, open=%s",
            self.connection.is_closed,
        )
        self.channel = await self.connection.channel()
        await self.channel.set_qos(prefetch_count=self.PREFETCH_COUNT)
        await self.router.init_internal_routing_paths(self.channel)
        self.router.set_route_added_notifier(
            lambda route: logging_debug(
                " RouteAdded, setting up metrics: amqp.adapter.exchange.%s",
                route.exchange,
            )
        )

    async def send_message(self, message: DataMessage):
        """
        The API routine that sends a datagram message (DataMessage) to the destination. The Destination
        is determined from the DataMessage metadata section, using `domain` and `path` as key.
        :param message: message to send
        :return: nothing (void) as submitting message to RabbitMQ exchange does not return anything
         meaningful. There will be asynch ACK later that will confirm the message was sent to RMQ.
        """
        try:
            route = self.router.find_route_by_message(message)
            if route and self.rpc_exchange:
                if route.timeout <= 0:
                    # No response expected
                    pika_message: aio_pika.message.Message = aio_pika.message.Message(
                        body=DataMessageFactory.serialize(message),
                        content_encoding="utf-8",
                        delivery_mode=2,
                    )
                    await self.rpc_exchange.publish(
                        message=pika_message,
                        routing_key=self.router.get_routing_key(
                            message
                        ),  # context path is a routing key
                    )
                    logging_info(f"Msg Publish (no-reply), messageId: {message.id()}, to: {route}")
                else:
                    # This is RPC call, there should be response
                    pika_message: aio_pika.message.Message = aio_pika.message.Message(
                        body=DataMessageFactory.serialize(message),
                        content_encoding="utf-8",
                        delivery_mode=2,
                        correlation_id=str(message.id()),
                        reply_to=self.router.get_reply_to_queue_name(),
                    )
                    await self.rpc_exchange.publish(
                        message=pika_message,
                        routing_key=self.router.get_routing_key(
                            message
                        ),  # context path is a routing key
                    )
                    logging_info(
                        "Msg Publish (RPC call), messageId: %s, to: %s",
                        message.id().as_string(),
                        route.as_string(),
                    )
            else:
                logging_warning(
                    "Message not sent, no route for %s/%s",
                    message.domain(),
                    message.path(),
                )

        except Exception as err:
            logging_error("Send message failed: %s", err)

    async def request_routes(self):
        await self.router.request_routes_from_adapters()

    def find_route(self, domain: str, path: str) -> AMQRoute:
        return self.router.find_route(domain, path)
