import socket

from amqplib.client_0_8 import basic_message

from amqpservices.base import log, config, AMQPBase, LOCALHOST_CONNECTION
from amqpservices.serializers.nop import NopSerializer


class _LazyResult(object):
    '''A lazy result. This class should not be instantiated by a user code.
    '''
    def __init__(self, client, correlation_id, timeout=None):
        self.client = client
        self.correlation_id = correlation_id
        self.timeout = timeout
    def get(self):
        '''Performs the reading of the result and returns it.
        '''
        if self.correlation_id in self.client.waiting_consumed:
            return self.client.waiting_consumed.pop(self.correlation_id)
        return self.client._consume(self.timeout, self.correlation_id, keep_notcorrelated=True)

class Client(AMQPBase):
    undefined_answer = object()
    def __init__(self, connparams=LOCALHOST_CONNECTION, serializer=None):
        self.init_connection_and_channel(connparams, connect_timeout=0.1)
        try:
            self.serializer = serializer or NopSerializer()

            self._correlation_id = 0
            self.waiting_consumed = {}
            self.error_state = False

            self._create_private_reply_queue()
            self._register_service_response_consumer()
        except:
            log.exception('Client.__init__ failed')
            config.sentry.captureException()
            raise

    def _gen_correlation_id(self):
        self._correlation_id += 1
        return str(self._correlation_id)

    def _create_private_reply_queue(self):
        self.reply_queue_name, _, _ = self.ch.queue_declare(exclusive=False)
        self.ch.queue_bind(exchange='amq.direct', queue=self.reply_queue_name, routing_key=self.reply_queue_name)
        log.info('Created reply queue %s' % self.reply_queue_name)

    def _register_service_response_consumer(self):
        def _response_callback(msg):
            log.debug('Got response <%s> <%s>', msg.properties, msg.body)
            self._last_response = msg
            msg.channel.basic_ack(msg.delivery_tag)
        self.ch.basic_consume(queue=self.reply_queue_name, callback=_response_callback)

    def _consume_response(self, timeout=None):
        log.debug('Waiting for response...')
        self._last_response = self.undefined_answer
        # We must be sure that we process a response, not some other AMQP message (eg. flow control msg)
        while self._last_response is self.undefined_answer:
            self._process_events(timeout)
        return self._last_response.properties.get('correlation_id'), self._last_response.body

    def send_request(self, routing_key, request, timeout=None, lazy=False):
        '''Sends a request message to an AMQP broker.

        :param routing_key: an AMQP routing key specifying destination. Queues defined in the config module have routing keys same as their names
        :param request: a body of the request method. In case a serializer is passed in the ``__init__`` it should match it's expected input
        :param timeout: timeout in seconds or None for no timeout. Throws :class:`socket.timeout` in case of timeout.
        :param lazy: returns a :class:`~amqpservices.client._LazyResult` which requires a ``get()`` call to perform actual reading of the result.
        '''

        want_reply = config.queues[routing_key]['want_reply']
        if lazy:
            assert want_reply
        if timeout is None:
            timeout = config.queues[routing_key].get('default_timeout')
        props = {
                'delivery_mode': 2 if config.queues[routing_key]['durable'] else 1,
        }
        if want_reply:
            props['reply_to'] = self.reply_queue_name
            props['correlation_id'] = self._gen_correlation_id()
        msg = basic_message.Message(body=self.serializer.to_bytes(request),
                **props)
        try:
            self.ch.basic_publish(msg, exchange='amq.direct', routing_key=routing_key)
            log.debug('Sent message %s to %s', props, routing_key)
            if lazy:
                return _LazyResult(self, props['correlation_id'])
            if want_reply:
                return self._consume(timeout, props['correlation_id'])
            return None
        except:
            self.error_state = True
            raise

    def _consume(self, timeout, correlation_id, keep_notcorrelated=False):
        while True:
            try:
                resp_correlation_id, res = self._consume_response(timeout=timeout)
            except socket.timeout:
                raise
            else:
                if correlation_id is not None and resp_correlation_id != correlation_id:
                    log.warn('Read message with not expected correlation_id')
                    if keep_notcorrelated:
                        self.waiting_consumed[resp_correlation_id] = res
                        log.warn('waiting_consumed=%s', self.waiting_consumed)
                    continue
                return self.serializer.from_bytes(res)

    def process_broker_events(self):
        '''Communicates with the AMQP broker and possibly processes events.
        It's good to call this method from time to time because a broker can
        asynchronously send events to clients (in case of a web application,
        this should be done after processing an HTTP request).
        '''
        try:
            self._process_events(0)
        except socket.error:
            pass
        else:
            msg = 'Processed AMQP events while process_broker_events()'
            log.warn(msg)
            config.sentry.captureMessage(msg)

    def reset(self):
        '''Resets state of a client by discarding possible responses waiting
        to be consumed and calling :func:`~amqpservices.client.Client.process_broker_events`.
        Raises :class:`~EnvironmentError` in case a connection to a broker is in invalid state.
        '''
        if self.waiting_consumed:
            self.waiting_consumed.clear()
        self.process_broker_events()
        if self.conn.channels is None:
            self.error_state = True
            raise EnvironmentError('Connection to a broker in invalid state')

