from collections import defaultdict
from decimal import Decimal

from django.conf import settings
from django.db import models
from django.db.models import Q
from django.utils import timezone
from django.utils.six import text_type

from staff.lib.tasks import run_task
from staff.person.models import Staff
from .models import Stat, Log
from .tasks import AchKudosGivenTask, MailKudosGivenTask

ADD_DELAY = timezone.timedelta(hours=24)
"""Отсрочка возможности повторной благодарности."""


class AddKudoController(object):
    """Отвественнен за логику добавления благодарности."""

    @staticmethod
    def calculate_power(issuer, recipients):
        """Вычисляет силу благодарности.

        :param Staff issuer: Благодарящий.
        :param list[Staff] recipients: Благодаримые.
        :param str|str message: Заметка к благодарности.

        """
        issuer_score = (
           Stat.objects
           .filter(person=issuer)
           .values_list('score', flat=True)
           .first()
        )

        # Базовая сила благодарности дающего пользователя равна
        # двоичному логарифму полученной им от других благодарности + 1.
        issuer_power = ((issuer_score.ln() / Decimal('2').ln()) if issuer_score else 0) + 1
        # Если благодарность раздаётся пакетно, то сила делится
        # на количество получающих.
        issuer_power = (issuer_power / len(recipients)) or 1

        return issuer_power

    @classmethod
    def add_kudo(cls, issuer, recipients, message='', notify=False):
        """Добавляет благодарность. Возвращает количество добавленных записей в журнал.

        :param Staff issuer: Благодарящий.
        :param list[str|str] recipients: Логины благодаримых.
        :param str|str message: Заметка к благодарности.
        :param bool notify: Следует ли оповестить получающих письмом о факте получения.

        :rtype: int

        """
        recipients = filter_recipients(recipients, issuer)  # type: list[Staff]

        if not recipients:
            return 0

        issuer_power = cls.calculate_power(issuer=issuer, recipients=recipients)
        message = text_type(message or '')
        log_entries = []
        recipients_ids = []

        for recipient in recipients:
            recipients_ids.append(recipient.id)

            log_entries.append(Log(
                issuer=issuer,
                recipient=recipient,
                power=issuer_power,
                message=message,
            ))

        # Пакетная запись в журнал.
        Log.objects.bulk_create(log_entries)

        objects_stat = Stat.objects

        uninitialized = set(recipients_ids).difference(
            objects_stat
            .filter(person_id__in=recipients_ids)
            .values_list('person_id', flat=True)
        )

        if uninitialized:
            # Среди благодаримых есть те, для кого ещё не созданы
            # записи счётчиков.
            objects_stat.bulk_create([
                Stat(person_id=uninitialized_id)
                for uninitialized_id in uninitialized
            ])

        # Пакетное обновление счётчиков пользователей.
        (
            objects_stat
            .filter(person__in=recipients)
            .update(score=models.F('score') + issuer_power)
        )

        if notify:
            cls.notify(log_entries=log_entries)

        if settings.ACHIEVEMENT_KUDOS_GIVER_ID:
            cls.give_achievement(issuer=issuer)

        return len(log_entries)

    @staticmethod
    def notify(log_entries):
        """Отправляет оповещение о выдаче балгодарности.

        :param Staff issuer: Благодарящий.
        :param list[Log] log_entries: Благодарящий.

        """
        run_task(
            MailKudosGivenTask,
            log_ids=[log_entry.id for log_entry in log_entries],
            countdown=60,
        )

    @staticmethod
    def give_achievement(issuer):
        """Выдаёт достижение за благодарность.

        :param Staff issuer: Благодарящий.

        """
        run_task(AchKudosGivenTask, persons_ids=[issuer.id], countdown=30)


