"""This module contains the models for the ``broker_models_hooks`` app"""

from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver

from mtp.apps.likes.models import Like
from mtp.apps.sync.models import Syncable

from .constants import ADD, CREATE, REMOVE, UPDATE
from .tasks import publish_to_broker


class BaseBrokerHook:
    """Base for the Broker hook mixin"""

    def should_be_sent_to_broker(self, event_type):  # pylint: disable=unused-argument, missing-param-doc
        """Check if the model should be sent to the broker

        Override this if some checks are needed to know if the model should be sent to the broker.

        Parameters
        ----------
        event_type: str

        Returns
        -------
        bool
            True if the serialized model should be sent to the broker,
            False otherwise. Default to True.

        """

        return True

    def get_serialized_representation(self):
        """Get the serialized representation of the current instance

        Must be implemented in sub classes.

        """

        raise NotImplementedError


class BrokerHookMixin(BaseBrokerHook):
    """Mixin providing a "hook" feature

    A django model who inherit from the ``BrokerHookMixin`` will publish its changes
    of state to the message broker, using the Django built-in signals.

    See Also
    --------
    ``receive_model_event`` signal receiver below.

    """

    def get_broker_topic(self):
        """Get the broker topic to post to when publishing the changes of this model

        Must be implemented in sub classes.

        """

        raise NotImplementedError


class BrokerHookM2MFromMixin:
    """Mixin only used to filter out the hooks coming from the ``BrokerHookM2MRelatedMixin``

    See Also
    --------
    ``receiver_m2m_changed`` signal receiver below.

    """

    pass


class BrokerHookM2MRelatedMixin(BaseBrokerHook):
    """Mixin who inherit from ``BaseBrokerHook``

    A django model who inherit from the ``BrokerHookM2MRelatedMixin`` will publish its changes
    of state to the message broker, using the Django built-in signals.

    See Also
    --------
    ``receiver_m2m_changed`` signal receiver below.

    """

    pass


@receiver(post_save)
def receive_model_event(sender, instance, created, raw, **kwargs):  # pylint: disable=unused-argument
    """Signal receiver for the ``BrokerHookMixin`` model.

    For the parameters, see ``django.db.models.signals.post_save``

    """

    # If the instance don't inherit from ``BrokerHookMixin``
    # Or if we are in the "raw" case do nothing
    if not isinstance(instance, BrokerHookMixin) or raw:
        return None

    event_type = CREATE if created else UPDATE

    # The instance is a ``Syncable`` object and is deleted, we fire a ``REMOVE`` event
    if isinstance(instance, Syncable) and instance.is_deleted:
        event_type = REMOVE

    _publish_to_broker(instance, event_type)


@receiver(post_save, sender=Like)
def receiver_model_event_like(sender, instance, created, raw, **kwargs):  # pylint: disable=unused-argument
    """Signal receiver for the ``Like`` model.

    For the parameters, see ``django.db.models.signals.post_save``.

    """

    liked_object = instance.liked_object

    # If the liked object is not a subclass of ``BrokerHookMixin``
    # Or if we are in in the "raw" case, do nothing
    if not isinstance(liked_object, BrokerHookMixin) or raw:
        return None

    _publish_to_broker(liked_object, UPDATE)


# Handled actions for the ``receiver_m2m_changed`` below
MTM_RECEIVER_ACTIONS = {
    'post_add': ADD,
    'post_remove': REMOVE
}


# pylint: disable=unused-argument
@receiver(m2m_changed)
def receiver_m2m_changed(sender, instance, action, reverse, model, pk_set, **kwargs):
    """Signal receiver for the changes on a "M2M" relation

    For the parameters, see ``django.db.models.signals.m2m_changed``.

    Notes
    -----
    - Since all our M2M relations don't use a through table, we can't filter the sender
    from the args. The filtering is done just below.
    - We don't handle the "post_clear" action type because we never reset our M2M relations
    from the API, and the hook system was made to track the changes made from the API.

    """

    broker_topic_method_base = 'get_%s_broker_topic'

    # Get the model that defines the m2m relation and the related model
    tracked_model = model if reverse else instance.__class__
    related_model = instance.__class__ if reverse else model

    # If the tracked model is not a subclass of ``BrokerHookM2MFromMixin`` OR
    # If the related model is not a subclass of ``BrokerHookM2MRelatedMixin`` OR
    # If the actions is not one of the "post_add" or "post_remove"
    if (not issubclass(tracked_model, BrokerHookM2MFromMixin) or
            not issubclass(related_model, BrokerHookM2MRelatedMixin) or
            action not in MTM_RECEIVER_ACTIONS):
        return

    # Load the tracked and the related instance(s)...
    if reverse:
        tracked_instances = tracked_model.objects.filter(pk__in=pk_set)
        related_instances = [instance]
    else:
        tracked_instances = [instance]
        related_instances = related_model.objects.filter(pk__in=pk_set)

    # ...And publish them on the broker
    for related_instance in related_instances:
        try:
            # Build and call the method to get the broker topic for this specific instance
            for tracked_instance in tracked_instances:
                broker_topic_name = getattr(
                    related_instance,
                    broker_topic_method_base % tracked_instance._meta.object_name.lower()
                )(tracked_instance)

                publish_to_broker.delay(
                    related_instance.get_serialized_representation(),
                    broker_topic_name,
                    MTM_RECEIVER_ACTIONS[action]
                )
        except AttributeError:
            # We fail silently because we don't want to block the save of one model
            # because there is a bad usage of this app. Better safe than sorry.
            pass


def _publish_to_broker(instance, event_type):  # pylint: disable=missing-param-doc,missing-type-doc
    """Publish a given event of the given instance to the message broker

    Parameters
    ----------
    instance: ``Model`` instance
        The model we want to publish on the broker.
    event_type: str
        One of the ``UPDATE`` or ``CREATE`` constants.

    """

    if instance.should_be_sent_to_broker(event_type):
        publish_to_broker.delay(
            instance.get_serialized_representation(),
            instance.get_broker_topic(),
            event_type
        )
