"""This module provides the ``TrainingActivity`` model for the ``trainings`` app"""

from django.conf import settings
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from mtp.discrete_apps.versionable.models import (
    Dependency,
    PolymorphicModelForDependency
)

from mtp.apps.assessments.models import AssessableActivity
from mtp.apps.broker_models_hooks.constants import CREATE
from mtp.apps.broker_models_hooks.models import BrokerHookMixin
from mtp.apps.common.models import NonAbstractChildOrderable
from mtp.apps.common.utils import (
    bypass_orderable,
    classproperty,
    get_concrete_models,
    sort_list_of_dicts
)
from mtp.apps.sync.models import Syncable, SyncableForPolymorphicMixin

from .. import constants
from ..managers import TrainingActivityManager


class ResultsToExcelActivityMixin:
    """Mixin to export the results of a given ``TrainingActivity`` to an excel file"""

    def is_exportable(self, session):
        """Test that this current activity results are exportable to excel

        Parameters
        ----------
        session: TrainingSession
            The session we want to export the results for.

        Returns
        -------
        bool
            True if this activity is exportable, False otherwise

        Notes
        -----
        Must be implemented in subclasses

        """

        raise NotImplementedError

    def results_to_excel(self, session, path):  # pylint: disable=unused-argument
        """Export the session results of this activity to excel

        Parameters
        ----------
        session: TrainingSession
            The session we want to export the results for.
        path: str or None
            The full path to save the created excel file
            If None, the file will not be written on disk but outputted as a ``io.BytesIO``.

        Returns
        -------
        str or io.BytesIO
            ``str`` The path of the created excel file
                If the ``path`` option was a str
            ``io.BytesIO`` The raw data of the excel file
                If the ``path`` option was None

        Notes
        -----
        Must be implemented in subclasses

        """

        raise NotImplementedError


class LiveActivityMixin(models.Model):
    """Give the possibility to transform an activity into a live activity.

    Live activities can be created on the fly, directly in a session after its creation.
    Theses live activities will be only available in the session they were created into.

    """

    session = models.ForeignKey(
        'trainings.TrainingSession',
        blank=True,
        null=True,
        verbose_name=_('session'),
        related_name='activities',
        on_delete=models.CASCADE
    )

    class Meta:
        abstract = True

    @property
    def is_live(self):
        """Check if the activity has been created on the fly.

        Notes
        -----
        If an activity has an assigned session, this activity has been created on the fly.
        So it's a live activity.

        Returns
        -------
        bool
            True if the activity is a live activity. Else False.

        """

        return bool(self.session_id)

    def save(self, *args, **kwargs):
        """If session is set,
        we raise an error if activity is :
        - linked to training and to a session at the same time
        - not linked to a training neither to a session

        For the parameters, see ``models.Model``

        """

        if self.session and self.training:
            raise TypeError(
                'The activity can not be linked to a training and to a session at the same time'
            )

        if not (self.session or self.training or self.library_status):
            raise TypeError(
                'The activity has to be linked to a training, to a session or to a library status'
            )

        super().save(*args, **kwargs)


