# coding: utf-8


from collections import defaultdict, deque
from operator import attrgetter

from django.db.models.constants import LOOKUP_SEP
from django.utils import six
from django.utils.encoding import force_str

from idm.users.constants.group import GROUP_TYPES

from idm.utils.i18n import get_localized_fieldname
from idm.utils.multilist import multilist
from idm.utils.tree import make_tree_from_descendats


class Representable(object):
    repr_template = '<%s: %s>'
    class_name_repr = None

    def __repr__(self):
        try:
            u = six.text_type(self)
        except (UnicodeEncodeError, UnicodeDecodeError):
            u = '[Bad Unicode data]'
        if not self.class_name_repr:
            self.class_name_repr = self.__class__.__name__
        return force_str(self.repr_template % (self.class_name_repr, u))


class LocalizedModel(object):
    def get_localized_field(self, field, lang=None):
        return getattr(self, get_localized_fieldname(field, lang))


class DifferenceTableMixin(object):
    def get_changes_for_new_root(self, new_root=None, membership_inheritance=None):
        """ Возвращает таблицу, показывающую приобретенные и потерянные узлы при смене родителя """
        assert (new_root is None) ^ (membership_inheritance is None)

        from idm.users.models import Group
        if isinstance(self, Group) and self.type == GROUP_TYPES.SERVICE:
            return self.get_changes_of_service_group_for_new_root(
                new_root=new_root,
                membership_inheritance=membership_inheritance,
            )

        lost_nodes = self.get_inherited_services()

        if membership_inheritance is not None:
            self.membership_inheritance = True
        descendants_with_inheritance, ancestors = self.get_descendants_splintered_by_inheritance()

        if membership_inheritance is not None:
            self.membership_inheritance = membership_inheritance
            if membership_inheritance:
                new_root = self.parent
            else:
                new_root = None

        if new_root and self.get_membership_inheritance():
            obtained_nodes = new_root.get_inherited_services(include_self=True)
        else:
            obtained_nodes = []

        result = {node: {
            'lost': lost_nodes,
            'obtained': obtained_nodes,
            'ancestors': node_ancestors,
        } for node, node_ancestors in zip(descendants_with_inheritance, ancestors)}
        return result

    def get_descendants_splintered_by_inheritance(self):
        """
        Возвращает два списка - потомков, наследующих роли узла, включая себя,
         и предков для каждого потомка
        """
        descendants = self.get_descendants(include_self=True).select_related('parent')
        tree = make_tree_from_descendats(descendants)

        ancestors = list()
        descendants_with_inheritance = list()
        queue = deque([(self, multilist(self))]) if self.get_membership_inheritance() else None
        while queue:
            cur, node_ancestors = queue.pop()
            descendants_with_inheritance.append(cur)
            ancestors.append(node_ancestors)
            for child in tree[cur]:
                if child.get_membership_inheritance():
                    queue.append((child, node_ancestors + child))

        return descendants_with_inheritance, ancestors

    def get_inherited_services(self, include_self=False):
        result = []
        if not self.get_membership_inheritance() and not include_self:
            return result
        ancestors = self.get_ancestors(include_self=include_self).reverse()
        for node in ancestors:
            result.append(node)
            if not node.get_membership_inheritance():
                break
        else:
            # Если дошли до конца, надо удалить царь-узел
            result.pop()
        return result

    def get_membership_inheritance(self):
        return getattr(self, 'membership_inheritance', True)


class RefreshFromDbWithoutRelatedMixin:
    def refresh_from_db(self, using=None, fields=None):
        """Копия оригинального метода, но не удаляем закешированные связанные объекты"""
        if fields is not None:
            # В оригинальном методе в случае fields is None удаляют весь кеш
            prefetched_objects_cache = getattr(self, '_prefetched_objects_cache', ())
            for field in fields:
                if field in prefetched_objects_cache:
                    del prefetched_objects_cache[field]
                    fields.remove(field)
            if not fields:
                return
            if any(LOOKUP_SEP in f for f in fields):
                raise ValueError(
                    'Found "%s" in fields argument. Relations and transforms '
                    'are not allowed in fields.' % LOOKUP_SEP)

        hints = {'instance': self}
        db_instance_qs = self.__class__._base_manager.db_manager(using, hints=hints).filter(pk=self.pk)

        # Use provided fields, if not set then reload all non-deferred fields.
        deferred_fields = self.get_deferred_fields()
        if fields is not None:
            fields = list(fields)
            db_instance_qs = db_instance_qs.only(*fields)
        elif deferred_fields:
            fields = [f.attname for f in self._meta.concrete_fields
                      if f.attname not in deferred_fields]
            db_instance_qs = db_instance_qs.only(*fields)

        db_instance = db_instance_qs.get()
        non_loaded_fields = db_instance.get_deferred_fields()
        for field in self._meta.concrete_fields:
            if field.attname in non_loaded_fields:
                # This field wasn't refreshed - skip ahead.
                continue
            setattr(self, field.attname, getattr(db_instance, field.attname))
            # Clear cached foreign keys.
            if field.is_relation and field.is_cached(self):
                # В оригинальном методе тут всегда делают field.delete_cached_value(self)
                # https://github.com/django/django/commit/136bf5c2142ae3261669d02e2dd050c3141f7f2f
                rel_instance = field.get_cached_value(self)
                local_val = getattr(db_instance, field.attname)
                related_val = None if rel_instance is None else getattr(rel_instance, field.target_field.attname)
                if local_val != related_val or (local_val is None and related_val is None):
                    field.delete_cached_value(self)

        # Clear cached relations.
        for field in self._meta.related_objects:
            if field.is_cached(self):
                field.delete_cached_value(self)

        self._state.db = db_instance._state.db
