"""This module provides the ``DjangoBackedCeleryTask`` subclass of the celery ``Task``"""

from celery import Task

from django.utils.timezone import now

from .models import DjangoTask


class DjangoBackedCeleryTask(Task):
    """Celery Task linked with a django ``Task`` instance

    Attributes
    ----------
    django_task_model: type
        The django model to use to create an instance that will be tied to the current task
        This default to the ``DjangoTask`` model.

    Examples
    --------
    >>> @app.task(bind=True, base=DjangoBackedCeleryTask):
    ... def log_something(self)
    ...     # Add a log entry into the limpyd redis model of the tied django model
    ...     django_task = self.get_django_task()
    ...     django_task.logger.debug('Task for user %s started !', dango_task.user)
    ...     return self.user.id
    ...
    >>> log_something.delay()

    See Also
    --------
    ``mtp.apps.common.limpyd_base.DjangoBackedRedisModelWithLogs``

    Notes
    -----
    Since Celery shares the same ``Task`` object for all tasks of the same kind in a process,
    we cannot set any state on ``self``. So the django instance is created on the ``__call__``
    method, and the ``get_django_task`` method needs to be called to retrieve this instance later.

    More info: http://docs.celeryproject.org/en/latest/userguide/tasks.html#instantiation

    """

    abstract = True
    django_task_model = DjangoTask

    def create_django_task(self, task_id, user, **kwargs):
        """Create an instance of``django_task_model`` with the provided arguments

        Parameters
        ----------
        task_id: uuid
            The internal ID of the celery task
        user: mtp.apps.accounts.models.MTPUser
            The user marked as owner of the task
        kwargs: dict
            Additional arguments passed from ``__call__``
            Maybe live modified, and used as part of the return value

        Returns
        -------
        tuple(models.Model, dict)
            models.Model
                The instance of ``django_task_model`` created
            dict
                kwargs, maybe modified for internal purpose
        """

        # Do not try to create an instance if we don't have a task_id (that may happen in tests)
        if not task_id:
            return None, kwargs

        django_task, __ = self.django_task_model.objects.get_or_create(
            celery_task_id=task_id,
            user=user,
        )

        return django_task, kwargs

    def get_django_task(self, task_id=None):
        """Return the instance of ``django_task_model`` tied to the current celery task

        Parameters
        ----------
        task_id: uuid
            The internal ID of the celery task. If ``None``, it will be retrieved from
            ``self.request``

        Returns
        -------
        models.Model
            The instance of ``django_task_model`` created in ``create_django_task``, tied to the
            current task, retrieved by filtering on the task id.

        """

        if task_id is None:
            task_id = self.request.id
            if task_id is None and len(self.request_stack.stack) > 1:
                # In some cases, celery pushed the request in its stack, so we can retrieve
                # it from this stack (``-1`` is the current ``self.request``, and ``-2`` is the
                # previous one, pushed to ``-2`` in the default ``__call__`` method
                task_id = self.request_stack.stack[-2].id

        if task_id is None:
            raise self.django_task_model.DoesNotExist()

        return self.django_task_model.objects.get(celery_task_id=task_id)

    def __call__(self, user, *args, **kwargs):
        """Launch the ``django_task`` creation

        For the parameters, see ``celery.app.task.Task``"""

        # we might need to pop some arguments from kwargs before calling the "real" method
        __, kwargs = self.create_django_task(self.request.id, user, **kwargs)

        return super().__call__(*args, **kwargs)

    # pylint: disable=unused-argument
    def after_return(self, status, retval, task_id, args, kwargs, einfo):
        """Set the ``finished_at`` and ``state`` fields of the tied ``django_task`` instance

        For the parameters, see ``celery.app.task.Task``"""

        try:
            django_task = self.get_django_task(task_id)
        except self.django_task_model.DoesNotExist:
            # If no django task found, its creation wasn't done, for any reason, so we do nothing
            pass
        else:
            django_task.finished_at = now()
            django_task.state = django_task.TASK_STATES.for_constant(status).value
            django_task.save()

    # pylint: disable=unused-argument
    def on_failure(self, exc, task_id, args, kwargs, einfo):
        """Set the ``state`` field of the tied ``django_task`` instance to FAILURE

        For the parameters, see ``celery.app.task.Task``"""

        try:
            django_task = self.get_django_task(task_id)
        except self.django_task_model.DoesNotExist:
            # If no django task found, its creation wasn't done, for any reason, so we do nothing
            pass
        else:
            django_task.state = django_task.TASK_STATES.FAILURE
            django_task.save()

    # pylint: disable=unused-argument
    def on_success(self, retval, task_id, args, kwargs):
        """Set the ``state`` field of the tied ``django_task`` instance to SUCCESS

        For the parameters, see ``celery.app.task.Task``"""

        try:
            django_task = self.get_django_task(task_id)
        except self.django_task_model.DoesNotExist:
            # If no django task found, its creation wasn't done, for any reason, so we do nothing
            pass
        else:
            django_task.state = django_task.TASK_STATES.SUCCESS
            django_task.manage_result(retval)
            django_task.save()

    # pylint: disable=unused-argument
    def on_retry(self, exc, task_id, args, kwargs, einfo):
        """Set the ``state`` field of the tied ``django_task`` instance to RETRY

        For the parameters, see ``celery.app.task.Task``"""

        try:
            django_task = self.get_django_task(task_id)
        except self.django_task_model.DoesNotExist:
            # If no django task found, its creation wasn't done, for any reason, so we do nothing
            pass
        else:
            django_task.state = django_task.TASK_STATES.RETRY
            django_task.save()