class ViewKudoController(object):
    """Ответственнен за логику получения информации о благодарностях."""

    @staticmethod
    def get_available(recipients, issuer):
        """Для указанных логинов пользователей возвращает множество,
        с теми из них, которых может поблагодарить указанный благодарящий.

        :param Staff issuer: Благодарящий.
        :param list[str|str] recipients: Логины благодаримых.

        :rtype: List[Staff]

        """
        return [recipient.login for recipient in filter_recipients(recipients, issuer)]

    @staticmethod
    def get_stats(users):
        """Возвращает список объектов статистики благодарностей
        для указанных пользователей.

        :param list[str|str] users: Логины пользователей, для которых
            требуется получить записи статистики.

        :rtype: list[dict]

        """
        result = (
            Stat.objects
            .filter(person__login__in=users)
            .select_related('person__login')
            .values('person__login', 'score')
        )

        return list(result)

    @staticmethod
    def get_log(users, given, taken):
        """Возвращает кортеж со словарями выданных и полученных объектов
        истории благодарностей для указанных пользователей.

        :param list[str|str] users:
        :param bool given: Получить записи журнала о выдаче благодарностей
        :param bool taken: Получить записи журнала о получении благодарностей

        :rtype: tuple[dict[str|str, Log], dict[str|str, Log]]

        """
        assert any((given, taken)), "Both 'given' and 'taken' my not be False"

        users = set(users)

        query_filter = Q()

        if taken:
            query_filter |= Q(recipient__login__in=users)

        if given:
            query_filter |= Q(issuer__login__in=users)

        log_entries = list(
            Log.objects
            .filter(query_filter)
            .select_related(
                'issuer__login',
                'recipient__login'
            )
            .values(
                'created_at',
                'power',
                'message',
                'issuer__login',
                'recipient__login'
            )
            .order_by('-created_at')
        )

        def gather_entries(grouper):
            """Собирает из журнала записи, отсноящиеся к указанному
            полю группировки.

            :param str|str grouper: Поле группировки (логин отправителя/получателя).
            :rtype: dict
            """
            gathered = defaultdict(list)

            for log_entry in log_entries:
                login = log_entry[grouper]
                if login in users:
                    gathered[login].append(log_entry)

            return gathered

        result = (
            gather_entries('issuer__login') if given else {},
            gather_entries('recipient__login') if taken else {},
        )

        return result


def filter_recipients(recipients, issuer):
    """Фильтрует получателей благодарностей.

    :param list[str|str]|str|str recipients: Логины получателей благодарностей.
    :param Staff issuer: Благодаряющий.

    :rtype: list[Staff]

    """
    # Уволенные и виртуальные ползователи никого не благодарят.
    if issuer.is_dismissed or issuer.is_robot:
        return []

    if not isinstance(recipients, list):
        recipients = [recipients]

    recipients = (
        Staff.objects
        .filter(login__in=recipients)
    )

    filtered_out = get_delayed_recipients(issuer=issuer, recipients=recipients)
    filtered_out.add(issuer.login)

    recipients_filtered = [
        recipient
        for recipient in recipients
        if not recipient.is_dismissed and recipient.login not in filtered_out]

    return recipients_filtered


def get_delayed_recipients(issuer, recipients):
    """Возвращает логины получателей благодарностей,
    которым пока рано получать повторную благодарность от указанного дающий.

    :param Staff issuer: Благодаряющий.
    :param models.QuerySet recipients: Благодаримые.

    :rtype: set

    """
    # Если планировщику не понравится, то для PG должно быть
    # достаточно упорядочить убывающему по времени создания
    # и .distinct(), но нужно будет придумать что-то с sqlite.
    delayed = (
        Log.objects
        .filter(
            created_at__gte=timezone.now() - ADD_DELAY,
            issuer=issuer,

            # Упрощаяем SQL.
            recipient_id__in={recipient.id for recipient in recipients}
        )
        .values_list('recipient_id', flat=True)
    )

    if len(recipients) == 1:
        # Чтобы чуть ускориться: для единичного сотрудника
        # достаточно единственной записи.
        delayed = delayed[:1]

    delayed = set(delayed)

    return {recipient.login for recipient in recipients if recipient.id in delayed}
