""" This module holds the models of the ``brainstormers`` app. """

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.core.validators import MaxLengthValidator
from django.db import models
from django.utils.functional import cached_property
from django.utils.text import Truncator
from django.utils.translation import ugettext_lazy as _

from django_extensions.db.models import TimeStampedModel

from mtp.apps.broker_models_hooks.models import BrokerHookMixin
from mtp.apps.common.models import Orderable
from mtp.apps.likes.models import Like, Likeable
from mtp.apps.sync.models import Syncable
from mtp.apps.trainings.models import TrainingActivity

from .constants import BRAINSTORMER_TYPE_CHOICES

from .managers import (
    BrainstormerManager,
    ProxyfiedBrainstormerManager,
    BrainstormerPropositionGroupManager,
    BrainstormerPropositionGroupQuerySet,
    BrainstormerPropositionManager,
    BrainstormerPropositionQuerySet,
    BrainstormerQuerySet,
)


class BrainstormerTemplate(TrainingActivity):
    """ A Template is a Brainstormer model.

    Attributes
    ----------
    name : django ``CharField``
        The name of this brainstormer template.
        Inherited from ``TrainingActivity``.
    training : django ``ForeignKey``
        The training this brainstormer template belongs to.
        Inherited from ``TrainingActivity``.
    sort_order : django ``PositiveIntegerField``
        The sort order of this brainstormer template.
        Inherited from ``Orderable``.
    brainstormer_type : django ``CharField``
        The type of brainstomer template.

    """

    brainstormer_type = models.CharField(
        max_length=20,
        choices=BRAINSTORMER_TYPE_CHOICES,
        default=BRAINSTORMER_TYPE_CHOICES.PROPOSITIONWALL,
        verbose_name=_('brainstormer type')
    )

    class Meta:
        ordering = ['sort_order']

    def __str__(self):
        return '%s (template)' % self.name

    @classmethod
    def verbose_activity_name(cls, brainstormer_type=BRAINSTORMER_TYPE_CHOICES.PROPOSITIONWALL):
        """Class method to return the activity name depending on the `BrainstormerType.

        Parameters
        ----------
        brainstormer_type: string
            the `BRAINSTORMER_TYPE_CHOICES` used for this template

        Returns
        -------
        string
            the verbose name of the brainstormer.

        """
        return BRAINSTORMER_TYPE_CHOICES.for_value(brainstormer_type).display

    def verbose_activity_instance_name(self):
        """Same as verbose_activity_name but for an instance instead of a class.

        Returns
        -------
        string
            the verbose name of the brainstormer instance.

        """
        return BRAINSTORMER_TYPE_CHOICES.for_value(self.brainstormer_type).display

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

        Returns
        -------
        str

        """

        return BRAINSTORMER_TYPE_CHOICES.for_value(self.brainstormer_type).constant.lower()

    @classmethod
    def get_create_url(cls, training, brainstormer_type):
        """Get the url of to create a new brainstormer template.

        Parameters
        ----------
        brainstormer_type: str
            the `BRAINSTORMER_TYPE_CHOICES` used for this template

        training: Training
            training the `BrainstormerTemplate` to be created belongs to

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

        """
        return reverse(
            'trainings:brainstormer-template-create',
            kwargs={
                'training_uuid': training.pk,
                'brainstormer_type_slug': brainstormer_type
            }
        )

    @classmethod
    def get_import_url(cls, training):
        """ Get the url of to import a new brainstormer template.

        Parameters
        ----------
        training: Training
            training the `BrainstormerTemplate` to be created belongs to

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

        """
        return reverse(
            'brainstormers:brainstormer-template-import-xls',
            kwargs={'training_uuid': training.pk}
        )

    def save(self, *args, **kwargs):
        """Manually crop the "name" field

        For the parameters, see ``django.db.models.Model``
        """

        if len(self.name) > 100:
            self.name = Truncator(self.name).chars(100, truncate='(...)')

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

    def get_export_url(self):
        """ Get the url of to export a brainstormer template.

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

        """
        return reverse(
            'brainstormers:brainstormer-template-export-xls',
            kwargs={'brainstormer_template_uuid': self.pk}
        )

    def get_absolute_url(self):
        """ Get the url of the current brainstormer template.

        It actually returns the detail url.

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

        """
        return self.get_detail_url()

    def get_detail_url(self):
        """ Get the url of the current brainstormer template.

        It actually returns the edit url, as there is no "read" view
        for a brainstormer template.

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

        """
        return reverse(
            'brainstormers:template-detail',
            kwargs={'template_uuid': self.pk}
        )

    def get_edit_url(self):
        """ Get the url to edit the current brainstormer template.

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

        """
        return reverse(
            'brainstormers:template-update',
            kwargs={'template_uuid': self.pk}
        )

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

        We disable this option for brainstormer template.

        """
        return None


