import json

from django.db import models
from django.db.models.fields.related import ForeignKey, ManyToManyField
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.serializers import serialize
from django.urls import reverse
from django.utils.safestring import mark_safe

from taggit.managers import TaggableManager
from taggit.models import TagBase, GenericTaggedItemBase

from .constants import *
from .fields import ColorField
from .templatetags.helpers import title_with_uppers


class ChangeLoggedModel(models.Model):
    """
    Abstract class providing fields and functions to log changes made to a model.
    """

    created = models.DateTimeField(auto_now_add=True, blank=True, null=True)
    updated = models.DateTimeField(auto_now=True, blank=True, null=True)

    class Meta:
        abstract = True

    def serialize(self):
        """
        Returns a JSON representation of an object using Django's built-in serializer.
        """
        return json.loads(serialize("json", [self]))[0]["fields"]

    def log_change(self, user, request_id, action):
        """
        Creates a new ObjectChange representing a change made to this object.
        """
        ObjectChange(
            user=user,
            request_id=request_id,
            changed_object=self,
            action=action,
            object_data=self.serialize(),
        ).save()


class ObjectChange(models.Model):
    """
    Records a change done to an object and the user who did it.
    """

    time = models.DateTimeField(auto_now_add=True, editable=False)
    user = models.ForeignKey(
        to=User,
        on_delete=models.SET_NULL,
        related_name="changes",
        blank=True,
        null=True,
    )
    user_name = models.CharField(max_length=150, editable=False)
    request_id = models.UUIDField(editable=False)
    action = models.PositiveSmallIntegerField(choices=OBJECT_CHANGE_ACTION_CHOICES)
    changed_object_type = models.ForeignKey(
        to=ContentType, on_delete=models.PROTECT, related_name="+"
    )
    changed_object_id = models.PositiveIntegerField()
    changed_object = GenericForeignKey(
        ct_field="changed_object_type", fk_field="changed_object_id"
    )
    related_object_type = models.ForeignKey(
        to=ContentType,
        on_delete=models.PROTECT,
        related_name="+",
        blank=True,
        null=True,
    )
    related_object_id = models.PositiveIntegerField(blank=True, null=True)
    related_object = GenericForeignKey(
        ct_field="related_object_type", fk_field="related_object_id"
    )
    object_repr = models.CharField(max_length=256, editable=False)
    object_data = JSONField(editable=False)

    class Meta:
        ordering = ["-time"]

    def save(self, *args, **kwargs):
        self.user_name = self.user.username
        self.object_repr = str(self.changed_object)

        return super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse("utils:objectchange_details", kwargs={"pk": self.pk})

    def get_html_icon(self):
        if self.action == OBJECT_CHANGE_ACTION_CREATE:
            return mark_safe('<i class="fas fa-plus-square text-success"></i>')
        if self.action == OBJECT_CHANGE_ACTION_UPDATE:
            return mark_safe('<i class="fas fa-pen-square text-warning"></i>')
        if self.action == OBJECT_CHANGE_ACTION_DELETE:
            return mark_safe('<i class="fas fa-minus-square text-danger"></i>')
        return mark_safe('<i class="fas fa-question-circle text-secondary"></i>')

    def __str__(self):
        return "{} {} {} by {}".format(
            title_with_uppers(self.changed_object_type),
            self.object_repr,
            self.get_action_display().lower(),
            self.user_name,
        )


class Tag(TagBase, ChangeLoggedModel):
    color = ColorField(default="9e9e9e")
    comments = models.TextField(blank=True, default="")

    def get_absolute_url(self):
        return reverse("utils:tag_details", kwargs={"slug": self.slug})


class TaggedItem(GenericTaggedItemBase):
    tag = models.ForeignKey(
        to=Tag, related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE
    )

    class Meta:
        index_together = ("content_type", "object_id")


class TaggableModel(models.Model):
    """
    Abstract class that just provides tags to its subclasses.
    """

    tags = TaggableManager(through=TaggedItem)

    class Meta:
        abstract = True


class TemplateModel(models.Model):
    """
    Abstract class providing functions to be used in templates.
    """

    class Meta:
        abstract = True

    def to_dict(self):
        data = {}

        # Iterate over croncrete and many-to-many fields
        for field in self._meta.concrete_fields + self._meta.many_to_many:
            value = None

            # If the value of the field is another model, fetch an instance of it
            # Exception made of tags for which we just retrieve the list of them for later
            # conversion to simple strings
            if isinstance(field, ForeignKey):
                value = self.__getattribute__(field.name)
            elif isinstance(field, ManyToManyField):
                value = list(self.__getattribute__(field.name).all())
            elif isinstance(field, TaggableManager):
                value = list(self.__getattribute__(field.name).all())
            else:
                value = self.__getattribute__(field.name)

            # If the instance of a model as a to_dict() function, call it
            if isinstance(value, TemplateModel):
                data[field.name] = value.to_dict()
            elif isinstance(value, list):
                data[field.name] = []
                for element in value:
                    if isinstance(element, TemplateModel):
                        data[field.name].append(element.to_dict())
                    else:
                        data[field.name].append(str(element))
            else:
                data[field.name] = value

        return data