import asyncio
import json
import logging
import uuid
from types import FunctionType

import aio_pika
from aio_pika import Message, DeliveryMode
from aio_pika.abc import AbstractRobustConnection, AbstractIncomingMessage, AbstractRobustChannel
from yarl import URL


logging.basicConfig(level=logging.INFO)


class AsyncPikaClient:
    _instance: "AsyncPikaClient" = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, publish_queue: str, consume_queue: str, url: URL, process_callable: FunctionType):
        if not hasattr(self, '_initialized'):
            self.queue_consumer = None
            self.queue_publish = None
            self.channel: AbstractRobustChannel | None = None
            self.url = url
            self.queue_publish_name = publish_queue
            self.queue_consume_name = consume_queue
            self.connection: None | AbstractRobustConnection = None
            self.process_callable = process_callable
            self._initialized = True

    @classmethod
    def instance(cls) -> "AsyncPikaClient":
        """
        Глобальный доступ к клиенту RabbitMQ.
        """
        if cls._instance is None:
            raise NameError('Object is not initialized')
        return cls._instance

    async def init(self):
        # подключение к каналу
        self.connection = await aio_pika.connect_robust(self.url)
        self.channel = await self.connection.channel()

        # объявляем очередь для отправки и получения
        self.queue_publish = await self.channel.declare_queue(
            self.queue_publish_name, durable=True
        )
        self.queue_consumer = await self.channel.declare_queue(
            self.queue_consume_name, durable=True
        )

    async def send_message(self, payload: dict, wait_response: bool = False):
        """Публикует сообщения в очередь"""
        if not self.channel:
            raise RuntimeError("Client is not initialized, Call init() first")

        # future используем если будем ждать ответ от получателя
        future: asyncio.Future | None = None
        correlation_id = str(uuid.uuid4())
        body = json.dumps(payload).encode()
        reply_queue_name = None

        if wait_response:
            reply_queue_name = ''
            future = asyncio.get_event_loop().create_future()
            reply_queue = await self.channel.declare_queue(
                reply_queue_name, durable=False, auto_delete=True)

            async def on_response(msg: AbstractIncomingMessage):
                if msg.correlation_id == correlation_id:
                    async with msg.process():
                        future.set_result(json.loads(msg.body.decode()))

            await reply_queue.consume(on_response, no_ack=False)

        message = Message(
            body,
            delivery_mode=DeliveryMode.PERSISTENT,
            correlation_id=correlation_id,
            reply_to=reply_queue_name
        )

        await self.channel.default_exchange.publish(
            message, routing_key=self.queue_publish_name
        )

        logging.info(f"Send message: {payload}")

        if not wait_response:
            return None

        try:
            return await asyncio.wait_for(future, timeout=0.1)
        finally:
            await reply_queue.cancel()

    async def _on_message(self, message: AbstractIncomingMessage) -> None:
        """Обработчик сообщений"""
        logging.info(f'Get message')
        async with message.process(ignore_processed=True):
            data = json.loads(message.body.decode())
            result = await self.process_callable(data, message.correlation_id, message.reply_to)
            if result:
                await self._reply_message(result, reply_to=message.reply_to, id_message=message.correlation_id)
            return None

    async def _reply_message(self, result: dict, reply_to, id_message) -> None:
        message = Message(
            body=json.dumps(result).encode(),
            correlation_id=id_message)
        await self.channel.default_exchange.publish(message, routing_key=reply_to)

    async def start_consumer(self) -> None:
        """Запускает общение в фоне"""
        if not self.connection:
            raise RuntimeError("Client is not initialized, Call init() first")

        asyncio.create_task(self._consume_loop())

    async def _consume_loop(self) -> None:
        """Бесконечный цикл для получения сообщений"""
        async with self.queue_consumer.iterator() as queue_iter:
            async for message in queue_iter:
                asyncio.create_task(self._on_message(message))

    async def close(self) -> None:
        """Закрыть канал и соединение."""
        if self.channel:
            await self.channel.close()
        if self.connection:
            await self.connection.close()
