# coding: utf-8


import collections
import logging
import random
from itertools import groupby
from operator import attrgetter
from typing import Set

from django.db import transaction
from django.db import models
from django.conf import settings

from idm.core.constants.email_templates import NOT_FULLY_REGISTERED_PASSPORT_LOGINS_TEMPLATES
from idm.core.constants.groupmembership import GROUPMEMBERSHIP_STATE
from idm.core.constants.passport_login import PASSPORT_LOGIN_STATE
from idm.core.constants.role import ROLE_STATE, NOT_NOTIFY_ON_REGISTER_PASSPORT_LOGINS_TO
from idm.core.constants.rolefield import FIELD_TYPE
from idm.core.exceptions import PassportLoginGenerationError, MultiplePassportLoginUsersError
from idm.core.workflow.common.subject import subjectify
from idm.notification.utils import send_reminder
from idm.sync import passport
from idm.utils.queryset import queryset_iterator

log = logging.getLogger('idm.core.querysets')


class UserPassportLoginQuerySet(models.QuerySet):
    def with_active_subscribable_roles(self, system=None):
        from idm.core.models import System, RoleNode, Role

        if system:
            available_systems_pks = [system.pk] if system.slug not in settings.IDM_SID67_EXCLUDED_SYSTEMS else []
        else:
            available_systems_pks = System.objects.exclude(
                slug__in=settings.IDM_SID67_EXCLUDED_SYSTEMS
            ).values_list('pk', flat=True)

        bad_nodes = RoleNode.objects.non_subscribable_pks()

        subscribable_roles = (
            Role.objects
            .filter(
                passport_logins__pk__in=self.values_list('pk', flat=True),
                system__is_active=True,
                system__pk__in=available_systems_pks,
                is_active=True,
            )
            .exclude(node__pk__in=bad_nodes)
            .values_list('pk', flat=True)
        )
        qs = self.filter(roles__pk__in=subscribable_roles)
        return qs

    def to_subscribe(self, system=None, force=False):
        states = [PASSPORT_LOGIN_STATE.CREATED, PASSPORT_LOGIN_STATE.UNSUBSCRIBED]
        if force:
            states.append(PASSPORT_LOGIN_STATE.SUBSCRIBED)
        return (
            self
            .filter(state__in=states)
            .with_active_subscribable_roles(system=system)
            .select_related('user')
        )

    def to_unsubscribe(self):
        states = [PASSPORT_LOGIN_STATE.CREATED, PASSPORT_LOGIN_STATE.SUBSCRIBED]
        subscribable = self.with_active_subscribable_roles()
        return (
            self
            .filter(state__in=states)
            .exclude(pk__in=subscribable.values_list('pk', flat=True))
            .select_related('user')
        )

    def subscribed(self):
        return self.filter(state=PASSPORT_LOGIN_STATE.SUBSCRIBED)


