import bisect
import logging
import requests

from datetime import timedelta
from functools import cached_property

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models import Count, Q, F

from intranet.femida.src.candidates.models import Reference
from intranet.femida.src.offers.choices import OFFER_STATUSES
from intranet.femida.src.offers.models import Offer
from intranet.femida.src.utils.pluralization import get_russian_plural_form


class AchievementsError(Exception):
    pass


logger = logging.getLogger(__name__)
User = get_user_model()

INTERVIEW_COUNTER_THRESHOLDS = [10, 25, 50, 100, 200, 500, 1000, 2000]


class AchievementsAPI:

    def _process_request(self, method_name, url, params=None, data=None):
        params = params or {}
        data = data or {}
        headers = {
            'Authorization': 'OAuth %s' % settings.FEMIDA_ROBOT_TOKEN,
        }
        try:
            response = requests.request(
                method=method_name,
                url=url,
                params=params,
                data=data,
                headers=headers,
                verify=settings.YANDEX_INTERNAL_CERT_PATH,
                timeout=settings.STAFF_TIMEOUT,
            )
        except requests.exceptions.RequestException:
            logging.exception('Staff achievements API is not responding')
            raise AchievementsError

        try:
            data = response.json()
        except ValueError:
            logging.exception('Error during parse json from API achievements API')
            raise AchievementsError

        if not response.ok:
            logging.error(
                'Staff achievements API responded with status `%d`: %s',
                response.status_code, data
            )
            return

        return data

    def _get(self, login, achievement_id):
        params = {
            'person.login': login,
            'achievement.id': achievement_id,
            '_fields': 'id,is_active,revision,level,comment',
        }
        url = settings.STAFF_ACHIEVEMENTS_API_URL + 'given/'
        data = self._process_request('get', url=url, params=params)
        if data['total'] > 0:
            achievement = data['objects'][0]
            return achievement

    def _edit(self, given_achievement_id, **data):
        url = '{base_url}given/{achievement_id}/'.format(
            base_url=settings.STAFF_ACHIEVEMENTS_API_URL,
            achievement_id=given_achievement_id,
        )
        return self._process_request('put', url=url, data=data)

    def _create(self, login, achievement_id, level, comment=''):
        data = {
            'person.login': login,
            'achievement.id': achievement_id,
            'level': level,
            'comment': comment,
        }
        url = settings.STAFF_ACHIEVEMENTS_API_URL + 'given/'
        return self._process_request('post', url=url, data=data)

    def give(self, login, achievement_id, level=-1, comment=''):
        """
        Если ачивки нет, то создаем ее.
        Если ачивка есть и она в статусе is_active=False, то меняем статус на True.
        Если у ачивки не совпадает level, то меняем его.
        """
        given_achievement = self._get(login, achievement_id)

        if given_achievement is None:
            self._create(login, achievement_id, level, comment)
            logger.info('Achievement `%d` was given to %s', achievement_id, login)
            return

        updated = False

        if given_achievement['is_active'] is False:
            given_achievement = self._edit(
                given_achievement_id=given_achievement['id'],
                is_active=True,
                revision=given_achievement['revision'],
            )
            if given_achievement is None:
                return
            updated = True

        if level != given_achievement['level'] or comment != given_achievement['comment']:
            self._edit(
                given_achievement_id=given_achievement['id'],
                level=level,
                comment=comment,
                revision=given_achievement['revision'],
            )
            updated = True

        if not updated:
            logger.warning(
                'Achievement `%d` is already given to `%s`',
                achievement_id, login,
            )
        else:
            logger.info('Achievement `%d` was updated for %s', achievement_id, login)

    def take_away(self, login, achievement_id):
        """
        Если ачивки нет, то ничего не делаем.
        Если есть и в статусе is_active=False, то ничего не делаем.
        Если есть и в статусе is_active=True, то меняем статус на False
        """
        given_achievement = self._get(login, achievement_id)
        if given_achievement is None:
            logger.warning(
                'Achievement `%d` was not given to `%s`'
                'There is nothing to take away', achievement_id, login
            )

        elif given_achievement['is_active'] is False:
            logger.warning(
                'Achievement `%d` is already inactive for `%s`',
                achievement_id, login
            )

        else:
            self._edit(
                given_achievement_id=given_achievement['id'],
                is_active=False,
                revision=given_achievement['revision'],
            )


def give_interview_counter_achievements(logins=None, strict=True):
    users = (
        User.objects
        .filter(
            is_dismissed=False,
            interviews__state='finished',
            interviews__type__in=(
                'screening',
                'regular',
                'aa',
            ),
        )
    )
    if logins is not None:
        users = users.filter(username__in=logins)

    interview_counts = (
        users
        .annotate(interviews_count=Count('interviews'))
        .filter(interviews_count__gte=INTERVIEW_COUNTER_THRESHOLDS[0])
        .values_list('username', 'interviews_count')
    )

    error_count = success_count = 0
    unprocessed_usernames = []
    for username, count in interview_counts:
        level = bisect.bisect(INTERVIEW_COUNTER_THRESHOLDS, count)
        comment = 'Собеседующий {level}-го уровня.\nПроведено {count} секций в Фемиде.'.format(
            level=level,
            count=INTERVIEW_COUNTER_THRESHOLDS[level - 1],
        )
        try:
            AchievementsAPI().give(
                login=username,
                achievement_id=settings.STAFF_INTERVIEW_COUNTER_ACHIEVEMENT_ID,
                level=level,
                comment=comment,
            )
        except AchievementsError as exc:
            if strict:
                raise exc
            else:
                error_count += 1
                unprocessed_usernames.append(username)

        else:
            success_count += 1

    return success_count, error_count, unprocessed_usernames


