import uuid

from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models, router, transaction
from django.utils.encoding import force_str
from django.utils.html import conditional_escape
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _

from treenode import classproperty
from treenode.cache import clear_cache, query_cache, update_cache
from treenode.debug import debug_performance
from treenode.exceptions import CacheError
from treenode.memory import clear_refs, update_refs
from treenode.signals import connect_signals, no_signals
from treenode.utils import contains_pk, join_pks, split_pks


class TreeNodeModel(models.Model):
    """
    Usage:

    from django.db import models
    from treenode.models import TreeNodeModel


    class MyModel(TreeNodeModel):

        treenode_display_field = 'name'

        name = models.CharField(max_length=50)

        class Meta(TreeNodeModel.Meta):
            verbose_name = 'My Model'
            verbose_name_plural = 'My Models'
    """

    # Options
    treenode_display_field = None

    # Fields
    # All fields are for internal usage and they are prefixed by 'tn_'
    # to avoid direct access and conflicts with possible existing fields.

    tn_ancestors_pks = models.TextField(
        blank=True,
        default="",
        editable=False,
        verbose_name=_("Ancestors pks"),
    )

    tn_ancestors_count = models.PositiveIntegerField(
        default=0,
        editable=False,
        verbose_name=_("Ancestors count"),
    )

    tn_children_pks = models.TextField(
        blank=True,
        default="",
        editable=False,
        verbose_name=_("Children pks"),
    )

    tn_children_count = models.PositiveIntegerField(
        default=0,
        editable=False,
        verbose_name=_("Children count"),
    )

    tn_depth = models.PositiveIntegerField(
        default=0,
        editable=False,
        validators=[MinValueValidator(0), MaxValueValidator(10)],
        verbose_name=_("Depth"),
    )

    tn_descendants_pks = models.TextField(
        blank=True,
        default="",
        editable=False,
        verbose_name=_("Descendants pks"),
    )

    tn_descendants_count = models.PositiveIntegerField(
        default=0,
        editable=False,
        verbose_name=_("Descendants count"),
    )

    tn_index = models.PositiveIntegerField(
        default=0,
        editable=False,
        verbose_name=_("Index"),
    )

    tn_level = models.PositiveIntegerField(
        default=1,
        editable=False,
        validators=[MinValueValidator(1), MaxValueValidator(10)],
        verbose_name=_("Level"),
    )

    tn_parent = models.ForeignKey(
        "self",
        related_name="tn_children",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        verbose_name=_("Parent"),
    )

    tn_priority = models.PositiveIntegerField(
        default=0,
        validators=[MinValueValidator(0), MaxValueValidator(9999999999)],
        verbose_name=_("Priority"),
    )

    tn_order = models.PositiveIntegerField(
        default=0,
        editable=False,
        verbose_name=_("Order"),
    )

    tn_siblings_pks = models.TextField(
        blank=True,
        default="",
        editable=False,
        verbose_name=_("Siblings pks"),
    )

    tn_siblings_count = models.PositiveIntegerField(
        default=0,
        editable=False,
        verbose_name=_("Siblings count"),
    )

    # Public methods

    def delete(self, using=None, keep_parents=False, cascade=True):
        with no_signals():
            if not cascade:
                children_qs = self.get_children_queryset()
                children_qs.update(tn_parent=None)
            self.__class__.objects.filter(pk=self.pk).delete()
        self.update_tree()

    @classmethod
    def delete_tree(cls):
        with no_signals():
            with transaction.atomic(using=router.db_for_write(cls)):
                cls.objects.all().delete()
            clear_refs(cls)
            clear_cache(cls)

    @classmethod
    def _get_all(cls, cache=True):
        if cache:
            try:
                return query_cache(cls)
            except CacheError:
                pass
        return list(cls.objects.all())

    def get_ancestors(self, cache=True):
        if cache:
            try:
                return query_cache(self.__class__, pks=self.tn_ancestors_pks)
            except CacheError:
                pass
        return list(self.get_ancestors_queryset())

    def get_ancestors_count(self):
        return self.tn_ancestors_count

    def get_ancestors_pks(self):
        return split_pks(self.tn_ancestors_pks)

    def get_ancestors_queryset(self):
        return self.__class__.objects.filter(pk__in=self.get_ancestors_pks())

    def get_breadcrumbs(self, attr=None, cache=True):
        objs = (self.get_ancestors(cache=cache) if self.tn_parent_id else []) + [self]
        return [getattr(obj, attr) for obj in objs] if attr else objs

    def get_children(self, cache=True):
        if cache:
            try:
                return query_cache(self.__class__, pks=self.tn_children_pks)
            except CacheError:
                pass
        return list(self.get_children_queryset())

    def get_children_count(self):
        return self.tn_children_count

    def get_children_pks(self):
        return split_pks(self.tn_children_pks)

    def get_children_queryset(self):
        return self.__class__.objects.filter(pk__in=self.get_children_pks())

    def get_depth(self):
        return self.tn_depth

    def get_descendants(self, cache=True):
        if cache:
            try:
                return query_cache(self.__class__, pks=self.tn_descendants_pks)
            except CacheError:
                pass
        return list(self.get_descendants_queryset())

    def get_descendants_count(self):
        return self.tn_descendants_count

    def get_descendants_pks(self):
        return split_pks(self.tn_descendants_pks)

    def get_descendants_queryset(self):
        return self.__class__.objects.filter(pk__in=self.get_descendants_pks())

    def get_descendants_tree(self, cache=True):
        return self.__get_nodes_tree(instance=self, cache=cache)

    def get_descendants_tree_display(self, cache=True):
        objs = self.get_descendants(cache=cache)
        strs = [f"{obj}" for obj in objs]
        d = "\n".join(strs)
        return d

    # def get_descendants_tree_dump(self, indent=2, default=None):
    #     data = self.get_descendants_tree()
    #     func = lambda obj:str(obj.pk)
    #     dump = json.dumps(data,
    #         sort_keys=True,
    #         indent=indent,
    #         default=func if not default else default)
    #     return dump

    def get_display(self, indent=True, mark="— "):
        indentation = (mark * self.tn_ancestors_count) if indent else ""
        indentation = force_str(indentation)
        text = self.get_display_text()
        text = conditional_escape(force_str(text))
        return indentation + text

    def get_display_text(self):
        """
        Gets the text that will be indented in `get_display` method.
        Returns the `treenode_display_field` value if specified,
        otherwise falls back on the model's pk.
        Override this method to return another field or a computed value. #27
        """
        text = ""
        if (
            hasattr(self, "treenode_display_field")
            and self.treenode_display_field is not None
        ):
            field_name = self.treenode_display_field
            text = getattr(self, field_name, "")
        if not text and self.pk:
            text = self.pk
        return force_str(text)

    def get_first_child(self, cache=True):
        return self.get_children(cache=cache)[0] if self.get_children_count() else None

    def get_index(self):
        return self.tn_index

    def get_last_child(self, cache=True):
        return self.get_children(cache=cache)[-1] if self.get_children_count() else None

    def get_level(self):
        return self.tn_level

    def get_order(self):
        return self.tn_order

    def get_parent(self):
        return self.tn_parent

    def get_parent_pk(self):
        return self.tn_parent_id

    def set_parent(self, obj):
        with no_signals():
            if obj:
                obj_cls = obj.__class__
                cls = self.__class__
                if obj_cls != cls:
                    raise ValueError(
                        "obj can't be set as parent, "
                        f"it is istance of {obj_cls.__name__}, "
                        f"expected instance of {cls.__name__}."
                    )
                if obj == self:
                    raise ValueError("obj can't be set as parent of itself.")
                if not obj.pk:
                    obj.save()
                if contains_pk(self.tn_descendants_pks, obj.pk):
                    obj.tn_parent = self.tn_parent
                    obj.save()
            self.tn_parent = obj
            self.save()
        self.update_tree()

    def get_priority(self):
        return self.tn_priority

    def set_priority(self, val):
        self.tn_priority = val
        self.save()

    def get_root(self, cache=True):
        root_pk = self.get_root_pk()
        if cache:
            try:
                return query_cache(self.__class__, pk=root_pk)
            except CacheError:
                pass
        return self.__class__.objects.get(pk=root_pk)

    def get_root_pk(self):
        return (split_pks(self.tn_ancestors_pks) + [self.pk])[0]

    @classmethod
    def get_roots(cls, cache=True):
        if cache:
            try:
                return [obj for obj in query_cache(cls) if obj.tn_ancestors_count == 0]
            except CacheError:
                pass
        return list(cls.get_roots_queryset())

    @classmethod
    def get_roots_queryset(cls):
        return cls.objects.filter(tn_ancestors_count=0)

    def get_siblings(self, cache=True):
        if cache:
            try:
                return query_cache(self.__class__, pks=self.tn_siblings_pks)
            except CacheError:
                pass
        return list(self.get_siblings_queryset())

    def get_siblings_count(self):
        return self.tn_siblings_count

    def get_siblings_pks(self):
        return split_pks(self.tn_siblings_pks)

    def get_siblings_queryset(self):
        return self.__class__.objects.filter(pk__in=self.get_siblings_pks())

    @classmethod
    def get_tree(cls, cache=True):
        return cls.__get_nodes_tree(instance=None, cache=cache)

    @classmethod
    def get_tree_display(cls, cache=True):
        objs = cls._get_all(cache=cache)
        strs = [f"{obj}" for obj in objs]
        d = "\n".join(strs)
        return d

    # @classmethod
    # def get_tree_dump(cls, cache=True, indent=2, default=None):
    #     data = cls.get_tree(cache=cache)
    #     func = lambda obj:str(obj.pk)
    #     dump = json.dumps(data,
    #         sort_keys=True,
    #         indent=indent,
    #         default=func if not default else default)
    #     return dump

    def is_ancestor_of(self, obj):
        return (
            self.__class__ == obj.__class__
            and self.pk
            and self.pk != obj.pk
            and contains_pk(obj.tn_ancestors_pks, self.pk)
        )

    def is_child_of(self, obj):
        return (
            self.__class__ == obj.__class__
            and self.pk
            and self.pk != obj.pk
            and contains_pk(obj.tn_children_pks, self.pk)
        )

    def is_descendant_of(self, obj):
        return (
            self.__class__ == obj.__class__
            and self.pk
            and self.pk != obj.pk
            and contains_pk(obj.tn_descendants_pks, self.pk)
        )

    def is_first_child(self):
        return self.pk and self.tn_index == 0

    def is_last_child(self):
        return self.pk and self.tn_index == self.tn_siblings_count

    def is_leaf(self):
        return self.pk and self.tn_children_count == 0

    def is_parent_of(self, obj):
        return (
            self.__class__ == obj.__class__
            and self.pk
            and self.pk != obj.pk
            and obj.tn_ancestors_count > 0
            and str(self.pk) == split_pks(obj.tn_ancestors_pks)[-1]
        )

    def is_root(self):
        return self.pk and self.tn_ancestors_count == 0

    def is_root_of(self, obj):
        return self.is_root() and self.is_ancestor_of(obj)

    def is_sibling_of(self, obj):
        return (
            self.__class__ == obj.__class__
            and self.pk
            and self.pk != obj.pk
            and self.tn_ancestors_pks == obj.tn_ancestors_pks
        )

    @classmethod
    def update_tree(cls):
        debug_message_prefix = (
            f"[treenode] update {cls.__module__}.{cls.__name__} tree: "
        )

        with debug_performance(debug_message_prefix):
            # update db
            objs_data = cls.__get_nodes_data()

            with transaction.atomic(using=router.db_for_write(cls)):
                obj_manager = cls.objects
                for obj_pk, obj_data in objs_data.items():
                    obj_manager.filter(pk=obj_pk).update(**obj_data)

            # update in-memory instances
            update_refs(cls, objs_data)

            # update cache instances
            try:
                update_cache(cls)
            except CacheError:
                pass

    # Private methods

    def __get_node_order_str(self):
        priority_max = 9999999999
        priority_len = len(str(priority_max))
        priority_val = priority_max - min(self.tn_priority, priority_max)
        priority_key = str(priority_val).zfill(priority_len)
        alphabetical_val = slugify(str(self))
        alphabetical_key = alphabetical_val.ljust(priority_len, "z")
        alphabetical_key = alphabetical_key[0:priority_len]

        if isinstance(self.pk, uuid.UUID):
            pk_val = self.pk.int
            pk_val = int(str(pk_val)[:priority_len])
        else:
            try:
                pk_val = min(self.pk, priority_max)
            except TypeError:
                pk_val = str(self.pk)

        pk_key = str(pk_val).zfill(priority_len)
        s = f"{priority_key}{alphabetical_key}{pk_key}"
        s = s.upper()
        return s

    def __get_node_data(self, objs_list, objs_dict):
        obj_dict = {}

        # update ancestors
        parent_pk = self.get_parent_pk()

        ancestors_list = []
        ancestor_pk = parent_pk
        while ancestor_pk:
            ancestor_obj = objs_dict.get(str(ancestor_pk))
            if not ancestor_obj:
                # this may happen loading fixtures, when the current object
                # references a parent object that has not been created yet
                break
            ancestors_list.insert(0, ancestor_obj)
            ancestor_pk = ancestor_obj.get_parent_pk()
        ancestors_pks = [obj.pk for obj in ancestors_list]
        ancestors_count = len(ancestors_pks)

        order_objs = list(ancestors_list) + [self]
        order_strs = [obj.__get_node_order_str() for obj in order_objs]
        order_str = "".join(order_strs)

        obj_dict = {
            "instance": self,
            "pk": self.pk,
            "tn_parent_pk": parent_pk,
            "tn_ancestors_pks": ancestors_pks,
            "tn_ancestors_count": ancestors_count,
            "tn_children_pks": [],
            "tn_children_count": 0,
            "tn_descendants_pks": [],
            "tn_descendants_count": 0,
            "tn_siblings_pks": [],
            "tn_siblings_count": 0,
            "tn_depth": 0,
            "tn_level": (ancestors_count + 1),
            "tn_order": 0,
            "tn_order_str": order_str,
        }

        return obj_dict

    @classmethod
    def __get_nodes_data(cls):  # noqa: C901
        objs_qs = cls.objects.select_related("tn_parent")
        objs_list = list(objs_qs)
        objs_dict = {str(obj.pk): obj for obj in objs_list}
        objs_data_dict = {
            str(obj.pk): obj.__get_node_data(objs_list, objs_dict) for obj in objs_list
        }

        def objs_data_sort(obj):
            return objs_data_dict[str(obj["pk"])]["tn_order_str"]

        objs_data_list = list(objs_data_dict.values())
        objs_data_list.sort(key=objs_data_sort)
        objs_pks_by_parent = {}
        objs_order_cursor = 0
        objs_index_cursors = {}
        objs_index_cursor = 0

        # index objects by parent pk
        for obj_data in objs_data_list:
            obj_parent_key = str(obj_data["tn_parent_pk"])
            objs_pks_by_parent.setdefault(obj_parent_key, [])
            objs_pks_by_parent[obj_parent_key].append(obj_data["pk"])

            # update global order with normalized value
            obj_data["tn_order"] = objs_order_cursor
            objs_order_cursor += 1

            # update child index
            obj_parent_key = str(obj_data["tn_parent_pk"])
            objs_index_cursor = objs_index_cursors.get(obj_parent_key, 0)
            obj_data["tn_index"] = objs_index_cursor
            objs_index_cursor += 1
            objs_index_cursors[obj_parent_key] = objs_index_cursor

        for obj_data in sorted(
            objs_data_list, key=lambda obj: obj["tn_level"], reverse=True
        ):
            # update children
            children_parent_key = str(obj_data["pk"])
            obj_data["tn_children_pks"] = list(
                objs_pks_by_parent.get(children_parent_key, [])
            )
            obj_data["tn_children_count"] = len(obj_data["tn_children_pks"])

            # update siblings
            siblings_parent_key = str(obj_data["tn_parent_pk"])
            obj_data["tn_siblings_pks"] = list(
                objs_pks_by_parent.get(siblings_parent_key, [])
            )
            obj_data["tn_siblings_pks"].remove(obj_data["pk"])
            obj_data["tn_siblings_count"] = len(obj_data["tn_siblings_pks"])

            # update descendants and depth
            if obj_data["tn_children_count"] > 0:
                obj_children_pks = obj_data["tn_children_pks"]
                obj_descendants_pks = list(obj_children_pks)
                obj_depth = 1
                for obj_child_pk in obj_children_pks:
                    obj_child_key = str(obj_child_pk)
                    obj_child_data = objs_data_dict[obj_child_key]
                    obj_child_descendants_pks = obj_child_data.get(
                        "tn_descendants_pks", []
                    )
                    if obj_child_descendants_pks:
                        obj_descendants_pks += obj_child_descendants_pks
                        obj_depth = max(obj_depth, obj_child_data["tn_depth"] + 1)

                if obj_descendants_pks:

                    def obj_descendants_sort(obj_pk):
                        return objs_data_dict[str(obj_pk)]["tn_order"]

                    obj_descendants_pks.sort(key=obj_descendants_sort)
                    obj_data["tn_descendants_pks"] = obj_descendants_pks
                    obj_data["tn_descendants_count"] = len(
                        obj_data["tn_descendants_pks"]
                    )
                    obj_data["tn_depth"] = obj_depth

        for obj_data in objs_data_list:
            obj = obj_data["instance"]
            obj_key = str(obj_data["pk"])

            # join all pks lists
            obj_data["tn_ancestors_pks"] = join_pks(obj_data["tn_ancestors_pks"])
            obj_data["tn_children_pks"] = join_pks(obj_data["tn_children_pks"])
            obj_data["tn_descendants_pks"] = join_pks(obj_data["tn_descendants_pks"])
            obj_data["tn_siblings_pks"] = join_pks(obj_data["tn_siblings_pks"])

            # clean data
            obj_data.pop("instance", None)
            obj_data.pop("pk", None)
            obj_data.pop("tn_parent_pk", None)
            obj_data.pop("tn_order_str", None)

            keys = [
                "tn_ancestors_count",
                "tn_ancestors_pks",
                "tn_children_count",
                "tn_children_pks",
                "tn_depth",
                "tn_descendants_count",
                "tn_descendants_pks",
                "tn_index",
                "tn_level",
                "tn_order",
                "tn_siblings_count",
                "tn_siblings_pks",
            ]
            for key in keys:
                if obj_data[key] == getattr(obj, key, None):
                    obj_data.pop(key, None)

            if len(obj_data) == 0:
                objs_data_dict.pop(obj_key, None)

        return objs_data_dict

    @classmethod
    def __get_nodes_tree(cls, instance=None, cache=True):
        def __get_node_tree(obj):
            child_tree = {"node": obj, "tree": []}
            child_tree_append = child_tree["tree"].append
            if obj.tn_children_pks:
                children_pks = split_pks(obj.tn_children_pks)
                for child_pk in children_pks:
                    child_obj = objs_dict.get(child_pk)
                    if child_obj:
                        child_tree_append(__get_node_tree(child_obj))
            return child_tree

        if instance:
            objs_list = instance.get_descendants(cache=cache)
            objs_dict = {str(obj.pk): obj for obj in objs_list}
            objs_tree = __get_node_tree(instance)["tree"]
        else:
            objs_list = cls._get_all(cache=cache)
            objs_dict = {str(obj.pk): obj for obj in objs_list}
            objs_tree = [__get_node_tree(obj) for obj in objs_list if obj.tn_level == 1]

        return objs_tree

    # Public properties
    # All properties map a get_{{property}}() method.

    @property
    def ancestors(self):
        return self.get_ancestors()

    @property
    def ancestors_count(self):
        return self.get_ancestors_count()

    @property
    def ancestors_pks(self):
        return self.get_ancestors_pks()

    @property
    def breadcrumbs(self):
        return self.get_breadcrumbs()

    @property
    def children(self):
        return self.get_children()

    @property
    def children_count(self):
        return self.get_children_count()

    @property
    def children_pks(self):
        return self.get_children_pks()

    @property
    def depth(self):
        return self.get_depth()

    @property
    def descendants(self):
        return self.get_descendants()

    @property
    def descendants_count(self):
        return self.get_descendants_count()

    @property
    def descendants_pks(self):
        return self.get_descendants_pks()

    @property
    def descendants_tree(self):
        return self.get_descendants_tree()

    @property
    def descendants_tree_display(self):
        return self.get_descendants_tree_display()

    @property
    def first_child(self):
        return self.get_first_child()

    @property
    def index(self):
        return self.get_index()

    @property
    def last_child(self):
        return self.get_last_child()

    @property
    def level(self):
        return self.get_level()

    @property
    def order(self):
        return self.get_order()

    @property
    def parent(self):
        return self.get_parent()

    @property
    def parent_pk(self):
        return self.get_parent_pk()

    @property
    def priority(self):
        return self.get_priority()

    @classproperty
    def roots(cls):  # noqa: B902
        return cls.get_roots()

    @property
    def root(self):
        return self.get_root()

    @property
    def root_pk(self):
        return self.get_root_pk()

    @property
    def siblings(self):
        return self.get_siblings()

    @property
    def siblings_count(self):
        return self.get_siblings_count()

    @property
    def siblings_pks(self):
        return self.get_siblings_pks()

    @classproperty
    def tree(cls):  # noqa: B902
        return cls.get_tree()

    @classproperty
    def tree_display(cls):  # noqa: B902
        return cls.get_tree_display()

    class Meta:
        abstract = True
        ordering = ["tn_order"]

    def __str__(self):
        return conditional_escape(self.get_display(indent=True))


connect_signals()