class Brainstormer(Orderable, Syncable):
    """ A Brainstormer allows to organize propositions on a work subject
    through groups.

    A Brainstormer is built by copying a BrainstormerTemplate.
    However, it is linked to a training session instead of a training.

    Attributes
    ----------
    name : django ``CharField``
        The name of this brainstormer
    session : django ``ForeignKrey``
        The training session nthis brainstormer is a part of.

    Notes
    -----
    This model is ``Syncable`` and ``Orderable``.
    """

    brainstormer_type = models.CharField(
        max_length=20,
        choices=BRAINSTORMER_TYPE_CHOICES,
        default=BRAINSTORMER_TYPE_CHOICES.PROPOSITIONWALL)
    name = models.CharField(_('name'), max_length=100)
    description = models.TextField(null=True, blank=True)
    session = models.ForeignKey('trainings.TrainingSession', on_delete=models.CASCADE)
    anonymous = models.BooleanField(default=False)
    objects = BrainstormerManager.from_queryset(BrainstormerQuerySet)()

    is_exportable = True

    class Meta:
        ordering = ['sort_order']

    def __str__(self):
        return '%s' % self.name

    def save(self, *args, **kwargs):
        """
        A Brainstormer can inherit of BrainstormerTemplate data, so before save it we verify
        if a template is given in kwargs for populate attributes of Brainstormer.
        """
        try:

            # We populate the attributes of the brainstormer with data
            # from its originating template.
            template = kwargs.pop('template')
            self.name = Truncator(template.name).chars(100, truncate='(...)')
            self.sort_order = template.sort_order
            self.brainstormer_type = template.brainstormer_type
        except KeyError:

            # No originating template?
            pass

        finally:

            # We can now save the brainstormer...
            super().save(*args, **kwargs)

            # ... soft-delete the groups if the brainstormer is soft-deleted
            if self.is_deleted:
                for group in self.groups.all():
                    group.is_deleted = self.is_deleted
                    group.save()

    def get_propositions(self):
        """Get the list of proposals for this brainstormer.

        The QuerySet prefetch the "likes" for each proposal.

        Returns
        -------
        BrainstormerPropositionQuerySet
            Return all the propositions made during a brainstormer session.

        """
        return self.propositions.non_deleted().prefetch_related(
            models.Prefetch('likes', queryset=Like.objects.non_deleted())
        )

    def get_create_proposition_url(self):
        """Get the url to create a new proposition

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

        """
        return reverse('brainstormers:create_proposition', kwargs={'brainstormer_uuid': self.pk})

    def get_detail_url(self):
        """Get the url to view this brainstormer

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

        """

        return reverse('brainstormers:detail', kwargs={'brainstormer_uuid': self.pk})

    def get_update_url(self):
        """Get the url to update this brainstormer

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

        """

        return reverse('brainstormers:update', kwargs={'brainstormer_uuid': self.pk})

    def get_delete_url(self):
        """Get the url to delete this brainstormer

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

        """

        return reverse('brainstormers:delete', kwargs={'brainstormer_uuid': self.pk})

    def proxified(self):
        """Get the proxy model corresponding to self.brainstormer_type.

        Returns
        -------
        `PropositionWall`, `QuestionWall` or `WorldCloud` instance
        """
        type_dict = {
            BRAINSTORMER_TYPE_CHOICES.PROPOSITIONWALL: PropositionWall,
            BRAINSTORMER_TYPE_CHOICES.QUESTIONWALL: QuestionWall,
            BRAINSTORMER_TYPE_CHOICES.WORDCLOUD: WordCloud
        }
        # pylint: disable=attribute-defined-outside-init
        self.__class__ = type_dict[self.brainstormer_type]
        return self


