import base64
import json
import sys
import os
from datetime import date, datetime
from pytz import timezone

import boto3
import botocore

from .base import Connector
from ..exceptions import QueueDoesNotExist

import logging
logger = logging.getLogger('sqjobs.sqs')

is_pypy = '__pypy__' in sys.builtin_module_names


class SQS(Connector):
    """
    Manages a single connection to SQS
    """

    def __init__(self, access_key, secret_key, region_name='us-east-1', endpoint_url=None):
        """
        Creates a new SQS object

        :param access_key: access key with write access to AWS SQS
        :param secret_key: secret key with write access to AWS SQS
        :param region_name: a region name, like 'us-east-1'
        :param endpoint_url: URL to use a custom region
        """
        self.access_key = access_key
        self.secret_key = secret_key
        self.region_name = region_name
        self.endpoint_url = endpoint_url

        self._cached_connection = None

    def __repr__(self):
        return 'SQS("{ak}", "{sk}", region_name="{region_name}")'.format(
            ak=self.access_key,
            sk="%s******%s" % (self.secret_key[0:6], self.secret_key[-4:]),
            region_name=self.region_name,
        )

    @property
    def connection(self):
        """
        Creates (and saves in a cache) a connection to SQS
        """
        if self._cached_connection is None:
            config = {
                'service_name': 'sqs',
                'region_name': self.region_name,
                'aws_access_key_id': self.access_key,
                'aws_secret_access_key': self.secret_key,
            }

            if self.endpoint_url:
                config['endpoint_url'] = self.endpoint_url

            self._cached_connection = boto3.resource(**config)
            logger.debug('Created a new connection to SQS')

        return self._cached_connection

    def enqueue(self, queue_name, payload):
        message = SQSMessage.encode(payload)
        queue = self._get_queue(queue_name)

        if not queue:
            raise QueueDoesNotExist('The queue %s does not exist' % queue_name)

        queue.send_message(MessageBody=message)
        logger.info('Sent new message to %s', queue_name)

    def dequeue(self, queue_name, wait_time=20):
        queue = self._get_queue(queue_name)
        messages = None

        if not queue:
            raise QueueDoesNotExist('The queue %s does not exist' % queue_name)

        while not messages:
            messages = queue.receive_messages(
                MaxNumberOfMessages=1,
                WaitTimeSeconds=wait_time,
                AttributeNames=['All'],
            )

            if not messages:
                logger.debug('No message retrieved from %s', queue_name)

                if wait_time == 0:
                    return None  # Non-blocking mode

        logger.info('New message retrieved from %s', queue_name)
        payload = SQSMessage.decode(messages[0], queue_name)

        return payload

    def delete(self, queue_name, message_id):
        queue = self._get_queue(queue_name)

        if not queue:
            raise QueueDoesNotExist('The queue %s does not exist' % queue_name)

        queue.delete_messages(Entries=[{
            'Id': '1',
            'ReceiptHandle': message_id
        }])

        logger.info('Deleted message from queue %s', queue_name)

    def set_retry_time(self, queue_name, message_id, delay):
        queue = self._get_queue(queue_name)

        if not queue:
            raise QueueDoesNotExist('The queue %s does not exist' % queue_name)

        queue.change_message_visibility_batch(Entries=[{
            'Id': '1',
            'ReceiptHandle': message_id,
            'VisibilityTimeout': delay or 0
        }])

        logger.info('Changed retry time of a message from queue %s', queue_name)

    def serialize_job(self, job_name, job_id, args, kwargs):
        return {
            'id': job_id,
            'name': job_name,
            'args': args,
            'kwargs': kwargs
        }

    def unserialize_job(self, job_class, payload):
        job = job_class()

        job.id = payload['id']
        job.queue_name = payload['_metadata']['queue_name']
        job.broker_id = payload['_metadata']['id']
        job.retries = payload['_metadata']['retries']
        job.created_on = payload['_metadata']['created_on']
        args = payload['args'] or []
        kwargs = payload['kwargs'] or {}

        return job, args, kwargs

    def _get_queue(self, name):
        try:
            environment = os.environ.get('ENVIRONMENT', False)
            aws_queues_enabled = os.environ.get('AWS_SAM_QUEUES', False)
            if environment and aws_queues_enabled:
                # running in the AWS environments
                name = f'{environment}-{name}'
            return self.connection.get_queue_by_name(QueueName=name)
        except botocore.exceptions.ClientError:
            return None


class SQSMessage(object):

    @staticmethod
    def encode(payload):
        payload_str = json.dumps(payload, default=SQSMessage.json_formatter)
        payload_encoded = base64.b64encode(payload_str.encode('utf-8'))
        return payload_encoded.decode('utf-8')

    @staticmethod
    def decode(message, queue_name):
        body = message.body

        if is_pypy:
            body = body.encode("utf-8")

        payload_decoded = base64.b64decode(body)
        payload = json.loads(payload_decoded.decode("utf-8"))

        retries = int(message.attributes['ApproximateReceiveCount'])
        created_on = int(message.attributes['SentTimestamp'])

        payload['_metadata'] = {
            'id': message.receipt_handle,
            'retries': retries,
            'created_on': datetime.fromtimestamp(created_on / 1000, tz=timezone('UTC')),
            'queue_name': queue_name
        }

        logging.debug('Message payload: %s', str(payload))

        return payload

    @staticmethod
    def json_formatter(obj):
        if isinstance(obj, datetime):
            return obj.strftime('%Y-%m-%d %H:%M:%S')
        elif isinstance(obj, date):
            return obj.strftime('%Y-%m-%d')

        return None
