import logging
from itertools import groupby
from operator import itemgetter

import waffle
from django.conf import settings
from django.contrib.postgres.search import SearchVector
from django.db import models
from django.db.models import Q, Count
from django.utils import timezone
from django.utils.translation import ugettext as _

from idm.closuretree.managers import CttManager
from idm.core.constants.node_relocation import RELOCATION_STATE
from idm.core.constants.role import ROLE_STATE
from idm.core.querysets.base import UnwrapMixin
from idm.core.workflow.exceptions import RoleNodeDoesNotExist
from idm.framework.backend.upsert import UpsertQueryMixin
from idm.framework.utils import add_to_instance_cache
from idm.nodes.updatable import StatefulQuerySet
from idm.notification.utils import report_problem
from idm.permissions.utils import get_permission
from idm.utils.i18n import get_lang_key
from idm.utils.sql import PrefixSearchQuery, RegexReplaceExpression

log = logging.getLogger(__name__)
TS_VECTOR = SearchVector(
    RegexReplaceExpression('slug', PrefixSearchQuery.SANITIZE_REGEXP, ' ', 'g'),
    RegexReplaceExpression('name', PrefixSearchQuery.SANITIZE_REGEXP, ' ', 'g'),
    RegexReplaceExpression('name_en', PrefixSearchQuery.SANITIZE_REGEXP, ' ', 'g'),
    config='simple',
)


def unique_preserve_order(iterable):
    unique = []
    seen = set()
    for item in iterable:
        if item not in seen:
            unique.append(item)
            seen.add(item)
    return unique


class RoleNodeQueryset(UnwrapMixin, UpsertQueryMixin, StatefulQuerySet):
    def public(self) -> 'RoleNodeQueryset':
        return self.filter(is_public=True)

    def get_alive_system_nodes(self, system) -> 'RoleNodeQueryset':
        return self.active().filter(system=system)

    def get_node_by_slug_path(self, system, slug_path) -> 'RoleNodeQueryset':
        nodes = self.get_alive_system_nodes(system)
        node = nodes.get(slug_path=slug_path)
        return node

    def get_all_nodes_by_value_path(self, system, value_path) -> 'RoleNodeQueryset':
        return self.filter(system=system, value_path=value_path, is_key=False)

    def text_search(self, query: str, with_aliases: bool = False) -> 'RoleNodeQueryset':
        """
        Добавляет Full Text Search по slug, name, name_en в случае если активен switch `rolenode_full_text_search`.
        Иначе поиск работает через ILIKE.
        В дополнение ILIKE по полям alias_name, alias_name_en если передан параметр with_aliases
        """
        search_q = Q()
        if with_aliases:
            aliases_q = Q(aliases__is_active=True)
            aliases_q &= Q(aliases__name__istartswith=query) | Q(aliases__name_en__istartswith=query)
            search_q |= aliases_q
        if waffle.switch_is_active('rolenode_full_text_search'):
            return (
                self.annotate(search=TS_VECTOR)
                .filter(search_q | Q(search=PrefixSearchQuery(query, config='simple')))
            )
        else:
            return self.filter(
                search_q | Q(slug__istartswith=query) | Q(name__istartswith=query) | Q(name_en__istartswith=query)
            )

    def order_by_with_l10n(self, fieldname: str, lang: str= None) -> 'RoleNodeQueryset':
        if lang is None:
            lang = get_lang_key()
        if lang != 'ru':
            fieldname = '%s_en' % fieldname
        return self.order_by(fieldname)

    def prefetch_for_hashing(self):
        return self.select_related('nodeset', 'system').prefetch_related('fields', 'aliases', 'responsibilities__user')

    def of_operational_system(self):
        return self.filter(system__is_broken=False, system__is_active=True)

    def deprived_nodes_with_returnable_roles(self, system=None):
        nodes = self.of_operational_system().filter(state='deprived')
        if system:
            nodes = nodes.filter(system=system)
        with_returnable_roles = nodes.filter(roles__state__in=ROLE_STATE.RETURNABLE_STATES).distinct()

        return with_returnable_roles

    def non_subscribable_pks(self):
        pks = []
        for system_slug, slug_paths in settings.IDM_SID67_EXCLUDED_NODES.items():
            nodes = self.filter(system__slug=system_slug, slug_path__in=slug_paths)
            pks.extend(list(nodes.values_list('pk', flat=True)))
        return pks

    def superdirty(self):
        return self.filter(relocation_state=RELOCATION_STATE.SUPERDIRTY)

    def ct_computed(self):
        return self.filter(relocation_state=RELOCATION_STATE.CT_COMPUTED)

    def feilds_computed(self):
        return self.filter(relocation_state=RELOCATION_STATE.FIELDS_COMPUTED)

    def good(self):
        return self.filter(relocation_state__isnull=True)

    def bad(self):
        return self.filter(relocation_state__isnull=False)

    def lock_by_select(self):
        return list(self.select_for_update().values_list('pk', flat=True))


