import functools
import logging

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.base import ModelBase
from model_utils.managers import InheritanceManager

from apis_core.generic.abc import GenericModel

logger = logging.getLogger(__name__)


class RelationManager(InheritanceManager):
    def create_between_instances(self, subj, obj, *args, **kwargs):
        subj_object_id = subj.pk
        subj_content_type = ContentType.objects.get_for_model(subj)
        obj_object_id = obj.pk
        obj_content_type = ContentType.objects.get_for_model(obj)
        rel = self.create(
            subj_object_id=subj_object_id,
            subj_content_type=subj_content_type,
            obj_object_id=obj_object_id,
            obj_content_type=obj_content_type,
        )
        logger.debug("Created relation %s between %s and %s", rel.name(), subj, obj)
        return rel


# This ModelBase is simply there to check if the needed attributes
# are set in the Relation child classes.
class RelationModelBase(ModelBase):
    def __new__(metacls, name, bases, attrs):
        if name == "Relation":
            return super().__new__(metacls, name, bases, attrs)
        else:
            new_class = super().__new__(metacls, name, bases, attrs)
            if not (new_class._meta.abstract or new_class._meta.proxy):
                if not hasattr(new_class, "subj_model"):
                    raise ValueError(
                        "%s inherits from Relation and must therefore specify subj_model"
                        % name
                    )
                if not hasattr(new_class, "obj_model"):
                    raise ValueError(
                        "%s inherits from Relation and must therefore specify obj_model"
                        % name
                    )

                # `subj_model` or `obj_model` being a list was supported in an earlier
                # version of apis, but it is not anymore
                if isinstance(getattr(new_class, "subj_model", None), list):
                    raise ValueError("%s.subj_model must not be a list" % name)
                if isinstance(getattr(new_class, "obj_model", None), list):
                    raise ValueError("%s.obj_model mut not be a list" % name)

            if not new_class._meta.ordering:
                logger.warning(
                    f"{name} inherits from Relation but does not specify 'ordering' in its Meta class. "
                    "Empty ordering could result in inconsitent results with pagination. "
                    "Set a ordering or inherit the Meta class from Relation.",
                )

            return new_class


@functools.cache
def get_by_natural_key(natural_key: str):
    app_label, name = natural_key.lower().split(".")
    return ContentType.objects.get_by_natural_key(app_label, name).model_class()


class Relation(GenericModel, models.Model, metaclass=RelationModelBase):
    subj_content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name="relation_subj_set"
    )
    subj_object_id = models.PositiveIntegerField(null=True)
    subj = GenericForeignKey("subj_content_type", "subj_object_id")
    obj_content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name="relation_obj_set"
    )
    obj_object_id = models.PositiveIntegerField(null=True)
    obj = GenericForeignKey("obj_content_type", "obj_object_id")

    objects = RelationManager()

    class Meta:
        indexes = [
            models.Index(
                fields=["subj_content_type"], name="relations_r_subj_content_type"
            ),
            models.Index(fields=["subj_object_id"], name="relations_r_subj_object_id"),
            models.Index(
                fields=["obj_content_type"], name="relations_r_obj_content_type"
            ),
            models.Index(fields=["obj_object_id"], name="relations_r_obj_object_id"),
            models.Index(
                fields=["subj_content_type", "subj_object_id"],
                name="relations_r_subj_c_t_o_i",
            ),
            models.Index(
                fields=["obj_content_type", "obj_object_id"],
                name="relations_r_obj_c_t_o_i",
            ),
        ]

    def save(self, *args, **kwargs):
        subj_model = getattr(self, "subj_model", None)
        if subj_model and self.subj_content_type is subj_model:
            raise ValidationError(f"{self.subj} is not of type {subj_model}")
        obj_model = getattr(self, "obj_model", None)
        if obj_model and self.obj_content_type is obj_model:
            raise ValidationError(f"{self.obj} is not of type {obj_model}")
        super().save(*args, **kwargs)

    @property
    def subj_to_obj_text(self) -> str:
        if hasattr(self, "name"):
            return f"{self.subj} {self.name()} {self.obj}"
        return f"{self.subj} relation to {self.obj}"

    @property
    def obj_to_subj_text(self) -> str:
        if hasattr(self, "reverse_name"):
            return f"{self.obj} {self.reverse_name()} {self.subj}"
        return f"{self.obj} relation to {self.subj}"

    def __str__(self):
        return self.subj_to_obj_text

    @classmethod
    def subj_model_type(cls):
        model = cls.subj_model
        return get_by_natural_key(model) if isinstance(model, str) else model

    @classmethod
    def obj_model_type(cls):
        model = cls.obj_model
        return get_by_natural_key(model) if isinstance(model, str) else model

    @classmethod
    def name(cls) -> str:
        return cls._meta.verbose_name

    @classmethod
    def reverse_name(cls) -> str:
        return cls._meta.verbose_name + " reverse"

    @classmethod
    def name_and_reverse_name(cls) -> str:
        """
        Return a string with both the name and the reverse name.

        If they are identical, return only the name.
        """
        if cls.name() != cls.reverse_name():
            return f"{cls.name()} - {cls.reverse_name()}"
        return cls.name()
