"""Managers and querysets for the models in ``mtp.apps.trainings``."""

from collections import OrderedDict
from datetime import timedelta

from django.db import models
from django.db.models import BooleanField, Q, Value, When
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from mtp.apps.assessments.managers import AssessableQuerySet
from mtp.apps.common.utils import start_of_day
from mtp.apps.sync.managers import SyncableQuerySet
from mtp.discrete_apps.versionable.managers import (
    PolymorphicManagerForDependency,
    PolymorphicQuerySetForDependency,
    VersionableManager,
    VersionableQuerySet,
)


class VersionableTrainingQuerySet(SyncableQuerySet, VersionableQuerySet):
    """Queryset for the ``mtp.apps.trainings.models.Training`` model."""

    def annotate_is_duplicable_by(self, user):
        """Annotate the queryset with a ``is_duplicable`` flag for each training.

        A training can be duplicable if the ``business_unit`` is managed by the current user.

        Parameters
        ----------
        user: MTPUser
            The user we want to annotate the QuerySet for.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), annotated.

        """

        managed_groups_pks = user.managed_groups.values_list('pk', flat=True)

        return self.annotate(
            is_duplicable=models.Case(
                models.When(
                    business_unit_id__in=managed_groups_pks,
                    then=models.Value(True)
                ),
                default=models.Value(False),
                output_field=models.BooleanField(),
            )
        )

    def filter_is_duplicable_by(self, user):
        """Filter only trainings who can be duplicable.

        Parameters
        ----------
        user: MTPUser
            The user we want to filter the QuerySet for.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """
        return self.annotate_is_duplicable_by(user).filter(is_duplicable=True)

    def filter_is_draftable(self):
        """Filter Training instances having a ``version_state`` in the
        ``DRAFTABLE_STATES`` subset.

        Returns
        -------
        TrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter_in_state(self.model.DRAFTABLE_STATES)

    def annotate_is_owned(self, user):
        """Annotate the queryset with a ``is_owned`` flag for a given user.

        In the result queryset, this flag is set to ``True`` for trainings marked as
        owned by the given user, and ``False`` otherwise.

        Parameters
        ----------
        user : mtp.apps.accounts.models.MTPUser
            The user for whom we want to flag all trainings.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), annotated.

        """

        return self.annotate(
            is_owned=models.Case(
                models.When(
                    owner=user,
                    then=models.Value(True)
                ),
                default=models.Value(False),
                output_field=models.BooleanField(),
            )
        )

    def owned_by(self, user):
        """Filter only trainings who belongs to a user.

        Parameters
        ----------
        user : mtp.apps.accounts.models.MTPUser
            The user for whom we want the trainings.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(owner=user)

    def available_for_user(self, user):
        """Filter only trainings which can be used to create a new ``TrainingSession``.

        The training's ``version_state`` must be set to ``PUBLISHED``,
        and owned by the user.

        Parameters
        ----------
        user : mtp.apps.accounts.models.MTPUser
            The user for whom we want the trainings.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.owned_by(user).exclude_is_archived()

    def for_trainee(self, user):
        """Filter the ``Training`` for the users who belongs to the `trainee` group.

        trainee users have access to the ``Training`` who holds at least
        one ``TrainingSession`` the user is into, and who have a date in the past.

        Parameters
        ----------
        user : mtp.apps.accounts.models.MTPUser
            The user for whom we want the trainings.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(
            training_sessions__in=user.training_sessions.filter(
                start_date__lte=start_of_day(timezone.now())
            ),
        )

    def for_job_profile(self, job_profile):
        """Filter only trainings associated with a job_profile.

        Parameters
        ----------
        job_profile : mtp.apps.accounts.models.Group
            The job_profile for whom we want the trainings.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """
        return self.filter(job_profile=job_profile)

    def for_business_unit(self, business_unit):
        """Filter only trainings associated with a business_unit.

        Parameters
        ----------
        business_unit : mtp.apps.accounts.models.Group
            The business_unit for whom we want the trainings.

        Returns
        -------
        VersionableTrainingQuerySet
            A clone of the original queryset (``self``), filtered.

        """
        return self.filter(business_unit=business_unit)

    def recently_created(self, user=None):
        """Extract the recently created trainings for a user from the current queryset

        The trainings will be annotated with a notice to explain the reason why each of these
        training is in this list, possibly by getting info from a celery task

        Parameters
        ----------
        user : MTPUser, optional
            The user for which the list is displayed, to set the notices. If not give, no notice
            will be attached

        Returns
        -------
        list
            A list with the recently created trainings.

        """

        now = timezone.now()
        in_the_past = now - timedelta(days=1)
        in_a_recent_past = now - timedelta(minutes=60)

        # Extract the trainings created during the last 24h
        all_recent_trainings = self.filter(
            created_at__gte=in_the_past
        ).order_by(
            '-created_at'
        ).select_related('created_by_task')

        # Use the task of each training to keep or remove them from the final list
        from mtp.apps.trainings.models import TrainingTask  # Avoid recursive import

        result = []
        for training in all_recent_trainings:
            try:
                task = training.created_by_task
            except TrainingTask.DoesNotExist:
                # Not created by a task, we only display it if it was done very recently
                if training.created_at >= in_a_recent_past:
                    training.notice = _('Recently created')  # default notice
                    result.append(training)
                continue

            # We have a task:
            # - if the user created it, we only display it if it was done very recently
            if user and user.pk == task.user_id and training.created_at < in_a_recent_past:
                continue

            # - when we keep the training, we add a notice
            training.notice = task.get_finished_notice_for_user(user)

            result.append(training)

        return result

    def get_duplicate_ids(self, training_ids, exclude_owner_id=None):
        """Find the all the duplicates ids of the provided training ids

        Parameters
        ----------
        training_ids: list(str)
            The list of training ids for which we want the direct duplicate ids
        exclude_owner_id: None or str
            If provided, the training with the corresponding owner_id will be excluded.

        Returns
        -------
        list(str)
            The list of duplicate ids

        Notes
        -----
        This method is recursive.

        """

        if not training_ids:
            return []

        direct_duplicate_ids = self.filter(parent_version__in=training_ids)

        if exclude_owner_id:
            direct_duplicate_ids = direct_duplicate_ids.exclude(owner_id=exclude_owner_id)

        direct_duplicate_ids = list(direct_duplicate_ids.values_list(
            'id', flat=True
        ))

        return direct_duplicate_ids + self.get_duplicate_ids(
            direct_duplicate_ids,
            exclude_owner_id
        )


