import contextlib
import os
import uuid

from proton import Disposition

import aorta.lib.timezone


class BaseBuffer:
    """Specifies the interface for all outbound message buffer
    implementations.

    All :class:`BaseBuffer` implementations hold the following
    assumptions:

    -   AMQP links are named correctly. This means that if the link,
        for whatever reason, gets interrupted, it can be re-established
        and the deliveries resumed. It also allows the :class:`BaseBuffer`
        implementations to distinguish deliveries from different links.
    -   The preceding item implies that containers must also be properly
        named, as the AMQP 1.0 specification states in section 2.6.1:
        *"A link’s name uniquely identifies the link from the container
        of the source to the container of the target node, e.g. if the
        container of the source node is A, and the container of the target
        node is B, the link may be globally identified by the (ordered)
        tuple (A,B,<name>)."*
    -   :class:`BaseBuffer` implementations assume that container ids
        a globally unique in the AMQP network.
    -   :class:`BaseBuffer` also assumes that the ``delivery-tag`` on
        message transfers is **globally** unique. This deviates from the
        AMQP 1.0 specification, section 2.6.12, which only states *"Each
        delivery MUST be identified by a delivery-tag chosen by the sending
        application. The delivery-tag MUST be unique amongst all deliveries
        that could be considered unsettled by either end of the Link."*.
        This decision was made to prevent collisions between the delivery
        tags that were auto-generated by the :mod:`proton` library, on
        different links in different processes, but using the same data
        store.
    """
    initial_backoff = 5000

    def backoff(self, n):
        """Calculate the delay in microsecond how long a message must be
        held before being retransmitted.
        """
        return int(self.initial_backoff * (1.25**n))

    def generate_tag(self):
        """Generates a globally unique delivery tag."""
        return uuid.UUID(bytes=os.urandom(16)).hex

    def now(self):
        """Return an aware :class:`int` instance representing the
        current date and time in milliseconds since the UNIX epoch.
        """
        return aorta.lib.timezone.now()

    def delay(self, delay=None):
        """Return a tuple containing two integers representing the
        queued-at time and no-transmission-before time in milliseconds
        since the UNIX epoch.
        """
        assert (delay or 0) >= 0,\
            "`delay` must be a positive integer"
        qat = nbf = self.now()
        if delay:
            nbf += delay
        return qat, nbf

    def put(self, message, delay=None, on_settled=None):
        """Place a new message on the message queue."""
        retval = self.enqueue(message, *self.delay(delay))
        if callable(on_settled):
            on_settled(message)
        assert message.id is not None,\
            "Egress messages must have an id."
        return retval

    def get(self, tag):
        """Return a :class:`proton.Message` instance by its
        delivery tag.
        """
        raise NotImplementedError

    def enqueue(self, message, qat, nbf):
        """Queue a new message for transmission.

        Args:
            message (proton.Message): the message to enqueue.
            qat (int): the date and time at which
                the message was queued.
            nbf (int): the date and time before which
                the message may not be transmitted.

        Returns:
            None
        """
        raise NotImplementedError

    def pop(self):
        """Return the next :class:`proton.Message` instance
        that is queued for transmission. This may delete the
        message data from the persistent storage medium.
        """
        raise NotImplementedError

    def transfer(self, host, source, target, sender, channel=None):
        """Transmit the next message in the queue to the AMQP remote
        peer. Return the delivery tag.

        Args:
            host (str): a string in the format ``host:port`` identifying
                the remote AMQP peer.
            source (str): the name of the source (local) container.
            target (str): the name of the target (remote) container.
            sender (proton.Sender): the link over which the message will
                be sent.
            channel (str): specifies the remote address.

        Returns:
            str
        """
        if not sender.credit:
            return None

        addr, port = host.split(':')
        assert port.isdigit(), "Invalid host: %s" % host
        with self.transaction():
            message = self.pop()
            if message is None:
                return

            if channel is not None:
                message.address = channel

            tag = self.generate_tag()
            delivery = sender.send(message, tag)

            assert delivery.tag == tag
            self.track(host, port, source, target, sender.name,
                delivery.tag, message)

        return tag

    def track(self, host, port, source, target, link, tag, message):
        """Track the delivery of `message` to the AMQP remote peer.

        Args:


        Returns:
            None
        """
        raise NotImplementedError

    @contextlib.contextmanager
    def transaction(self):
        """Start a transaction. The default implementation does nothing;
        :class:`BaseBuffer` implementations should define their own
        transactional behavior. It is assumed that the transaction
        begins when the context manager yields, commits when the context
        is exited, and rollbacks when an exception occurs.
        """
        yield

    def on_settled(self, delivery, remote_state, disposition):
        """Invoked when the AMQP peers agree on the state of a transfer.

        A transfer has three possible final outcomes (terminal delivery
        states):

        -   ``ACCEPTED``
        -   ``REJECTED``
        -   ``RELEASED``
        -   ``MODIFIED``

        The ``ACCEPTED`` outcome means that the remote has accepted
        the message and fullfills the conditions specified in the
        message headers e.g. durability.

        The ``REJECTED`` outcome means that the remote does not wish to handle
        the AMQP message because it is invalid and/or unprocessable.

        The ``RELEASED`` outcome indicates that the message was
        not (and will not be) processed.

        The ``MODIFIED`` outcome indicates that the message was modified,
        but not processed.

        For more information on terminal delivery states, consult the
        documentation on their respective methods or the AMQP 1.0
        specification, section 3.4.

        Args:
            delivery (proton.Delivery): represents the ongoing delivery.
            remote_state (proton.DispositionType): specifies the
                final outcome of the message transfer.
            disposition (proton.Disposition): the message disposition
                describing the remote outcome.

        Returns:
            None
        """
        if remote_state == Disposition.ACCEPTED:
            self.on_accepted(delivery, self.get(delivery.tag),
                disposition=disposition)
        if remote_state == Disposition.REJECTED:
            self.on_rejected(delivery, self.get(delivery.tag),
                disposition=disposition)
        if remote_state == Disposition.RELEASED:
            self.on_released(delivery, self.get(delivery.tag),
                disposition=disposition)
        if remote_state == Disposition.MODIFIED:
            self.on_modified(delivery, self.get(delivery.tag),
                disposition=disposition)

    def on_accepted(self, delivery, message, disposition):
        """Invoked when the remote has indicated that it accepts the message.

        Args:
            delivery (proton.Delivery): describes the final state of the
                message transfer.
            message (proton.Message): the AMQP message.
            disposition (proton.Disposition): the message disposition
                describing the remote outcome

        Returns:
            None
        """
        raise NotImplementedError

    def on_rejected(self, delivery, message, disposition):
        """Invoked when the remote has terminated the transfer with the
        ``REJECTED`` outcome.

        The rejected outcome is described in the AMQP 1.0 specification,
        section 3.4.3: *"At the target, the rejected outcome is used to
        indicate that an incoming message is invalid and therefore
        unprocessable. The rejected outcome when applied to a message
        will cause the delivery-count to be incremented in the header of
        the rejected message."*

        This outcome indicates that the message was semantically invalid
        or otherwise unprocessable by the remote.

        Args:
            delivery (proton.Delivery): describes the final state of the
                message transfer.
            message (proton.Message): the AMQP message.
            disposition (proton.Disposition): the message disposition
                describing the remote outcome.

        Returns:
            None
        """
        message.delivery_count += 1
        self.error(delivery.tag, message)

    def on_released(self, delivery, message, disposition):
        """Invoked when the remote decides to release a message. Requeue
        message transmission with a delay of :attr:`retransmission_delay`
        seconds.

        Section 3.4.4 of the AMQP 1.0 specification states about the
        ``RELEASED`` outcome: *"At the source the released outcome means
        that the message is no longer acquired by the receiver, and has
        been made available for (re-)delivery to the same or other targets
        receiving from the node. The message is unchanged at the node (i.e.,
        the delivery-count of the header of the released message MUST NOT be
        incremented). As released is a terminal outcome, transfer of payload
        data will not be able to be resumed if the link becomes suspended. A
        delivery can become released at the source even before all transfer
        frames have been sent. This does not imply that the remaining
        transfers for the delivery will not be sent. The source MAY
        spontaneously attain the released outcome for a message (for example
        the source might implement some sort of time-bound acquisition lock,
        after which the acquisition of a message at a node is revoked to
        allow for delivery to an alternative consumer)."*

        Args:
            delivery (proton.Delivery): describes the final state of the
                message transfer.
            message (proton.Message): the AMQP message.
            disposition (proton.Disposition): the message disposition
                describing the remote outcome.

        Returns:
            None
        """
        self.put(message, delay=self.backoff(message.delivery_count))

    def on_modified(self, delivery, message, disposition):
        """Invoked when the remote modified the message.

        Section 3.4.5. of the AMQP 1.0 specification describes the
        ``MODIFIED`` outcome: *"At the target, the modified outcome is
        used to indicate that a given transfer was not and will not be
        acted upon, and that the message SHOULD be modified in the specified
        ways at the node."

        Args:
            delivery (proton.Delivery): describes the final state of the
                message transfer.
            message (proton.Message): the AMQP message.
            disposition (proton.Disposition): the message disposition
                describing the remote outcome.

        Returns:
            None
        """
        message.delivery_count += 1
        if disposition.undeliverable:
            self.error(delivery.tag, message, undeliverable=True)
            return

        self.put(message, delay=self.backoff(message.delivery_count))

    def error(self, tag, message, undeliverable=False):
        """Invoked when a message could not be delivered.

        Args:
            tag (str): identifies the delivey.
            message (proton.Message): the message that errored.
            undeliverable (bool): indicates if the message was
                undeliverable. If `undeliverable` is ``False``,
                the message was rejected by the remote, it this
                parameter is ``True``, it could not be delivered
                to the remote.

        Returns:
            None
        """
        raise NotImplementedError

    def __len__(self):
        raise NotImplementedError
