import datetime
import os
import os.path
import socket
import time

import amqplib.client_0_8 as amqp

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

try:
    import mmstats

    class AMQPServicesStats(mmstats.MmStats):
        worker_id = mmstats.StringField(label='amqpservices.worker_id')
        processed = mmstats.CounterField(label='amqpservices.processed')
        exceptions = mmstats.CounterField(label='amqpservices.exceptions')
        last_before_callback = mmstats.DoubleField(label='amqpservices.last_before_callback')
        last_after_callback = mmstats.DoubleField(label='amqpservices.last_after_callback')
        state = mmstats.StringField(label='amqpservices.state')
        totaltime_callback = mmstats.DoubleField(label='amqpservices.totaltime_callback')
        avg_callback = mmstats.AverageField(label='amqpservices.avg_callback')

    USE_MMSTATS = True
    log.info('Using mmstats')

except ImportError:
    USE_MMSTATS = False
    log.info('Not using mmstats - no such module')


def worker_id(service):
    process_created = datetime.datetime.fromtimestamp(os.path.getmtime('/proc/self'))
    return '{}-{}-{}-{}'.format(
            service,
            socket.gethostname(),
            process_created.strftime('%y%m%d.%H%M%S'),
            os.getpid())

class Server(AMQPBase):
    '''
    :param service: name of a service from the config module
    :param callback: a callable supporting a call ``callback(props, body)``, where ``props`` is a dictionary of AMQP message properties and ``body`` is a message body - either a raw string or a rich type produced by a serializer's ``from_bytes`` method.
    :param serializer: an object implementing `~amqpservices.serializers.base.Serializer` methods used for de-serializing requests and serializing responses from the ``callback``
    :param connparams: parameters for a connection to a AMQP broker

    '''
    def __init__(self, service, callback, serializer=None,
            connparams=LOCALHOST_CONNECTION):
        self.init_connection_and_channel(connparams, connect_timeout=5)
        try:
            self.callback = callback
            self.service = service
            self.serializer = serializer or NopSerializer()

            self.worker_id = worker_id(service)

            self.stats = None
            if USE_MMSTATS:
                path = getattr(config.module, 'MMSTATS_BASE_DIR', '')
                if path:
                    self.stats = AMQPServicesStats(path=path, filename=self.worker_id + '.stats')
                    self.stats.worker_id = self.worker_id
                else:
                    log.warn('Empty or unset MMSTATS_BASE_DIR config variable, stats not available')

            #for sig in signal.SIGHUP, signal.SIGUSR1, signal.SIGUSR2:
            #    signal.signal(sig, self._signal_catched)

            log.info('Starting setup of worker pid=%d name=%s', os.getpid(), self.worker_id)
            for queue_name, queue_config in config.module.SERVICES[self.service]['queues'].items():
                self._create_public_queue(queue_name, queue_config)
                self._register_service_request_consumer(queue_name, self.callback)
            self._create_bc_queue()
            self._register_service_request_consumer(self.bc_queue_name, self.callback)
        except:
            log.exception('Server.__init__ failed')
            config.sentry.captureException()
            raise

    def _create_public_queue(self, queue_name, queue_config):
        arguments = {}
        if queue_config.get('x-message-ttl') is not None:
            arguments['x-message-ttl'] = queue_config['x-message-ttl']
        self.ch.queue_declare(queue=queue_name,
                durable=queue_config['durable'],
                auto_delete=queue_config.get('auto_delete', False),
                arguments=arguments,
                )
        # Routing keys are the same as queue names
        self.ch.queue_bind(exchange='amq.direct', queue=queue_name, routing_key=queue_name)
        log.info('Created public service queue %s' % queue_name)

    def _bc_routing_key(self):
        return 'broadcast.' + self.service

    def _create_bc_queue(self):
        self.bc_queue_name, _, _ = self.ch.queue_declare(exclusive=False)
        self.ch.queue_bind(exchange='amq.direct', queue=self.bc_queue_name, routing_key=self._bc_routing_key())

    def _register_service_request_consumer(self, queue_name, callback):
        def _request_callback(msg):
            log.info('Got msg')
            log.debug('Msg props, body: <%s> <%s>', msg.properties, msg.body[:1000])
            if self.stats:
                self.stats.state = 'callback'
                self.stats.last_before_callback = time.time()
            before_callback = time.time()
            error_condition = False
            try:
                body_rich = self.serializer.from_bytes(msg.body)
                ret_rich = callback(msg.properties, body_rich)
                ret = self.serializer.to_bytes(ret_rich)
                assert ret is None or isinstance(ret, str), \
                    'Serializer must serialize callback\'s response to None or str'
            except:
                log.exception('Exception thrown by the callback')
                config.sentry.captureException()
                if self.stats:
                    self.stats.exceptions.inc()
                # Reject the message. We are sleeping 1 second here to avoid
                # high CPU/disk usage caused by constant ping-pong between
                # our process that rejects the message and RabbitMQ which
                # redelivers the message to us. A small cost is that we
                # block this consumer for a second.
                log.warn('Sleeping before rejection')
                time.sleep(1)
                msg.channel.basic_reject(msg.delivery_tag, True)
                log.warn('Rejected')
                error_condition = True
            else:
                if self.stats:
                    self.stats.processed.inc()
                log.debug('Response callback finished with success')

            after_callback = time.time()
            spent_callback = after_callback - before_callback
            if self.stats:
                self.stats.totaltime_callback += spent_callback
                self.stats.avg_callback.add(spent_callback)
                self.stats.last_after_callback = time.time()

            if not error_condition and 'reply_to' in msg.properties:
                if self.stats:
                    self.stats.state = 'response'
                body = ret if ret is not None else '<None>'
                msg.channel.basic_publish(amqp.basic_message.Message(
                    body=body,
                    correlation_id=msg.properties['correlation_id']),
                    exchange='amq.direct',
                    routing_key=msg.properties['reply_to'])
                log.info('Processed msg')
                log.debug('Sent reply with body="%s" to %s', body, msg.properties['reply_to'])
            if not error_condition:
                if self.stats:
                    self.stats.state = 'ack'
                msg.channel.basic_ack(msg.delivery_tag)
            if self.stats:
                self.stats.state = 'wait'
        self.ch.basic_consume(queue=queue_name, callback=_request_callback)

    def loop(self):
        '''Infinite server loop: reads client's requests and sends responses
        using a callback passed in the ``__init__`` method.
        '''

        try:
            while self.ch.callbacks:
                self._process_events(timeout=None)
        except:
            log.exception('')
            log.critical('Unhandled exception from loop()')
            config.sentry.captureException()
            raise

    def _signal_catched(self, signo, frame):
        log.warn('Not handling signal %s', signo)

