from django.db import models
from django.db.models.signals import m2m_changed

from .models import Syncable


# pylint: disable=unused-argument
def m2m_changed_syncable(sender, instance, action, reverse, model, pk_set, **kwargs):
    """Receiver for the ``m2m_changed`` signal to update syncable linked instances.

    We only check for some* actions, and only if the model declaring the m2m is a subclass of
    ``Syncable``, and in this case we save the related instances (of the model delcaring the m2m)
    to update the ``last_update`` field.

    * Tracked actions:
        - pre_clear : "pre" is the only moment we can know the touched instances
        - pre_remove : idem
        - post_add : we can wait for the operation to be done to know the added instances

    Parameters
    ----------
    sender : through instance
        Instance of the ``through`` model of the m2m relation (automatic or manual)
    action : str
        One of ``pre_add``, ``pre_remove``, ``pre_clear``, ``post_add``, ``post_remove``,
        ``post_clear``
    reverse : bool
        If the m2m is updated from an instance of the model declaring the m2m field (``reverse``
        is ``False``) or from an instance of the model on the other side (``reverse`` is ``False``)

        ``instance``, ``model`` and ``pk_set`` arguments depends on the value of ``reverse``:

        If ``reverse`` is ``False``:
          - ``instance`` is an instance of the model declaring the m2m field
          - ``model`` is the model on the other side of the m2m relation
          - ``pk_set`` are PKs of the model defined in ``model``.
        If ``reverse`` is ``True``:
          - ``instance`` is an instance of the other side of the relation
          - ``model`` is the model declaring the m2m relation
          - ``pk_set`` are PKs of the model defined in ``model``

    Raises
    ------
    self.LockedFieldException
        The exception class defined in the ``LockedFieldException`` attribute.
        If the m2m field on the related versionable instance is not editable.

    """

    # Stop if the m2m is already a syncable through.
    through_model = sender.__class__
    if isinstance(through_model, Syncable):
        return

    # Stop if not the action we track
    if action not in {'pre_clear', 'pre_remove', 'post_add'}:
        return

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

    # We only care about ``Syncable`` models
    if not issubclass(tracked_model, Syncable):
        return

    # Get the related field
    related_field = [field for field in tracked_model._meta.get_fields()
                     if isinstance(field, models.ManyToManyField)
                     and field.related_model == related_model][0]

    # Get all the updated instances
    if reverse:
        filters = {}
        if action == 'pre_clear':
            # Removed PKs are not given when clearing the relation, so we need to get the
            # ones that have ``instance`` (ie the related entry, at the other side of the m2m
            # relation)
            filters[related_field.name] = instance
        else:
            filters['pk__in'] = pk_set
        instances = tracked_model.objects.filter(**filters)
    else:
        instances = [instance]

    # For each instance, save the model to re-active the sync
    for instance in instances:
        instance.save()


# Connect the ``m2m_changed_lockable`` method to the ``m2m_signal`` without specifying the
# ``sender`` argument, as we'll do all the check in the function itself.
m2m_changed.connect(m2m_changed_syncable, weak=False, dispatch_uid='m2m_changed_syncable')