class PropositionWall(Brainstormer):
    """PropositionWall is a Brainstormer with groups.

    Its a proxy model. It means that it share the same database table than a Brainstormer.
    The only difference is the `get_groups` and `get_grouped_propositions` not available
    for `Brainstormer` `QuestionWall` and `WordCloud`.
    """

    objects = ProxyfiedBrainstormerManager()
    proxyfied_type = BRAINSTORMER_TYPE_CHOICES.PROPOSITIONWALL

    class Meta:
        proxy = True
        ordering = ['sort_order']

    def get_groups(self):
        """Get the groups for this propositionwall.

        Returns
        -------
        BrainstormerPropositionQuerySet
            Return all the proposition groups linked to this propositionwall.

        """
        return self.groups.non_deleted()

    def get_grouped_propositions(self):
        """Get ordered propositions in groups.

        Returns
        -------
        dict
            Return all the propositions made during a brainstormer session,
            organized according to the group they belong to.

        """
        grouped_propositions = {}
        for group in self.get_groups():
            grouped_propositions[group] = group.propositions.non_deleted().annotate_with_likes()

        return grouped_propositions

    def get_ungrouped_propositions(self):
        """Get ordered propositions without group.

        Returns
        -------

        list
            Return all the propositions with no group made durring a brainstormer session.
        """
        return self.get_propositions().filter(groups__isnull=True).annotate_with_likes()

    def get_export_groups(self):
        """Return the groups from this brainstormer
        plus an empty group for non grouped propositions.

        Notes
        -----

          the xls exporter is unable to render anything but models. Because we need a placeholder
          for ungrouped propositions, we create a group without saving it to the database. Next,
          `get_export_propositions` will detect this special group and
          will return all the ungrouped propositions

        """
        empty_group = BrainstormerPropositionGroup()
        empty_group.name = "No Group"
        empty_group.brainstormer = self
        groups = list(self.groups.non_deleted()) + [empty_group]
        return groups

    def get_export_xls_url(self):
        """Get the url to export this brainstormer to an xls file

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

        """

        return reverse('brainstormers:brainstormer-export-xls',
                       kwargs={'brainstormer_uuid': self.pk})

    def get_export_csv_url(self):
        """Get the url to export this brainstormer to an csv file

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

        """

        return reverse('brainstormers:export_csv', kwargs={'brainstormer_uuid': self.pk})

    def get_resource(self):
        """Get the resource class matching the current proxy class."""

        from .resources import PropositionWallResource
        return PropositionWallResource


class QuestionWall(Brainstormer):
    """QuestionWall is a Brainstormer with creatable propositions on CMS.

    Its a proxy model. It means that it share the same database table than a Brainstormer.
    """
    objects = ProxyfiedBrainstormerManager()
    proxyfied_type = BRAINSTORMER_TYPE_CHOICES.QUESTIONWALL

    class Meta:
        proxy = True
        ordering = ['sort_order']

    def get_create_proposition_url(self):
        """Get the url to create proposition in this questionwall

        Returns
        -------
        string

        """
        return reverse('brainstormers:create_proposition', kwargs={
            'brainstormer_uuid': self.id,
        })

    def get_export_xls_url(self):
        """Get the url to export this questionwall to an xls file.

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

        """
        return reverse('brainstormers:brainstormer-export-xls',
                       kwargs={'brainstormer_uuid': self.pk})

    def get_export_csv_url(self):
        """Get the url to export this questionwall to an csv file

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

        """

        return reverse('brainstormers:export_csv', kwargs={'brainstormer_uuid': self.pk})

    def get_resource(self):
        """Get the resource class matching the current proxy class."""

        from .resources import QuestionWallResource
        return QuestionWallResource


    def get_export_propositions(self):
        """Returns all propositions belonging of the QuestionWall."""
        return self.propositions.non_deleted().annotate_with_likes()


class WordCloud(Brainstormer):
    """WordCloud is a brainstormer without groups and without creatable propositions on CMS.

    Its a proxy model. It means that it share the same database table than a Brainstormer.
    """
    objects = ProxyfiedBrainstormerManager()

    proxyfied_type = BRAINSTORMER_TYPE_CHOICES.WORDCLOUD
    is_exportable = False

    class Meta:
        proxy = True
        ordering = ['sort_order']


class BrainstormerPropositionGroup(BrokerHookMixin, Orderable, Syncable):
    """ Group Brainstormer propositions.

    Attributes
    ----------
    brainstormer : django ``ForeignKey``
        The brainstormer this group belongs to.
    name : django ``CharField``
        A short name for this group.
    color : django ``CharField``
        The color associated with this group.
    coord_x : django ``DecimalField``
        The position of this group on an x-axis. Useful on iOS.
    coord_y : django ``DecimalField``
        The position of this group on an y-axis. Useful on iOS.

    Notes
    -----
    This model is ``Syncable`` and ``Orderable``.
    """

    brainstormer = models.ForeignKey(
        'Brainstormer',
        related_name='groups',
        on_delete=models.CASCADE
    )
    name = models.CharField(max_length=50, blank=True, verbose_name=_('name'))
    color = models.CharField(max_length=30, blank=True, verbose_name=_('color'))
    coord_x = models.DecimalField(max_digits=6, decimal_places=2, blank=True, default=0)
    coord_y = models.DecimalField(max_digits=6, decimal_places=2, blank=True, default=0)

    objects = BrainstormerPropositionGroupManager.from_queryset(
        BrainstormerPropositionGroupQuerySet)()

    class Meta:
        ordering = ['sort_order']

    def __str__(self):
        return '%s' % self.name

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        # Archiving propositions if group is archived
        if self.is_deleted:
            for proposition in self.propositions.all():
                proposition.is_deleted = self.is_deleted
                proposition.save()

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

        Returns
        -------
        str

        """

        return '%s/brainstormers/%s/groups' % (
            self.brainstormer.session_id,
            self.brainstormer_id
        )

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

        Returns
        -------
        dict

        """

        # Avoid circular imports
        from mtp.apps.brainstormers.api.serializers import (
            BrainstormerPropositionGroupSimpleSerializer
        )

        return BrainstormerPropositionGroupSimpleSerializer(self).data

    def get_export_propositions(self):
        """Returns all propositions belonging to `self`.

        If `self` does not have a pk, returns all the
        `self.brainstormer.propositions` without groups.

        See Brainstormer.get_export_groups for an explanation of this behavior

        """
        if not self.pk:
            return self.brainstormer.propositions.filter(groups=None).annotate_with_likes()
        else:
            return self.propositions.non_deleted().annotate_with_likes()


