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 import Case, When
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

    def to_content_type_with_targets(self, content_type):
        """
        Return the queryset annotated with the target content type
        and object id, based on the content_type that is passed.
        """
        return self.annotate(
            target_content_type=Case(
                When(subj_content_type=content_type, then="obj_content_type"),
                default="subj_content_type",
            ),
            target_id=Case(
                When(subj_content_type=content_type, then="obj_object_id"),
                default="subj_object_id",
            ),
        )


# 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()