class BaseAchievement:
    achievement_id = None

    def __init__(self, strict=True):
        self._strict = strict

    def give_all_delay(self):
        pass

    def give_all(self):
        unprocessed = []
        for username, count in self._collection():
            if not self._give_one(username, count):
                unprocessed.append(username)
        return unprocessed

    def _give_one(self, username, level):
        try:
            AchievementsAPI().give(
                login=username,
                achievement_id=self.achievement_id,
                level=level,
                comment=self._comment(level),
            )
        except AchievementsError as exc:
            if self._strict:
                raise exc
            else:
                return False

        return True

    def _collection(self):
        return []

    @staticmethod
    def _comment(_n):
        return ''

    @staticmethod
    def _append_usernames_filter(base_q, username_field, usernames):
        if usernames is not None:
            return base_q & Q(**{f'{username_field}__in': usernames})
        else:
            return base_q

    @staticmethod
    def _add_pretty_number(level, template):
        strings = ['одного', 'двух', 'трёх', 'четырёх']
        number = strings[level - 1] if level < 5 else f'{level}'
        return template.format(number=number)


class DontGiveAnyAchievement(BaseAchievement):
    """
    Ачивка-заглушка, чтобы не делать проверок на None там,
    где пытаемся выдать ачивку, если её выдавать не нужно.
    Ничего никому не выдаем.
    """
    def __init__(self, *args, **kwargs):
        super().__init__()


class ApprovedReferenceAchievement(BaseAchievement):
    """
    Зеленая ачивка
    """
    achievement_id = settings.STAFF_APPROVED_REFERENCE_ACHIEVEMENT_ID

    def __init__(self, usernames=None, **kwargs):
        super().__init__(**kwargs)
        self._usernames = usernames

    def give_all_delay(self):
        from intranet.femida.src.staff.tasks import give_approved_reference_counter_achievement

        return give_approved_reference_counter_achievement.delay(self._logins)

    def _collection(self):
        username_field = 'created_by__username'
        references_count = (
            Reference.objects.filter(self._get_q())
            .values(username_field)
            .annotate(count=Count(username_field))
        )

        for reference in references_count:
            yield reference[username_field], reference['count']

    def _get_q(self):
        result_q = Q(status__in=['approved', 'approved_without_benefits'])
        return self._append_usernames_filter(result_q, 'created_by__username', self._logins)

    @staticmethod
    def _comment(level):
        who = get_russian_plural_form(
            level,
            'классного профессионала',
            'классных профессионалов',
            'классных профессионалов'
        )
        return BaseAchievement._add_pretty_number(level, f'Порекомендовал {{number}} {who}')

    @property
    def _logins(self):
        return self._usernames


class AcceptedOfferReferenceAchievement(ApprovedReferenceAchievement):
    """
    Фиолетовая ачивка
    """
    achievement_id = settings.STAFF_ACCEPTED_OFFER_REFERENCE_ACHIEVEMENT_ID

    def __init__(self, offer_id=None, **kwargs):
        super().__init__(**kwargs)
        self._offer_id = offer_id

    def give_all_delay(self):
        from intranet.femida.src.staff.tasks import give_accepted_offer_reference_counter_achievement

        return give_accepted_offer_reference_counter_achievement.delay(self._offer_id)

    def _get_q(self):
        # relativedelta не поддерживается в запросах, а timedelta поддерживатеся.
        # В timedelta максимальный квант - 1 день.
        # Поэтому считаем, что полгода - это 365,2425/2 = 182.62125 = 183 дня.
        return super()._get_q() & Q(
            submission__candidate__offers__status=OFFER_STATUSES.closed,
            submission__candidate__offers__created__gt=F('created'),
            submission__candidate__offers__closed_at__lte=F('created') + timedelta(days=183),
        )

    @staticmethod
    def _comment(level):
        who = get_russian_plural_form(level, 'друга', 'друзей', 'друзей')
        return BaseAchievement._add_pretty_number(level, f'Помог трудоустроить в Яндекс {{number}} {who}')

    @property
    def _logins(self):
        return self._offer_logins if self._offer_id else super()._logins

    @cached_property
    def _offer_logins(self):
        username_key = 'created_by__username'
        offer = Offer.unsafe.get(id=self._offer_id)
        references = Reference.objects.filter(submission__candidate_id=offer.candidate_id)
        usernames = {x[username_key] for x in references.values(username_key)}
        return list(usernames)