class VersionableTrainingManager(VersionableManager.from_queryset(VersionableTrainingQuerySet)):
    """Manager for the ``mtp.apps.trainings.models.Training`` model."""

    pass


class LiveActivityQueryset(models.QuerySet):
    """QuerySet for the ``LiveActivity`` abstract model."""

    def for_session(self, session):
        """Filter the queryset with the provided session.

        Parameters
        ----------
        session : TrainingSession
            The session for which we want the activities.

        Returns
        -------
        QuerySet

        """

        # using ``Q(session=session) | Q(training=session.training)`` raise a deepcopy bug
        # in polymorphic
        return self.filter(Q(session_id=session.pk) | Q(training_id=session.training_id))

    def for_training(self, training):
        """Filter the queryset with the provided training.

        Parameters
        ----------
        training : Training
            The training for which we want the activities.

        Returns
        -------
        QuerySet

        """

        return self.filter(training=training)


class TrainingActivityQuerySet(LiveActivityQueryset,
                               AssessableQuerySet,
                               SyncableQuerySet,
                               PolymorphicQuerySetForDependency):
    """Queryset for the ``mtp.apps.trainings.models.TrainingActivity`` model."""

    def for_user(self, user):
        """Filter only activities owned by the given user.

        Parameters
        ----------
        user: MTPUser
            The user for which we want to filter the owned activities

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(training__owner=user)

    def annotate_is_visible(self, user):
        """Annotate the queryset accordingly to the given user

        Parameters
        ----------
        user: MTPUser
            The user for which we want to annotate the queryset

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), annotated.

        """

        return self.select_related(
            'library_status',
            'library_status__owner',
            'training',
            'training__owner'
        ).annotate(
            is_visible=models.Case(
                When(
                    Q(training__owner=user)
                    | Q(library_status__owner=user, library_status__is_shared=False)
                    | Q(library_status__is_shared=True),
                    then=Value(True)
                ),
                default=Value(False),
                output_field=BooleanField(),
            )
        )

    def visible(self):
        """Filter only visible activities

        You must call ``annotate_is_visible`` before using this filter

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(is_visible=True)

    def annotate_is_editable(self, user):
        """Annotate the queryset accordingly to the given user

        Parameters
        ----------
        user: MTPUser
            The user for which we want to annotate the queryset

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), annotated.

        """

        from mtp.apps.trainings.models import Training

        return self.select_related(
            'library_status',
            'library_status__owner',
            'training',
            'training__owner'
        ).annotate(
            is_editable_by_user=models.Case(
                When(
                    Q(training__owner=user)
                    & Q(training__version_state=Training.VERSION_STATES.DRAFT),
                    then=Value(True)
                ),
                When(
                    Q(library_status__owner=user)
                    & Q(library_status__is_shared=False),
                    then=Value(True)
                ),
                default=Value(False),
                output_field=BooleanField(),
            )
        )

    def editable(self):
        """Filter only editable activities by the provided user

        You must call ``annotate_is_editable`` before using this filter

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(is_editable_by_user=True)

    def deletable(self, user):
        """Filter only deletable activities by the provided user

        Parameters
        ----------
        user: MTPUser
            The user for which we want to filter the editable activities

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        from mtp.apps.trainings.models import Training

        return self.filter(
            Q(training__version_state=Training.VERSION_STATES.DRAFT)
            | Q(library_status__owner=user)
        )

    def available(self):
        """Filter out the deactivated ``TrainingActivity`` instances

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        from mtp.apps.trainings.models import TrainingActivity

        return self.instance_of(*TrainingActivity.available_activity_models())

    def duplicable_in_private_library(self, user):
        """Filter the ``TrainingActivity`` duplicable in the user's library

        Parameters
        ----------
        user: MTPUser
            The user for which we want the ``TrainingActivity``

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(
            Q(library_status__owner=user, library_status__is_shared=False)
            | Q(library_status__is_shared=True)
            | Q(training__owner=user)
        )

    def duplicable_in_public_library(self, user):
        """Filter the ``TrainingActivity`` duplicable in the public library

        Parameters
        ----------
        user: MTPUser
            The user for which we want the ``TrainingActivity``

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(
            Q(library_status__owner=user, library_status__is_shared=False)
            | Q(training__owner=user)
        )

    def user_library(self, user):
        """Filter the ``TrainingActivity`` available in the given user library

        Parameters
        ----------
        user: MTPUser
            The user for which we want the ``TrainingActivity``

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(
            library_status__owner=user,
            library_status__is_shared=False
        )

    def public_library(self):
        """Filter the ``TrainingActivity`` shared in the public library

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(library_status__is_shared=True)

    def annotate_is_owned(self, user):
        """Annotate the Queryset with a ``is_owned`` flag depending on the provided user

        Parameters
        ----------
        user: MTPUser
            The user for which we want to annotate the ``QuerySet``

        Returns
        -------
        DependencyTrainingActivityQuerySet
            A clone of the original queryset (``self``), annotated.

        """

        return self.annotate(
            is_owned=models.Case(
                When(Q(training__owner=user) | Q(library_status__owner=user), then=Value(True)),
                default=Value(False),
                output_field=BooleanField(),
            )
        )


class TrainingActivityManager(
        PolymorphicManagerForDependency.from_queryset(TrainingActivityQuerySet)
    ):
    """Manager for the ``mtp.apps.trainings.models.TrainingActivity`` model."""

    def check_available_name(self, klass, name, training=None, owner=None, is_shared=None):
        """Check if the given ``TrainingActivity`` name is available in the given "target"

       The "target" can be:
       - A ``Training`` instance (the ``training`` param is provided)
       - The public or the private library (the ``owner`` and ``is_shared`` params
       are provided)

        Parameters
        ----------
        klass: models.Model
            The model of the ``TrainingActivity
        name: str
            The name of the ``TrainingActivity``
        training: Training
            The training of the ``TrainingActivity``
        owner: MTPUser
            The owner of the ``TrainingActivity``
        is_shared: bool
            The shared status of the ``TrainingActivity``

        Returns
        -------
        tuple(bool, str)
            A tuple with two entries:
            - True if the given "name" is available in the given "target",
            False otherwise
            - The original name if available, or a suggested name otherwise

        """

        existing_activities = self.instance_of(klass).non_deleted()

        # The target is a training
        if training:
            existing_activities = existing_activities.filter(training=training)
        # The target is either the public or the private library
        elif owner:
            existing_activities = existing_activities.filter(
                library_status__owner=owner,
                library_status__is_shared=is_shared
            )

        # We don't have any existing duplicates with the same name in the target
        if not existing_activities.filter(name=name).exists():
            return True, name

        duplicate_number = 1

        while True:
            new_name = '%s (%d)' % (name, duplicate_number)

            if not existing_activities.filter(name=new_name).exists():
                break

            duplicate_number += 1

        return False, new_name


class TrainingSessionQuerySet(SyncableQuerySet, models.QuerySet):
    """Queryset for the ``mtp.apps.trainings.models.Training`` model."""

    def visible_by_user(self, user):
        """Filter only sessions a user can access

        The QuerySet is filtered accordingly to the user roles or relations
        to the session's training.

        Parameters
        ----------
        user : mtp.apps.accounts.models.MTPUser
            The user for which we want the sessions.

        """

        return self.filter(
            models.Q(
                training__job_profile_id__in=user.managed_groups.job_profiles().values_list(
                    'pk', flat=True
                )
            ) | models.Q(
                training__business_unit_id__in=user.managed_groups.business_units().values_list(
                    'pk', flat=True
                )
            ) | models.Q(
                training__owner=user
            ) | models.Q(
                users=user,
                start_date__lte=start_of_day(timezone.now())
            )
        ).distinct()

    def not_closed(self):
        """Filter only sessions which don't have a ``state`` to ``CLOSED``

        Returns
        -------
        TrainingSessionQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.exclude(state=self.model.STATE_CHOICES.CLOSED)

    def closed(self):
        """Filter only sessions which have a ``state`` to ``CLOSED``

        Returns
        -------
        TrainingSessionQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(state=self.model.STATE_CHOICES.CLOSED)

    def for_training(self, training):
        """Filter only sessions which belongs to the given ``Training``.

        Parameters
        ----------
        training : mtp.apps.accounts.models.Training
            The training for which we want the sessions.

        Returns
        -------
        TrainingSessionQuerySet
            A clone of the original queryset (``self``), filtered.

        """

        return self.filter(training=training)

    def between_dates(self, date_begin=None, date_end=None):
        """Allow filter training sessions for a given time period.

        Parameters
        ----------
        date_begin : ``datetime``, default to ``None``
            If specified, only the session starting from this time are taken into account.
        date_end : ``datetime``, default to ``None``
            If specified, the session ending after this time are excluded.

        Returns
        -------
        TrainingSessionQuerySet
            A Queryset filtered if at least one if ``date_begin`` and ``date_end`` is set.

        """
        filters = {}

        if date_begin is not None:
            filters['start_date__gte'] = date_begin
        if date_end is not None:
            filters['end_date__lte'] = date_end

        if filters:
            return self.filter(**filters)

        return self


class TrainingSessionManager(models.Manager.from_queryset(TrainingSessionQuerySet)):
    """Manager for the ``mtp.apps.trainings.models.TrainingSession`` model."""

    def close_outdated(self, days_after=0, dry_run=False, async=False):
        """Close the "outdated" ``TrainingSession`` instances

        Where "outdated" means the session got a ``end_date`` in the past,
        and a ``state`` to ``DURING``.

        Parameters
        ----------
        days_after : int
            It's a kind of a "grace period". It's allow closing the session not directly
            after they were played, but after a given amount of days.
        dry_run : bool
            If ``False``, close the sessions and return the list of closed session pks,
            if ``True``, simply return the list of potential closed session pks without
            closing them.
        async : bool
            Default to ``False``.
            If ``True``, the session closing will be done asynchronously, otherwise it will be
            executed directly.

        Returns
        -------
        list
            A list containing the UUID of the closed sessions

        """

        date_end_filter = timezone.now() - timedelta(days=days_after)

        outdated_sessions = self.not_closed().between_dates(
            date_end=date_end_filter
        )

        # Yes one more query but this is executed only once a day...
        pks = list(outdated_sessions.values_list('pk', flat=True))

        if not dry_run:
            for session in outdated_sessions:
                session.close(async=async)

        return pks

    def get_assessing_sessions(self):
        """Get assessing sessions.

        Notes
        -----
        We use assessing activities in trainings to determine if the session is assessing or not.

        Returns
        -------
        QuerySet

        """

        return self.non_deleted().filter(
            training__activities__is_deleted=False,
            training__activities__is_assessing=True,
        ).distinct()


class TrainingTaskManager(models.Manager):
    """Manager for the ``mtp.apps.trainings.models.TrainingTask`` model."""

    def group_tasks(self, tasks):
        """Regroup tasks by training then by task type

        Parameters
        ----------
        tasks: iterator(mtp.apps.trainings.models.TrainingTask)
            The task to group

        Returns
        -------
        OrderedDict
            A dict with an entry by training having a task (the key being the training pk), then as
            value another dict with a `training` key holding the training itself, and a `tasks`
            key holding a dict with the task type constant as key, and as value an array of tasks
            for this training and task type.

            For example:

            {
                123: {
                    'training': <Training object>,
                    'tasks': {
                        'CREATE_DRAFT': [<TrainingTask object>, <TrainingTask object>],
                        'DUPLICATE_TO_MEMBER': [<TrainingTask object>],
                    }
                },
                234: {
                    'training': <Training object>,
                    'tasks': {
                        'DUPLICATE_TO_MEMBER': [<TrainingTask object>, <TrainingTask object>],
                    }
                },
            }

        """

        grouped_tasks = OrderedDict()
        for task in tasks:
            task_type = task.TASK_TYPES.for_value(task.task_type).constant
            grouped_tasks.setdefault(
                task.training.pk, {
                    'training': task.training,
                    'tasks': {}
                }
            )
            grouped_tasks[task.training.pk]['tasks'].setdefault(task_type, []).append(task)

        return grouped_tasks

    def group_tasks_for_json(self, tasks):
        """Regroup tasks by training then by task type, in a json-ready way

        Parameters
        ----------
        tasks: iterator(mtp.apps.trainings.models.TrainingTask)
            The task to group

        Returns
        -------
        OrderedDict
            A dict that have training pks as keys, and as values, a dict with a ``training_name``
            and a ``tasks`` keys, this last one holding a dict with an entry for each ``task_type``
            for which the training has waiting keys, each one being a dict with a ``remaining`` keys
            holding a list of the remaining tasks ids for this training+task_type

            For example:

            {
                "123": {
                    "training_name": "foo",
                    "tasks: {
                        "CREATE_DRAFT": {"remaining": [1, 2]},
                        "DUPLICATE_MEMBERS: {"remaining": [3]},
                    }
                },
                "234": {
                    "training_name": "bar",
                    "tasks: {
                        "DUPLICATE_MEMBERS: {"remaining": [5, 6]},
                    }
                },
            }
        """

        grouped_tasks = self.group_tasks(tasks)
        result = OrderedDict()
        for training_pk, training_info in grouped_tasks.items():
            result[training_pk] = {
                'training_name': training_info['training'].name,
                'tasks': {
                    task_type: {'remaining': [task.pk for task in tasks]}
                    for task_type, tasks in training_info['tasks'].items()
                }
            }

        return result
