"""
PolymorphicModel Meta Class
"""

import sys
import warnings

from django.db import models
from django.db.models.base import ModelBase

from .deletion import PolymorphicGuard
from .managers import PolymorphicManager
from .related_descriptors import (
    NonPolymorphicForwardOneToOneDescriptor,
    NonPolymorphicReverseOneToOneDescriptor,
)
from .utils import _clear_utility_caches

# PolymorphicQuerySet Q objects (and filter()) support these additional key words.
# These are forbidden as field names (a descriptive exception is raised)
POLYMORPHIC_SPECIAL_Q_KWORDS = {"instance_of", "not_instance_of"}


class ManagerInheritanceWarning(RuntimeWarning):
    pass


# check that we're on cpython to enable dumpdata frame inspection guard
check_dump = hasattr(sys, "_getframe")


###################################################################################
# PolymorphicModel meta class


class PolymorphicModelBase(ModelBase):
    """
    Manager inheritance is a pretty complex topic which may need
    more thought regarding how this should be handled for polymorphic
    models.

    In any case, we probably should propagate 'objects' and 'base_objects'
    from PolymorphicModel to every subclass. We also want to somehow
    inherit/propagate _default_manager as well, as it needs to be polymorphic.

    The current implementation below is an experiment to solve this
    problem with a very simplistic approach: We unconditionally
    inherit/propagate any and all managers (using _copy_to_model),
    as long as they are defined on polymorphic models
    (the others are left alone).

    Like Django ModelBase, we special-case _default_manager:
    if there are any user-defined managers, it is set to the first of these.

    We also require that _default_manager as well as any user defined
    polymorphic managers produce querysets that are derived from
    PolymorphicQuerySet.

    We also replace the parent/child relation field descriptors with versions that will
    use non-polymorphic querysets.

    If we have inheritance of the form ModelA -> ModelB ->ModelC then
    Django creates accessors like this:
    - ModelA: modelb
    - ModelB: modela_ptr, modelb, modelc
    - ModelC: modela_ptr, modelb, modelb_ptr, modelc

    These accessors allow Django (and everyone else) to travel up and down
    the inheritance tree for the db object at hand. This is important for deletion among
    other things.
    """

    def __new__(cls, model_name, bases, attrs, **kwargs):
        # create new model
        new_class = super().__new__(cls, model_name, bases, attrs, **kwargs)

        if new_class._meta.base_manager_name is None:
            # by default, use polymorphic manager as the base manager
            new_class._meta.base_manager_name = new_class._meta.default_manager_name or "objects"

        # ensure base_manager is a plain PolymorphicManager by resetting it if it
        # was not explicitly set and it defaults to a changed default_manager
        # the base class manager determination logic is complex enough that we prefer
        # to observe its application and correct rather than preempting it
        if (
            type(new_class._meta.default_manager) is not PolymorphicManager
            and new_class._meta.base_manager is new_class._meta.default_manager
        ):
            manager = PolymorphicManager()
            manager.name = "_base_manager"
            manager.model = new_class
            manager.auto_created = True
            new_class._meta.base_manager_name = None
            # write new manager to property cache
            new_class._meta.__dict__["base_manager"] = manager

        # wrap on_delete handlers of reverse relations back to this model with the
        # polymorphic deletion guard
        for fk in new_class._meta.fields:
            if isinstance(fk, (models.ForeignKey, models.OneToOneField)) and not isinstance(
                fk.remote_field.on_delete, PolymorphicGuard
            ):
                fk.remote_field.on_delete = PolymorphicGuard(fk.remote_field.on_delete)

        # replace the parent/child descriptors
        if new_class._meta.parents and not (new_class._meta.abstract or new_class._meta.proxy):
            # PolymorphicModel is guaranteed to be defined here
            from .models import PolymorphicModel

            def replace_inheritance_descriptors(model):
                for super_cls, field_to_super in model._meta.parents.items():
                    if issubclass(super_cls, PolymorphicModel):
                        if field_to_super is not None:
                            setattr(
                                new_class,
                                field_to_super.name,
                                NonPolymorphicForwardOneToOneDescriptor(field_to_super),
                            )
                            setattr(
                                super_cls,
                                field_to_super.remote_field.related_name
                                or field_to_super.remote_field.name,
                                NonPolymorphicReverseOneToOneDescriptor(
                                    field_to_super.remote_field
                                ),
                            )
                        else:  # pragma: no cover
                            # proxy models have no field_to_super because the relations
                            # are to the parent model - the else here should never
                            # happen b/c we filter out proxy models above
                            pass
                        replace_inheritance_descriptors(super_cls)

            replace_inheritance_descriptors(new_class)
        _clear_utility_caches()
        return new_class

    @property
    def base_objects(self):
        warnings.warn(
            "Using PolymorphicModel.base_objects is deprecated.\n"
            f"Use {self.__class__.__name__}.objects.non_polymorphic() instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self._base_objects

    @property
    def _base_objects(self):
        # Create a manager so the API works as expected. Just don't register it
        # anymore in the Model Meta, so it doesn't substitute our polymorphic
        # manager as default manager for the third level of inheritance when
        # that third level doesn't define a manager at all.
        manager = models.Manager()
        manager.name = "base_objects"
        manager.model = self
        return manager

    @property
    def _default_manager(cls):
        mgr = super()._default_manager
        if (
            check_dump
            and sys._getframe(1).f_globals.get("__name__")
            == "django.core.management.commands.dumpdata"
        ):
            # The downcasting of polymorphic querysets breaks dumpdata because it
            # expects to serialize multi-table models at each inheritance level.
            # dumpdata uses Model._default_manager to retrieve the objects by default
            # and uses Model._base_manager to retrieve objects if the --all flag is
            # specified. We need to make both of these managers polymorphic to satisfy
            # our contract that both Model.objects (_default_manager) is polymorphic and
            # reverse relations Other.related (_base_manager) to our polymorphic models
            # are also polymorphic.
            #
            # It would be best if load/dump data constructed its own managers like
            # migrations do, but it doesn't. The only way to get around this is to
            # detect when dumpdata is running and return the non-polymorphic manager in
            # that case. We do this here by inspecting the call stack and checking if
            # it came from the dumpdata command module. We use a CPython specific API
            # sys._getframe to inspect the call stack because it is very fast
            # (10s of nanoseconds) and disable the check if not on CPython
            # conceding that dumpdata will just not work in that case. It is important
            # that this check be fast because _default_manager is accessed very often.
            # inspect.stack() builds the entire stack frame and a bunch of complicated
            # datastructures - its use here should be avoided.
            #
            # Note that if you are stepping through this code in the debugger it will
            # be looking at the wrong frame because a bunch of debugging frames will be
            # on the top of the stack.
            return mgr.non_polymorphic() if isinstance(mgr, PolymorphicManager) else mgr
        return mgr

    @property
    def _base_manager(cls):
        mgr = super()._base_manager
        if (
            check_dump
            and sys._getframe(1).f_globals.get("__name__")
            == "django.core.management.commands.dumpdata"
        ):
            # base manager is used when the --all flag is passed - see analogous comment
            # for _default_manager
            return mgr.non_polymorphic() if isinstance(mgr, PolymorphicManager) else mgr
        return mgr
