# coding: utf-8


import itertools
from collections import defaultdict

from django.db.models import Prefetch
from django.utils import six

from idm.core.constants.workflow import PRIORITY_POLICIES, DEFAULT_PRIORITY
from idm.core.workflow.exceptions import GroupHasNoParentError, GroupDoesNotExist, NoSuchPriorityPolicyError, NoGroupError
from idm.core.workflow.plain.mixins import TreeMixin, SubjectWrapperMixin
from idm.core.workflow.plain.user import userify
from idm.framework.mixins import Representable
from idm.users import ranks as ranks_const
from idm.users.constants.group import GROUP_TYPES
from idm.users.models import Group, GroupResponsibility


def groupify(thing):
    if thing is None:
        result = thing
    elif isinstance(thing, GroupWrapper):
        result = thing
    elif isinstance(thing, Group):
        result = GroupWrapper(thing)
    else:
        params = {}
        if isinstance(thing, six.integer_types):
            params['external_id'] = thing
        else:
            params['slug'] = thing
        try:
            result = GroupWrapper(Group.objects.user_groups().active().get(**params))
        except Group.DoesNotExist:
            raise NoGroupError('Подразделение {0} не существует.'.format(thing))
    return result


class ChainLevel(list):
    def __init__(self, level, items):
        self.level = level
        super(ChainLevel, self).__init__(items)


def get_head_and_deputy_priorities(current_priority, priority_policy):
    head_priority, deputy_priority = current_priority, current_priority
    if priority_policy == PRIORITY_POLICIES.DEPUTY_LESS:
        head_priority += 1
    elif priority_policy == PRIORITY_POLICIES.HEAD_LESS:
        deputy_priority += 1
    return head_priority, deputy_priority


