import pika
import functools
from threading import Thread
from deprecated import deprecated
import network_constants as netconst

import logging
LOGGER = logging.getLogger(__name__)

@deprecated(reason='Can only receive messages with full connection. Use ReconnectingMessageManager for connection reuse instead.')
class MessageReceiver:

    _exchange_name = None
    _exchange_type = None
    _rabbit_mq_ip = None
    _rabbit_mq_port = None
    _auto_ack = None
    _queue_name = None

    _connection: pika.SelectConnection = None
    _message_received_callback = None
    _error_callback = None

    def __init__(self, exchange_name='beacon', exchange_type='fanout', rabbit_mq_ip=netconst.RABBIT_MQ_HOSTNAME, rabbit_mq_port=netconst.RABBIT_MQ_PORT, queue_name='', auto_ack=True):
        self._exchange_name = exchange_name
        self._exchange_type = exchange_type
        self._rabbit_mq_ip = rabbit_mq_ip
        self._rabbit_mq_port = rabbit_mq_port
        self._auto_ack = auto_ack
        self._queue_name = queue_name

    #region Connection establishment

    def _connect(self) -> pika.SelectConnection:
        connection = pika.SelectConnection(parameters=pika.ConnectionParameters(self._rabbit_mq_ip, self._rabbit_mq_port), 
            on_open_callback=self._connection_opened_callback, on_open_error_callback=self._connection_opened_error_callback, on_close_callback=self._connection_closed_callback)
        return connection

    def _connection_opened_callback(self, connection: pika.SelectConnection):
        # Create channel
        connection.channel(on_open_callback=self._channel_created_callback)

    def _connection_opened_error_callback(self, connection, err):
        LOGGER.error(f"RabbitMQ connection could not be established: {str(err)}")
        if self._error_callback != None:
            self._error_callback("Connection could not be established")

    def _connection_closed_callback(self, conn, error):
        if self._error_callback != None:
            self._error_callback("Connection closed")

    def _channel_created_callback(self, channel: pika.channel.Channel):
        # Create exchange
        cb = functools.partial(self._exchange_created_callback, userdata=channel)
        channel.exchange_declare(exchange=self._exchange_name, exchange_type=self._exchange_type, callback=cb)

    def _exchange_created_callback(self, answer_message, userdata):
        # Create queue
        channel: pika.channel.Channel = userdata
        cb = functools.partial(self._queue_created_callback, userdata=channel)
        exclusive_access = (self._queue_name == '')
        channel.queue_declare(queue=self._queue_name, exclusive=exclusive_access, callback=cb, auto_delete=exclusive_access)

    def _queue_created_callback(self, answer_message: pika.frame.Method, userdata):
        queue_name = answer_message.method.queue
        channel = userdata
        
        # Bind queue to exchange
        channel.queue_bind(exchange=self._exchange_name, queue=queue_name)
        LOGGER.info(f"RabbitMQ connection to exchange '{self._exchange_name}' established")

        # Consume messages
        channel.basic_consume(queue=queue_name, auto_ack=self._auto_ack, on_message_callback=self._message_received_callback)

    #endregion Connection establishment

    def start(self, message_received_callback, error_callback=None):
        '''Connects to RabbitMQ and starts listening for messages'''
        self._message_received_callback = message_received_callback
        self._error_callback = error_callback

        self._connection = self._connect()
        Thread(target=self._connection.ioloop.start).start()

    def stop(self):
        '''Stops listening for messages and closes the connection'''
        try:
            self._connection.ioloop.stop()
            self._connection.close()
            LOGGER.info("RabbitMQ connection closed")
        except pika.exceptions.ConnectionWrongStateError:
            LOGGER.warning("RabbitMQ connection already closed")