class RoleNodeManager(models.Manager.from_queryset(RoleNodeQueryset), CttManager):
    """Менеджер для работы с моделью узла дерева ролей"""

    def get_node_by_data(self, system, data, for_request=False, select_related=None):
        """Возвращает RoleNode, если data определяет какую-либо роль, иначе выбрасывает исключение"""
        select_related = select_related or []
        nodes = self.get_alive_system_nodes(system).filter(is_key=False).exclude(level=0).select_related(*select_related)
        if for_request:
            nodes = nodes.select_related('parent', 'system')

        # Ловим DoesNotExist при обращении к data, если узел не существует - выбрасываем RoleNodeDoesNotExist
        try:
            node = nodes.get(data=data)
        except self.model.DoesNotExist as e:
            data_repr = '{%s}' % ', '.join('%s: %s' % (key, value) for key, value in sorted(data.items()))
            raise RoleNodeDoesNotExist(_('Указан несуществующий узел: %s') % data_repr)
        return node

    def get_node_by_value_path(self, system, values_path):
        nodes = self.get_alive_system_nodes(system).filter(is_key=False)
        node = nodes.get(value_path=values_path)
        add_to_instance_cache(node, 'system', system)
        return node

    def get_node_by_unique_id(self, system, unique_id, include_depriving=False):
        node = None
        if include_depriving:
            nodes = (self.filter(system=system, unique_id=unique_id, state__in=['active', 'depriving'])
                     .order_by('state', '-updated_at'))
            if len(nodes) >= 1:
                node = nodes[0]
        else:
            nodes = self.get_alive_system_nodes(system).filter(unique_id=unique_id)
            try:
                node = nodes.get()
            except (self.model.DoesNotExist, self.model.MultipleObjectsReturned):
                pass
        return node

    def get_permitted_query(self, user, permission, system=None, for_view=False):
        """Возвращает запрос со списком нод (включая дочерние), к которым у пользователя есть нужный пермишен,
        с учетом InternalRole.
        """
        from idm.core.models import RoleNodeClosure
        permission_obj = get_permission(permission)

        speed_up_q = ~Q(parent__system__request_policy='anyone') if for_view else Q()

        closures = RoleNodeClosure.objects.filter(
            speed_up_q,
            parent__internal_roles__user_object_permissions__user=user,
            parent__internal_roles__user_object_permissions__permission=permission_obj,
        )

        if system:
            closures = closures.filter(parent__system=system)

        closures = closures.values_list('child_id', flat=True)
        return closures

    def get_owners_query(self, node, owning_slug, include_self=True):
        """
        Выбираем узлы на 3 или больше уровня вверх от текущего, а потом выбираем все узлы, которые имеют
        slug, равный owning_slug, и являются потомками этих узлов, но не более глубокими, чем на 3 уровня вглубь
        """

        cut_level = node.level - 3
        if not include_self:
            # Если мы не включаем текущий уровень, то нужно исключить из рассмотрения узел, который контролирует текущий
            # Уровень, который контролирует текущий, расположен на 3 уровня вверх от него
            # Проще всего исключить его, добавив условие по уровню чуть жёстче
            cut_level -= 2

        owned_ancestors = (
            node.get_ancestors(include_self=False).filter(is_key=True, level__lte=cut_level).order_by('-level')
        )
        owner_nodes = []
        for ancestor in owned_ancestors:
            descendants = ancestor.get_descendants(exact_depth=3).active()
            # условие по level важно, это дескриминирующий признак, приводящий к использованию другого индекса
            # см. IDM-5016
            descendants = descendants.filter(slug=owning_slug, level=ancestor.level + 3)
            owner_nodes.extend(descendants)

        return owner_nodes

    def get_owners(self, node, owning_slug, look_up=False, collect=False, groups=False, include_self=True):
        from idm.core.models import Role

        owner_nodes = self.get_owners_query(node, owning_slug, include_self)
        identifiers = []

        for owner_node in owner_nodes:
            roles = Role.objects.get_public_roles().active().filter(node=owner_node)
            if groups:
                idents = roles.filter(user=None).values_list('group__external_id', flat=True).distinct().order_by(
                    'group__external_id')
            else:
                idents = roles.filter(group=None).values_list('user__username', flat=True).distinct().order_by(
                    'user__username')
            idents = list(idents)
            if not look_up:
                return idents
            elif not collect:
                if idents:
                    return idents
            else:
                identifiers.extend(idents)
        return unique_preserve_order(identifiers)

    def poke_hanging_deprived_nodes_with_returnable_roles(self, system=None):
        nodes = self.deprived_nodes_with_returnable_roles(system)
        for node in nodes:
            node.deprive_roles_async(reason='Retrying to remove hanging roles')

    def send_reminders(self, threshold=None):
        from idm.core.models import System
        if threshold is None:
            threshold = settings.IDM_DEPRIVING_NODE_ROLES_THRESHOLD
        res = (self.filter(state='depriving', deprive_emailed_at__isnull=True, roles__is_active=True)
               .values('value_path', 'system_id').order_by('id')
               .annotate(roles_num=Count('roles')).filter(roles_num__gte=threshold)
               .values_list('value_path', 'roles_num', 'system_id', 'pk').order_by('system_id'))
        for system_id, rows in groupby(res, itemgetter(2)):
            system = System.objects.get(pk=system_id)
            rows = list(rows)
            nodes = [row[:2] for row in rows]
            log.warning("System %s: %d nodes with many active nodes are being deprived (sending email)", system.slug,
                        len(nodes))

            self.send_expiring_nodes_reminder(system, nodes)
            self.filter(pk__in=map(itemgetter(3), rows)).update(deprive_emailed_at=timezone.now())

    @staticmethod
    def send_expiring_nodes_reminder(system, nodes):
        subject = _('Удалённые узлы в системе "%s"') % system.get_name()
        context = {'system': system, 'nodes': nodes}
        report_problem(subject, ['emails/depriving_nodes.txt'], context, system=system, fallback_to_reponsibles=True)