class GroupWrapper(Representable, SubjectWrapperMixin, TreeMixin):
    """Класс-обертка для группы, предоставляющая некоторые методы."""

    object_attribute_name = 'group'
    no_parent_exception = GroupHasNoParentError
    userify_func = staticmethod(userify)
    _user_wrapper = None
    chain_level_class = ChainLevel

    @classmethod
    def get_user_wrapper(cls):
        if cls._user_wrapper is None:
            from idm.core.workflow.plain.user import UserWrapper
            cls._user_wrapper = UserWrapper
        return cls._user_wrapper

    def __init__(self, group, context=None):
        self.group = group
        self.context = context or {}

    def __eq__(self, other):
        assert isinstance(other, type(self))
        return self.group == other.group

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash(self.object)

    def __str__(self):
        return str(self.group)

    def __getattr__(self, name):
        return getattr(self.group, name)

    @property
    def head(self):
        return self.userify_func(self.group.head)

    def get_all_responsibles(self):
        responsibles = [self.userify_func(responsible) for responsible in self.group.get_responsibles(
            ranks=ranks_const.HEAD_OR_DEPUTY)
        ]
        return responsibles

    def get_responsibles(self, up_to_level=None, chain=True, reverse=True, include_self=True,
                         ranks=ranks_const.HEAD_OR_DEPUTY, priority_policy=PRIORITY_POLICIES.HEAD_LESS):
        """
                Возвращает список руководителей и заместителей с приоритетами начиная с этого департамента

                :param up_to_level: позволяет отсечь слишком высокоуровневые группы
                                    0 - только текущий уровень, положительное N – все группы ниже уровня N включительно
                                    отрицательное -N – все группы ниже, чем текущий-N включительно
                :param chain: True - складывает руководителей и заместителей по подразделениям, иначе кладет всех в один список
                :param reverse: True - от глубоких к корню. False - сверху в низ
                :param include_self: True - начинать поиск с текущего департамента. Инача начинает на один выше
                :param ranks: фильтрация по типу сотрудников
                :param priority_policy: как считать приоритет для руководителей и заместителей на одном уровне
                                        HEAD_LESS(default) - у руководителей приоритет меньше чем у заместителей на одном уровне
                                        DEPUTY_LESS - у заместителей меньше чем у руководителей на одном уровне
                                        EQUAL - одинаковый приоритет у тех и у других на одном уровне
                                        NO_DEPUTIES - вернуть только руководителей
                :return: Список списков руководителей и заместителей с приоритетами по подразделениям.
                          Если chain=False, то просто список
        """
        available_priorities = (PRIORITY_POLICIES.HEAD_LESS, PRIORITY_POLICIES.DEPUTY_LESS,
                                PRIORITY_POLICIES.EQUAL, PRIORITY_POLICIES.NO_DEPUTIES)

        if priority_policy not in available_priorities:
            raise NoSuchPriorityPolicyError("Priority {} does not exist. Available priorities are:"
                                            "{}".format(priority_policy, ", ".join(available_priorities)))

        # запрашивать только руководителей
        if priority_policy == PRIORITY_POLICIES.NO_DEPUTIES:
            ranks = (ranks_const.HEAD, )

        responsibilities = self.group.get_ancestor_responsibilities(
            up_to_level=up_to_level,
            ranks=ranks,
            include_self=include_self,
        )
        level_to_responsibilities = defaultdict(list)
        for resp in responsibilities:
            level_to_responsibilities[resp.group.level].append(resp)
        # сначала должны идти более глубокие уровни
        levels = sorted(level_to_responsibilities.keys(), reverse=reverse)
        chained = []
        flat = []
        visited = set()
        priority = 1

        for level in levels:
            level_resps = level_to_responsibilities[level]
            level_resps.sort(
                key=lambda resp: (ranks_const.RANK_WEIGHT[resp.rank], resp.user.username)
            )
            current_level = []

            head_priority, depity_priority = get_head_and_deputy_priorities(priority, priority_policy)

            for resp in level_resps:
                if resp.user in visited:
                    continue
                visited.add(resp.user)

                current_priority = DEFAULT_PRIORITY

                if resp.rank == ranks_const.HEAD:
                    current_priority = head_priority
                elif resp.rank == ranks_const.DEPUTY:
                    current_priority = depity_priority

                ranked_wrapper = self.get_user_wrapper()(user=resp.user, rank=resp.rank, priority=current_priority)
                current_level.append(ranked_wrapper)
                flat.append(ranked_wrapper)
            if current_level:
                chained.append(self.chain_level_class(level=level, items=current_level))

            priority = max(head_priority, depity_priority) + 1

        if chain:
            result = chained
        else:
            result = flat
        return result

    def _get_service_chain_of_heads(self, up_to_level, ranks, priority_policy):
        service_group: Group = self.group
        # Если это скоуп внутри сервиса, то нас интересует сервис
        if service_group.level == 2:
            service_group = service_group.parent
        service = service_group.get_service()
        services_line = service.get_ancestors(include_self=True)
        if up_to_level is not None:
            if up_to_level <= 0:
                up_to_level = service.level + up_to_level
                if up_to_level < 0:
                    up_to_level = 0
            services_line = services_line.filter(level__gte=up_to_level)
        group_slugs = []
        ordered_slugs = {}
        for n, slug in enumerate(services_line.values_list('slug', flat=True)):
            slug = 'svc_' + slug
            group_slugs.append(slug)
            ordered_slugs[slug] = n
        groups = list(
            Group.objects.filter(slug__in=group_slugs)
            .prefetch_related(
                Prefetch(
                    'responsibilities',
                    queryset=GroupResponsibility.objects.filter(is_active=True, user__is_active=True).select_related('user'),
                    to_attr='prefetched_active_responsibles',
                ),
            ),
        )
        groups.sort(key=lambda group: ordered_slugs[group.slug], reverse=True)
        groups_responsibles = [
            [responsible.user for responsible in group.prefetched_active_responsibles]
            for group in groups
        ]
        return groups_responsibles

    def get_chain_of_heads(self, up_to_level=None, ranks=ranks_const.HEAD_OR_DEPUTY,
                           priority_policy=PRIORITY_POLICIES.HEAD_LESS):
        """
            Возвращает список руководителей и заместителей с приоритетами начиная с ТЕКУЩЕГО департамента.
        """
        # to avoid circular import
        from idm.core.workflow.plain.approver import AnyApprover, Approver
        if self.group.type == GROUP_TYPES.SERVICE:
            service_levels = self._get_service_chain_of_heads(up_to_level, ranks, priority_policy)
            flat = [
                Approver(user, priority=index + 1)
                for index, block in enumerate(service_levels)
                for user in block
            ]
            approvers = AnyApprover(flat)
        else:
            chain = self.get_responsibles(up_to_level=up_to_level, ranks=ranks, priority_policy=priority_policy)
            approvers = AnyApprover(itertools.chain.from_iterable(chain))
        return approvers

    def get_heads(self):
        heads = [self.userify_func(head) for head in self.group.get_responsibles(ranks=(ranks_const.HEAD,))]
        return heads

    def get_deputies(self):
        deputies = [self.userify_func(deputy) for deputy in self.group.get_responsibles(ranks=(ranks_const.DEPUTY,))]
        return deputies

    def is_root(self):
        return self.group.parent is None or self.group.parent.is_root_node()

    def is_subgroup_of(self, slug):
        if isinstance(slug, str):
            filter_ = {
                'type': self.object.type,
                'slug': slug
            }
        elif isinstance(slug, int):
            filter_ = {
                'external_id': slug
            }
        else:
            raise ValueError('Wrong slug or id: %s' % slug)
        try:
            group = Group.objects.active().get(**filter_)
        except Group.DoesNotExist:
            raise GroupDoesNotExist('Group with slug "%s" does not exist' % slug)
        wrapper = self.as_wrapper(group, self.get_context())
        return self.is_descendant_of(wrapper)

    @property
    def staff_id(self):
        return self.group.external_id
