# -*- coding: utf-8 -*-

from datetime import timedelta
from functools import reduce

from django.db.models import Avg, Count, F

from mtp.apps.accounts.models import MTPUser
from mtp.apps.eventlog.models import ACTION_CHOICES as EVENT_TYPES, EventLog
from mtp.apps.trainings.models import TrainingSession

from .constants import ONE_DAY, OUTGOING_DATE_FORMAT

# TODO: Move to another place


class DateAvg(Avg):
    """Avg subclass to returns an average duration in seconds."""

    def convert_value(self, value, expression, connection, context):
        if value is None:
            return value
        return value.total_seconds()


def day_range(start_date, end_date):
    """Returns an iterator to iterate through all the days for the given period.

    Both dates are *included* in the range.

    Parameters
    ----------
    start_date : date
        The first day to iterate on.
    end_date : date
        The last day to iterate on.

    Returns
    -------
    iterator
        Iterate over all the days (as ``datetime.date`` instance) between the given dates,
        including these two dates

    """
    # return _daterange(start_date, end_date)
    current_date = start_date
    delta = timedelta(days=1)
    while current_date <= end_date:
        yield current_date
        current_date += delta


def _populate_series(start_date, end_date, queryset):
    series = list()
    # Here we don't want the end_date, but ``day_range`` includes it.
    for current_date in day_range(start_date.date(), end_date.date() - timedelta(days=1)):
        series.append(next(
            (d for i, d in enumerate(queryset) if current_date in d.values()),
            {'date': current_date, 'nb_event': 0}
        ))
    return {
        'title': reduce(lambda x, y: x + y['nb_event'], series, 0),
        'graph': list(map(_format_serie, series)),  # pylint: disable=bad-builtin
    }


def _format_serie(serie):
    return {
        'date': serie['date'].strftime(OUTGOING_DATE_FORMAT),
        'nb_event': serie['nb_event']
    }


def _all_days_between(date_begin, date_end):
    day = date_begin
    while day <= date_end:
        yield day
        day += ONE_DAY


def count_registrations(date_begin, date_end):
    queryset = MTPUser.objects.filter(
        date_joined__range=(date_begin, date_end)
    ).extra(
        {'date': 'date(date_joined)'}
    ).values(
        'date'
    ).annotate(
        nb_event=Count('id', distinct=True)
    ).values(
        'date', 'nb_event'
    ).order_by()

    return _populate_series(date_begin, date_end, list(queryset))


def count_cms_connections(date_begin, date_end):
    queryset = EventLog.objects.filter(
        created_at__range=(date_begin, date_end), action=EVENT_TYPES.CMS_LOGIN
    ).extra(
        {'date': 'date(created_at)'}
    ).values(
        'date'
    ).annotate(
        nb_event=Count('id', distinct=True)
    ).values(
        'date', 'nb_event'
    ).order_by()

    return _populate_series(date_begin, date_end, list(queryset))


def count_document_downloads(date_begin, date_end):
    queryset = EventLog.objects\
        .filter(created_at__range=(date_begin, date_end), action=EVENT_TYPES.DOCUMENT_DOWNLOAD)

    queryset = queryset.extra(
        {'date': 'date(created_at)'}
    ).values(
        'date'
    ).annotate(
        nb_event=Count('id', distinct=True)
    ).values(
        'date', 'nb_event'
    ).order_by()

    return _populate_series(date_begin, date_end, list(queryset))


def _sessions_queryset(date_begin=None, date_end=None, training_id=None, trainer_id=None):
    # If a session took place and is now over, it means that we actually
    # computed the average tablet usage time, so it must not be `None` any
    # more. We can therefore filter training sessions according to this.

    sessions_queryset = TrainingSession.objects.filter(
        date__range=(date_begin, date_end),
        average_tablet_usage_time__isnull=False
    )

    # Filter for a specific trainer.
    if trainer_id is not None:
        sessions_queryset = sessions_queryset.filter(trainer_id=trainer_id)

    # Filter for a specific training.
    if training_id is not None:
        sessions_queryset = sessions_queryset.filter(training_id=training_id)

    return sessions_queryset


