import logging
import json
from abc import ABC, abstractmethod
import boto3
from botocore.exceptions import ClientError

from redis import Redis

log = logging.getLogger(__name__)


class AbstractBackend(ABC):
    @abstractmethod
    def push(self, queue_name, data):
        """Push a message onto the queue.

        :param data: The Python native object to be pushed onto the queue.
        """
        pass

    @abstractmethod
    def listen(self, queue_names):
        """Iterator over messages in the queue.

        :param queue_names: The names of the queues to listen on.

        :returns: A triple consisting of a message handle, the queue name and
                  the structured message (likely a Python dict).
        """
        pass

    @abstractmethod
    def sign_off(self, handle):
        """Notify the backend that message is dealt with appropriately.  The
        backend should ensure that the message is not picked up again.

        :param handle: The backend-specific message handle.  Should the first
            item in the triples generated by :meth:`listen`.
        """
        pass


class RedisBackend(AbstractBackend):
    def __init__(self, redis):
        self._redis = redis

    def push(self, queue_name, data):
        self._redis.rpush(queue_name, json.dumps(data))

    def listen(self, queue_names):
        while True:
            packed = self._redis.blpop(queue_names, 30)
            if not packed:
                continue
            data = json.loads(packed[1].decode("utf-8"))
            queue_name = packed[0].decode("utf-8")
            # Sign-off functionality for redis is a noop, hence a message
            # handle is not needed: hence None
            yield (None, queue_name, data)

    def sign_off(self, handle):
        pass

    @classmethod
    def from_config(cls, config):
        if "type" in config:
            config = config.copy()
            del config["type"]
        return cls(Redis(**config))

    def __repr__(self):
        return "<{}(redis={})>".format(self.__class__.__name__, self._redis)


class SqsBackend(AbstractBackend):
    """SQS backend. Currently only supports listening to a single queue."""

    # Set long polling delay to maximum duration (20s)
    _WAIT_TIME_SECONDS = 20

    def __init__(self, client):
        self._client = client
        log.info("Created backend %s", self)

    def _get_queue_url(self, queue_name):
        try:
            response = self._client.get_queue_url(QueueName=queue_name)
        except ClientError:
            self._client.create_queue(QueueName=queue_name)
        response = self._client.get_queue_url(QueueName=queue_name)
        return response.get("QueueUrl")

    def push(self, queue_name, data):
        queue_url = self._get_queue_url(queue_name)
        self._client.send_message(QueueUrl=queue_url, MessageBody=json.dumps(data))

    def listen(self, queue_names):
        queue_url = self._get_queue_url(queue_names[0])
        while True:
            messages = self._client.receive_message(
                QueueUrl=queue_url, WaitTimeSeconds=self._WAIT_TIME_SECONDS
            )
            if "Messages" not in messages:
                log.debug("Timeout waiting for messages")
                continue
            for mess in messages["Messages"]:
                data = json.loads(mess["Body"])
                handle = {"ReceiptHandle": mess["ReceiptHandle"], "QueueUrl": queue_url}
                yield (handle, queue_names[0], data)

    def sign_off(self, handle):
        self._client.delete_message(**handle)

    @classmethod
    def from_config(cls, _config):
        return cls(boto3.client("sqs"))

    def __repr__(self):
        return "<{}(client={})>".format(self.__class__.__name__, self._client)
