import pika
from threading import Thread
import network_constants as netconst

import logging
LOGGER = logging.getLogger(__name__)

class MessageManager:
    '''This Class is used for sending and receiving messages with RabbitMQ'''
    _rabbit_mq_ip = None
    _rabbit_mq_port = None

    _prepare_receive_parameters = None
    
    _connection: pika.SelectConnection = None
    _send_channel: pika.channel.Channel = None
    _receive_channel: pika.channel.Channel = None

    def __init__(self, rabbit_mq_ip=netconst.RABBIT_MQ_HOSTNAME, rabbit_mq_port=netconst.RABBIT_MQ_PORT):
        self._rabbit_mq_ip = rabbit_mq_ip
        self._rabbit_mq_port = rabbit_mq_port
     
    def connect(self, error_callback=None):
        '''Creates a connection with two channels'''
        self._error_callback = error_callback
        
        self._connection = self._connect_async()
        Thread(target=self._connection.ioloop.start).start()

    def _connect_async(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 channels
        connection.channel(on_open_callback=self._channel_created_callback)
        connection.channel(on_open_callback=self._channel_created_callback)

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

    def _connection_closed_callback(self, connection, error):
        LOGGER.warning(f"RabbitMQ connection closed: {str(error)}")
        if self._error_callback != None:
            self._error_callback("Connection closed")

    def _channel_created_callback(self, channel: pika.channel.Channel):
        # Assign both channels
        if self._send_channel == None:
            self._send_channel = channel
        else:
            self._receive_channel = channel
            LOGGER.info("RabbitMQ connection established")
          
    def create_exchange_with_queue(self, exchange_name, exchange_type, queue_name, callback):
        '''Creates exchange and queue and binds them'''
        self._prepare_receive_parameters = {'exchange_name': exchange_name,
                                    'exchange_type': exchange_type,
                                    'queue_name': queue_name,
                                    'callback': callback}
        
        self._receive_channel.exchange_declare(exchange=exchange_name, exchange_type=exchange_type, callback=self._exchange_created_callback)
        
    def _exchange_created_callback(self, answer_message):
        # Create queue
        queue_name = self._prepare_receive_parameters['queue_name']
        exclusive_access = (queue_name == '')
        self._receive_channel.queue_declare(queue=queue_name, exclusive=exclusive_access, callback=self._queue_created_callback, auto_delete=exclusive_access)

    def _queue_created_callback(self, answer_message: pika.frame.Method):
        queue_name = answer_message.method.queue
        exchange_name = self._prepare_receive_parameters['exchange_name']
        
        # Bind queue to exchange
        self._receive_channel.queue_bind(exchange=exchange_name, queue=queue_name, callback=self._queue_bound_callback)

    def _queue_bound_callback(self, _):
        if self._prepare_receive_parameters['callback'] != None:
            self._prepare_receive_parameters['callback']()

    def start_consuming(self, queue_name, auto_ack, message_received_callback):
        '''Starts listening for messages'''
        self._receive_channel.basic_consume(queue=queue_name, auto_ack=auto_ack, on_message_callback=message_received_callback)

    def create_exchange(self, exchange_name, exchange_type):
        '''Creates the exchange'''
        self._send_channel.exchange_declare(exchange=exchange_name, exchange_type=exchange_type)

    def send_message(self, exchange_name, routing_key, message):
        '''Sends a message to the exchange'''
        self._send_channel.basic_publish(exchange_name, routing_key, message)

    def disconnect(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")