class TrainingActivity(LiveActivityMixin,
                       AssessableActivity,
                       BrokerHookMixin,
                       Dependency,
                       SyncableForPolymorphicMixin,
                       Syncable,
                       NonAbstractChildOrderable,
                       PolymorphicModelForDependency):
    """Base (non-abstract) Django model representing a training activity.

    Attributes
    ----------
    training : django ``ForeignKey`` field
        The training the activity belong to.
    name : django ``CharField``
        The name of the activity, mandatory.
    description : django ``TextField``
        Optional description for this activity.

    Notes
    -----
    This model is a ``Dependency`` of the ``Training``.

    """

    library_status = models.OneToOneField(
        'trainings.TrainingActivityLibraryStatus',
        related_name='training_activity',
        verbose_name=_('Library status'),
        null=True,
        on_delete=models.CASCADE
    )

    training = models.ForeignKey(
        'trainings.Training',
        verbose_name=_("training"),
        related_name='activities',
        blank=True,
        null=True,
        on_delete=models.CASCADE
    )

    name = models.CharField(max_length=255, verbose_name=_("name"))
    description = models.TextField(
        null=True,
        blank=True,
        verbose_name=_('Description')
    )

    needs_approval = models.BooleanField(
        default=False,
        verbose_name=_("needs trainer approval"),
        help_text=_("Does the activity needs a trainer approval to be started?")
    )

    non_lockable_fields = Dependency.non_lockable_fields + Syncable.non_lockable_fields

    objects = TrainingActivityManager()

    class Meta:
        unique_together = ('sort_order', 'training', 'session', 'library_status', )

    @classproperty
    def versionable_link(cls):  # pylint: disable=no-self-argument
        """Defines the link the the ``Versionable`` object"""

        from . import Training
        return Training, 'training'

    def __str__(self):
        return self.name

    @cached_property
    def activity_type(self):
        """Return the activity type

        Returns
        -------
        str

        """

        return self.__class__.__name__.lower()

    @classmethod
    def available_activity_models(cls):
        """Get the available activities models

        We exclude the deactivated activities from the list of ``TrainingActivity`` subclasses.

        Returns
        -------
        list(models.Model)
            A list of available ``TrainingActivity`` subclasses

        """

        from mtp.apps.brainstormers.models import BrainstormerTemplate
        from mtp.apps.documents.models import Document
        from mtp.apps.roleplays.models import Roleplay
        from mtp.apps.workshops.models import Workshop

        deactivated_activities = [Document, Roleplay, Workshop, BrainstormerTemplate]

        return [
            cls for cls in get_concrete_models(cls)
            if cls not in deactivated_activities
        ]

    @classmethod
    def available_activity_types(cls, training=None):
        """Get the available activities type and their related create urls

        The activity are ordered by their ``verbose_name``.

        Parameters
        ----------
        training: Training
            If provided, the create urls will related to this training,
            If not provided, the create urls will be related to the library.

        Returns
        -------
        list of dict
            A list of dict with three entries:
            - create_url: The url to create the activity type
            - verbose_name: The verbose name of the activity type
            - type: The activity type
            - import_url: The url to create a the activity type from a file

        """

        activity_types = []

        for activity_cls in cls.available_activity_models():

            if training:
                create_url = activity_cls.get_training_create_url(training=training)
            else:
                create_url = activity_cls.get_create_url()

            activity_type = {
                'create_url': create_url,
                'verbose_name': activity_cls.verbose_activity_name,
                'type': activity_cls.__name__.lower(),
            }

            activity_types.append(activity_type)

        return sort_list_of_dicts(activity_types, 'verbose_name')

    def get_broker_topic(self):
        """Get the broker topic to publish the change related to this activity

        Returns
        -------
        str

        """

        return '%s/activities' % self.session_id

    def should_be_sent_to_broker(self, event_type):
        """Check if the activity updates should be sent to the broker

        The activity should be sent to the broker if:
        - Its a brand new activity (the event_type should be ``CREATE``)
        - Its associated with a training_session
        - The training_session is ``OPEN``

        For the other parameters, see ``BaseBrokerHook.should_be_sent_to_broker``.

        """

        return self.session_id and event_type == CREATE and self.session.is_live

    def get_serialized_representation(self):
        """Get the serialized representation of this activity

        Returns
        -------
        dict

        """

        from ..apiv2.serializers import TrainingActivitySerializer

        return TrainingActivitySerializer(self).data

    def duplicate(self, **kwargs):
        """Override the ``duplicate`` from the ``Dependency``.

        We don't want the training to be duplicated when we duplicate an activity.

        For the parameters, see ``mtp.discrete_apps.duplicable.models.Duplicable``

        See also
        --------
        mtp/discrete_apps/duplicable/README.rst

        """

        with bypass_orderable():
            return super().duplicate(
                non_duplicable_relations={'training'},
                **kwargs
            )

    def duplicate_in_training(self, training, name=None):
        """Duplicate this activity in the targeted training

        Parameters
        ----------
        training: Training
            The training we want to duplicate this activity into
        name: str
            The name of the duplicate

        """

        if not name:
            name = self.name

        # Since the duplicable bypass the orderable stuff, we need to set the the values ourselves
        sort_order = training.activities.aggregate(
            models.Max('sort_order')
        )['sort_order__max'] or 0  # If we don't have any activities yet in this training

        return self.duplicate(
            name=name,
            training=training,
            library_status=None,
            sort_order=sort_order + 1
        )

    def duplicate_in_library(self, owner, is_shared=False, name=None):
        """Duplicate this activity in the given library

        Parameters
        ----------
        owner: MTPUser
            The owner of the newly created activity
        is_shared: bool
            True if this activity should be added to the "public library"
        name: str
            The name of the duplicate

        """

        # Create the ``TrainingActivityLibraryStatus`` who should be associated
        # with the newly created activity
        library_status = TrainingActivityLibraryStatus.objects.create(
            owner=owner,
            is_shared=is_shared
        )

        if not name:
            name = self.name

        # Duplicate this activity, detaching it from the current training if any,
        # and setting the library status created above
        return self.duplicate(name=name, training=None, library_status=library_status)

    def get_training_duplicate_url(self):
        """Get the url to duplicate the activity inside her current training

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('trainings:training-activity-duplicate', kwargs={
            'activity_uuid': self.pk, 'training_uuid': self.training.pk})

    def get_trainings_duplicate_url(self):
        """Get the url to duplicate the activity inside multiple trainings

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('trainings:trainings-activity-duplicate', kwargs={'activity_uuid': self.pk})

    def get_private_library_duplicate_url(self):
        """Get the url to duplicate the activity inside the user private library

        Returns
        -------
        string
            The wanted url.

        """
        return reverse(
            'trainings:private-library-activity-duplicate',
            kwargs={'activity_uuid': self.pk}
        )

    def get_public_library_duplicate_url(self):
        """Get the url to duplicate the activity inside the public library

        Returns
        -------
        string
            The wanted url.

        """
        return reverse(
            'trainings:public-library-activity-duplicate',
            kwargs={'activity_uuid': self.pk}
        )

    def get_delete_url(self):
        """Get the url to soft-delete the activity

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('trainings:activity-delete', kwargs={'activity_uuid': self.pk})

    def get_qrcode_data(self):
        """Get data will be stored in the QRCode to launch an activity.

        Returns
        -------
        dict
            Data to insert in the QRCode.

        """
        activity_type = getattr(constants.MODULE_CHOICES, self.__class__.__name__.upper())

        # When the activity is an original version, we return its id in `original_version` property.
        # This hack allow to keep compatibilities with old iPad versions.

        data = {
            'id': self.id,
            'original_version': self.original_version_id or self.id,
            'type': activity_type,
            'action': constants.ACTIVITY_LAUNCH_ACTION
        }

        return data

    def get_qrcode_url(self):
        """Get the url to download the QRCode able to launch activity.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse(
            'trainings:download-launch-qrcode-detail',
            kwargs={
                'activity_uuid': self.pk,
                'training_uuid': self.training.pk
            }
        )


class TrainingActivityLibraryStatus(Syncable):
    """Django model containing a ``TrainingActivity`` status regarding the library

    Attributes
    ----------
    owner: Django foreign key to ``MTPUser``
        The owner of the tied ``TrainingActivity``
    is_shared: django ``BooleanField`` field
        The status of the tied activity regarding the library
        If True, the tied activity will appear in the "shared library", and will be accessible
        by other trainers.

    """

    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='trainingactivity_library_statuses',
        verbose_name=_('Owner'),
        on_delete=models.CASCADE
    )

    is_shared = models.BooleanField(
        default=False,
        verbose_name=_('Is shared')
    )