class UserPassportLoginManager(models.Manager.from_queryset(UserPassportLoginQuerySet)):
    def add(self, login, role=None, user=None):
        assert role or user, "Role or user should be passed."
        if user is None:
            user = role.user
        assert user is not None
        log.info('Adding passport login <%s> for user <%s>' % (login, user.username))
        try:
            with transaction.atomic():
                passport_login, created = self.get_or_create(login=login, defaults={'user': user})
                passport_login.fetch_user()
                if passport_login.user and user != passport_login.user:
                    if role is not None:
                        raise MultiplePassportLoginUsersError(
                            'Can\'t add passport login to role %s to user %s, login %s already belongs to %s'
                            % (role.pk, role.user.username, login, passport_login.user.username)
                        )
                    else:
                        raise MultiplePassportLoginUsersError(
                            'Can\'t create passport login for user %s, login %s already belongs to %s'
                            % (user.username, login, passport_login.user.username)
                        )
                if role is not None:
                    passport_login.roles.add(role)
                return passport_login
        except Exception:
            log.exception('During linking existing passport login <%s> with role <%d>', login, role.pk)
            raise

    def update_from_roles(self):
        from idm.core.models import Role, RoleField
        systems_with_logins = list(RoleField.objects.filter(
            is_active=True,
            type=FIELD_TYPE.PASSPORT_LOGIN,
        ).values_list('node__system_id', flat=True))

        all_logins = self.select_related('user')
        existing_passport_logins = collections.defaultdict(dict)
        for passport_login in all_logins:
            existing_passport_logins[passport_login.user.username][passport_login.login] = passport_login

        # проходим по всем ролям, в том числе и неактивным
        roles = (
            Role.objects
            .filter(
                user__isnull=False,
                system__id__in=systems_with_logins
            )
            .select_related('user')
        )
        for role in queryset_iterator(roles):
            user = role.user
            sys_specific = role.system_specific if isinstance(role.system_specific, dict) else {}

            login = sys_specific.get('passport-login', '').lower().strip()
            if not login:
                continue

            existing_logins = existing_passport_logins.get(user.username, {})
            if login not in existing_logins:
                try:
                    self.add(login, role)
                except Exception:
                    # Получили какую-то ошибку, но попробуем обработать остальные роли
                    # раньше все исключения перехватывались в вызываемом методе self.add()
                    log.exception('Can\'t add passport login %s to role %s' % (login, role.id))
            else:
                try:
                    with transaction.atomic():
                        existing_logins[login].roles.add(role)
                except Exception:
                    log.exception('During linking existing passport login <%s> with role <%d>', login, role.pk)

    def subscribe_logins(self, system=None, force=False, threshold=None, logins=None):
        """Подписывает логины 67-ым сидом"""

        passport_logins = self.filter(login__in=logins) if logins else self.to_subscribe(system=system, force=force)

        if threshold is None:
            threshold = settings.IDM_SID67_THRESHOLD
        if not logins and passport_logins.distinct().count() > threshold:
            log.error('Threshold for sid67 is exceeded. %s is greater then %s', passport_logins.count(), threshold)
            return False
        for passport_login in passport_logins:
            passport_login.subscribe()
        return True

    def unsubscribe_logins(self):
        """Снимает подписку с логинов без активных ролей"""

        passport_logins = self.to_unsubscribe()
        for passport_login in passport_logins:
            passport_login.unsubscribe()

    def filter_valid(self, logins: Set[str], logins_to_exclude: Set[str]) -> Set[str]:
        return {
            login
            for login in logins
            if len(login) <= settings.MAX_PASSPORT_LOGIN_LENGTH
                and login not in logins_to_exclude
                and not passport.exists(login)
        }

    def generate_logins_by_roles_tree(
            self,
            base_login: str,
            system: 'System',
            logins_to_exclude: Set[str] = None,
    ) -> Set[str]:
        """
        Генерирует логины без учета конкретной роли
        """
        logins_to_exclude = logins_to_exclude or set()
        role_nodes = (system.root_role_node.get_descendants().filter(is_key=False,
                                                                     state__in=system.root_role_node.ACTIVE_STATES).
                      select_related('parent'))

        paths = {system.root_role_node.pk: base_login}
        for role_node in role_nodes:
            paths[role_node.pk] = '%s-%s' % (paths[role_node.parent.parent_id],
                                             role_node.slug.lower().replace('_', '-'))
        result = {path.lower().replace('_', '-') for path in paths.values()}
        result = self.filter_valid(result, logins_to_exclude)
        return result

    def generate_logins_by_role_node(
            self,
            base_login: str,
            role_node: 'RoleNode',
            logins_to_exclude: bool = None,
    ) -> Set[str]:
        """
        Генерирует логины с учетом role_node. Для логина username и роли system -> manager предлагает варианты:

          * yndx-username
          * yndx-username-manager
          * yndx-username-system-manager

        Если последние два логина заняты, к ним прибавляется окончание "-1", "-2" и т. д.
        Если логины слишком длинные, то генерятся на основе системы
          * yndx-username-system
        """
        logins_to_exclude = logins_to_exclude or set()
        result = {base_login}
        role_path = [slug.lower().replace('_', '-') for slug in role_node.as_split_path()]
        suffixes = {
            '-'.join(role_path),  # путь-до-роли
            role_path[-1]  # конечнаяроль
        }
        endings = [''] + ['-%d' % digit for digit in range(1, 100)]
        for suffix in suffixes:
            for ending in endings:
                login = '%s-%s%s' % (base_login, suffix, ending)
                if login not in logins_to_exclude:
                    result.add(login)
                    break  # Прекращаем перебирать окончания, но не суффиксы
            else:
                # Перебрали 99 окончаний, все логины оказались занятыми. Последняя попытка.
                # Проверять занятость логина не надо, так как занятые отфильтруются вне функции.
                result.add('%s-%s-%s' % (base_login, suffix, random.randint(100, 1000000)))

        result = self.filter_valid(result, logins_to_exclude)
        return result

    def generate_login_by_system(self, base_login: str, system: 'System', logins_to_exclude: bool = None) -> Set[str]:
        """Генерирует логины по слагу системы, добавляя в конце число"""
        logins_to_exclude = logins_to_exclude or set()
        generated_logins = set()
        endings = [''] + ['-%d' % digit for digit in range(1, 100)]
        for ending in endings:
            login = '%s-%s%s' % (base_login, system.slug.replace('_', '-'), ending)
            if self.filter_valid({login}, logins_to_exclude):
                generated_logins.add(login)
                break
        return generated_logins

    def generate_sequentially(self, base_login, logins_to_exclude=None):
        """Генерирует логины как базовый логин + число (последовательно)"""
        logins_to_exclude = logins_to_exclude or set()
        for ending in range(1, 1000):
            login = '%s-%s' % (base_login, ending)
            if self.filter_valid([login], logins_to_exclude):
                return {login}
        return set()

    def generate_random(self, base_login, logins_to_exclude=None):
        """Генерирует логины как базовый логин + число (случайно)"""
        logins_to_exclude = logins_to_exclude or set()
        logins = {'%s-%s' % (base_login, random.randint(100, 1000000))}
        logins = self.filter_valid(logins, logins_to_exclude)
        return logins

    def get_available_login(self, user):
        base_login = 'yndx-{username}'.format(username=user.username)
        passport_logins = [base_login] + sorted(self.generate_sequentially(base_login))
        for login in passport_logins:
            if not passport.exists(login):
                return login
        raise PassportLoginGenerationError(
            'All auto-generated passport_logins for user %s already exist' % user.username
        )

    def _get_filter_to_reminders_about_not_fully_registered_logins(self):
        from idm.core.models import Role

        groups_id = Role.objects.get_need_attach_passport_login_groups_id_in_aware_for_membership_systems()

        is_fully_registered_query = models.Q(is_fully_registered=False)
        # Выбираем только роли, выданные позже внедрения функционала (позже 27.03.2019)
        returnable_roles_query = models.Q(
            roles__state__in=ROLE_STATE.RETURNABLE_STATES,
            roles__added__gt=NOT_NOTIFY_ON_REGISTER_PASSPORT_LOGINS_TO,
        )
        memberships_query = models.Q(
            group_memberships__group_id__in=groups_id,
            group_memberships__state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES,
        )

        query = is_fully_registered_query & (returnable_roles_query | memberships_query)
        return query

    def send_reminders_about_not_fully_registered_logins(self):

        query = self._get_filter_to_reminders_about_not_fully_registered_logins()
        passport_logins = self.filter(query).select_related('user').distinct()

        users_to_reminders = groupby(passport_logins.order_by('user__center_id'), attrgetter('user'))

        for user, logins in users_to_reminders:
            subject = subjectify(user)
            logins_to_reminder = [login.login for login in logins]
            log.info(
                'Sending reminder about not fully registered logins %(subject)s' % {'subject': subject.get_ident()}
            )
            try:
                send_reminder(subject, NOT_FULLY_REGISTERED_PASSPORT_LOGINS_TEMPLATES, allow_external_recepients=True,
                              logins=logins_to_reminder)
            except Exception:
                log.exception(
                    'Error while trying to send reminder about not fully registered passport_logins to the %s'
                    % subject.get_ident()
                )