def count_actual_sessions(date_begin, date_end, training_id=None, trainer_id=None):
    sessions_queryset = _sessions_queryset(date_begin, date_end, training_id, trainer_id)

    sessions_queryset = sessions_queryset.extra(
        {'date': 'date(date)'}
    ).values(
        'date'
    ).annotate(
        nb_event=Count('id', distinct=True)
    ).values(
        'date', 'nb_event'
    ).order_by()

    return _populate_series(date_begin, date_end, list(sessions_queryset))


def count_actual_trainees(date_begin, date_end, training_id=None, trainer_id=None):
    sessions_queryset = _sessions_queryset(date_begin, date_end, training_id, trainer_id)

    events_queryset = EventLog.objects.filter(
        created_at__range=(date_begin, date_end),
        action=EVENT_TYPES.SESSION_JOIN
    )

    if len(sessions_queryset) > 0:
        events_queryset = events_queryset.filter(target_pk__in=sessions_queryset)

    events_queryset = events_queryset.extra(
        {'date': 'date(created_at)'}
    ).values(
        'date'
    ).annotate(
        nb_event=Count('id', distinct=True)
    ).values(
        'date', 'nb_event'
    ).order_by()

    return _populate_series(date_begin, date_end, list(events_queryset))


def compute_average_usage_time(date_begin, date_end, training_id=None, trainer_id=None):
    sessions_queryset = _sessions_queryset(date_begin, date_end, training_id, trainer_id)

    sessions_queryset = sessions_queryset.extra(
        {'date': 'date(date)'}
    ).values(
        'date'
    ).annotate(
        nb_event=Avg('average_tablet_usage_time')
    ).values(
        'date', 'nb_event'
    ).order_by()

    return _populate_series(date_begin, date_end, list(sessions_queryset))


def session_stats_by_trainer(date_begin, date_end, training_id=None):
    """Return a list of session statistics, grouped by trainer

    :param date_begin: datetime
    :param date_end: datetime
    :param training_id: str or unicode
    :returns: list
    """

    queryset = _sessions_queryset(
        date_begin,
        date_end,
        training_id
    ).values(
        'trainer__username'
    ).annotate(
        nb_sessions=Count('id', distinct=True)
    ).annotate(
        nb_trainees=Count('users', distinct=True)
    ).annotate(
        avg_session_duration=Avg('average_tablet_usage_time')
    ).annotate(
        username=F('trainer__username')
    ).values(
        'username', 'nb_sessions', 'nb_trainees', 'avg_session_duration'
    ).order_by()

    return list(queryset)


def session_stats_by_group_type(date_begin, date_end, training_id=None,
                                group_type_id=None):
    """Return a list of session statistics, filtered by group type

    :param date_begin: datetime
    :param date_end: datetime
    :param training_id: str or unicode
    :param group_type_id: str or unicode
    :returns: list
    """

    queryset = _sessions_queryset(
        date_begin,
        date_end,
        training_id
    ).values(
        'trainer__mgroups__name'
    ).annotate(
        nb_sessions=Count('id', distinct=True)
    ).annotate(
        nb_trainees=Count('users', distinct=True)
    ).annotate(
        avg_session_duration=Avg('average_tablet_usage_time')
    ).annotate(
        group_name=F('trainer__mgroups__name')
    ).annotate(
        group_type_id=F('trainer__mgroups__type__id')
    ).values(
        'group_type_id', 'group_name', 'nb_sessions', 'nb_trainees', 'avg_session_duration'
    ).filter(
        group_type_id=group_type_id
    ).values(
        'group_name', 'nb_sessions', 'nb_trainees', 'avg_session_duration'
    ).order_by()

    return list(queryset)