class BrainstormerProposition(BrokerHookMixin, Likeable, Orderable, Syncable, TimeStampedModel):
    """ Brainstormer proposition arrange by group.

    Attributes
    ----------
    text : django ``CharField``
        Text of the proposition. The max length depends on the brainstormer type.
        See ``TEXT_MAXLENGTH`` to get the different max lengths.
        Validation is forced in ``save`` (else it's forced at the database level,
        but we don't have a variable max length at the database level)
    user : django ``ForeignKey``
        The trainee who made this proposition.
    groups : django ``ManyToManyField``
        The groups this proposition belongs to.

    TEXT_MAXLENGTH : dict
        A dict to know the max length of the `text` field depending
        on the brainstormer type. The keys are the possible types
        for the brainstormer, from ``BRAINSTORMER_TYPE_CHOICES``
        and the values are integers.
    TEXT_MAX_MAXLENGTH : int
        The maximum length allowed for the `text` field. Used
        as a default max length, and in database.

    Notes
    -----
    This model is ``Syncable``, ``Orderable`` and ``TimeStamped``.

    """

    class Meta:
        ordering = ['sort_order']

    TEXT_MAXLENGTH = {
        BRAINSTORMER_TYPE_CHOICES.QUESTIONWALL: 140,
        BRAINSTORMER_TYPE_CHOICES.PROPOSITIONWALL: 80,
        BRAINSTORMER_TYPE_CHOICES.WORDCLOUD: 30,
    }
    TEXT_MAX_MAXLENGTH = max(TEXT_MAXLENGTH.values())

    brainstormer = models.ForeignKey(
        'Brainstormer',
        related_name='propositions',
        on_delete=models.CASCADE
    )
    text = models.CharField(max_length=TEXT_MAX_MAXLENGTH, verbose_name=_('text'))
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_('user'),
        on_delete=models.CASCADE
    )
    groups = models.ManyToManyField('BrainstormerPropositionGroup',
                                    related_name='propositions')

    objects = BrainstormerPropositionManager.from_queryset(
        BrainstormerPropositionQuerySet)()

    def __str__(self):
        return '%s' % self.text

    def _get_text_maxlength_validator(self):
        """Utils to get the validator that will handle the text max length

        Returns
        -------
        MaxLengthValidator

        """

        for validator in self._meta.get_field('text').validators:
            if isinstance(validator, MaxLengthValidator):
                return validator

    def __setattr__(self, key, value):
        """Will change the max length of the text field when the brainstormer type changes

        Parameters
        ----------
        key: str
            The name of the attribute to set
        value : any
            The value to set in the attribute

        """

        super().__setattr__(key, value)

        if key in ('brainstormer', 'brainstormer_id'):

            if self.brainstormer_id:
                maxlength = self.TEXT_MAXLENGTH.get(
                    self.brainstormer.brainstormer_type, self.TEXT_MAX_MAXLENGTH
                )
            else:
                maxlength = self.TEXT_MAX_MAXLENGTH

            validator = self._get_text_maxlength_validator()
            validator.limit_value = maxlength

    def save(self, *args, **kwargs):
        """Enforce the check of the text max length

        This is done because the max length of the text field depends
        on the brainstormer type, and we cannot be sure the validator
        has been called with the correct max length.

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

        """

        validator = self._get_text_maxlength_validator()
        try:
            validator(self.text)
        except ValidationError as exc:
            raise ValidationError({'text': exc})

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

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

        Returns
        -------
        str

        """

        brainstormer = self.brainstormer

        return '%s/brainstormers/%s/propositions' % (
            brainstormer.session_id,
            brainstormer.id
        )

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

        Returns
        -------
        dict

        """

        # Avoid circular imports
        from mtp.apps.brainstormers.api.serializers import BrainstormerPropositionSerializer

        return BrainstormerPropositionSerializer(self).data
