# -*- coding: utf-8 -*-
"""
PolymorphicModel Meta Class
"""
import inspect
import os
import sys
import warnings

import django
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.manager import ManagerDescriptor

from .managers import PolymorphicManager
from .query import PolymorphicQuerySet

# 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"]

DUMPDATA_COMMAND = os.path.join(
    "django", "core", "management", "commands", "dumpdata.py"
)


class ManagerInheritanceWarning(RuntimeWarning):
    pass


###################################################################################
# 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.
    """

    def __new__(self, model_name, bases, attrs):
        # print; print '###', model_name, '- bases:', bases

        # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
        if not attrs and model_name == "NewBase":
            return super(PolymorphicModelBase, self).__new__(
                self, model_name, bases, attrs
            )

        # Make sure that manager_inheritance_from_future is set, since django-polymorphic 1.x already
        # simulated that behavior on the polymorphic manager to all subclasses behave like polymorphics
        if django.VERSION < (2, 0):
            if "Meta" in attrs:
                if not hasattr(attrs["Meta"], "manager_inheritance_from_future"):
                    attrs["Meta"].manager_inheritance_from_future = True
            else:
                attrs["Meta"] = type(
                    "Meta", (object,), {"manager_inheritance_from_future": True}
                )

        # create new model
        new_class = self.call_superclass_new_method(model_name, bases, attrs)

        # check if the model fields are all allowed
        self.validate_model_fields(new_class)

        # validate resulting default manager
        if not new_class._meta.abstract and not new_class._meta.swapped:
            self.validate_model_manager(new_class.objects, model_name, "objects")

        # for __init__ function of this class (monkeypatching inheritance accessors)
        new_class.polymorphic_super_sub_accessors_replaced = False

        # determine the name of the primary key field and store it into the class variable
        # polymorphic_primary_key_name (it is needed by query.py)
        for f in new_class._meta.fields:
            if f.primary_key and type(f) != models.OneToOneField:
                new_class.polymorphic_primary_key_name = f.name
                break

        return new_class

    @classmethod
    def call_superclass_new_method(self, model_name, bases, attrs):
        """call __new__ method of super class and return the newly created class.
        Also work around a limitation in Django's ModelBase."""
        # There seems to be a general limitation in Django's app_label handling
        # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
        # We run into this problem if polymorphic.py is located in a top-level directory
        # which is directly in the python path. To work around this we temporarily set
        # app_label here for PolymorphicModel.
        meta = attrs.get("Meta", None)
        do_app_label_workaround = (
            meta
            and attrs["__module__"] == "polymorphic"
            and model_name == "PolymorphicModel"
            and getattr(meta, "app_label", None) is None
        )

        if do_app_label_workaround:
            meta.app_label = "poly_dummy_app_label"
        new_class = super(PolymorphicModelBase, self).__new__(
            self, model_name, bases, attrs
        )
        if do_app_label_workaround:
            del meta.app_label
        return new_class

    @classmethod
    def validate_model_fields(self, new_class):
        "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
        for f in new_class._meta.fields:
            if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS:
                e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
                raise AssertionError(e % (new_class.__name__, f.name))

    @classmethod
    def validate_model_manager(self, manager, model_name, manager_name):
        """check if the manager is derived from PolymorphicManager
        and its querysets from PolymorphicQuerySet - throw AssertionError if not"""

        if not issubclass(type(manager), PolymorphicManager):
            if django.VERSION < (2, 0):
                extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
            else:
                extra = ""
            e = (
                'PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
                " PolymorphicManager.{extra} to support retrieving subclasses".format(
                    model_name, manager_name, type(manager).__name__, extra=extra
                )
            )
            warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
            return manager

        if not getattr(manager, "queryset_class", None) or not issubclass(
            manager.queryset_class, PolymorphicQuerySet
        ):
            e = (
                'PolymorphicModel: "{0}.{1}" has been instantiated with a queryset class '
                "which is not a subclass of PolymorphicQuerySet (which is required)".format(
                    model_name, manager_name
                )
            )
            warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
        return manager

    @property
    def base_objects(self):
        warnings.warn(
            "Using PolymorphicModel.base_objects is deprecated.\n"
            "Use {0}.objects.non_polymorphic() instead.".format(
                self.__class__.__name__
            ),
            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(self):
        if len(sys.argv) > 1 and sys.argv[1] == "dumpdata":
            # TODO: investigate Django how this can be avoided
            # hack: a small patch to Django would be a better solution.
            # Django's management command 'dumpdata' relies on non-polymorphic
            # behaviour of the _default_manager. Therefore, we catch any access to _default_manager
            # here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
            # Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such.
            # (non-polymorphic default manager is 'base_objects' for polymorphic models).
            # This way we don't need to patch django.core.management.commands.dumpdata
            # for all supported Django versions.
            frm = inspect.stack()[
                1
            ]  # frm[1] is caller file name, frm[3] is caller function name
            if DUMPDATA_COMMAND in frm[1]:
                return self._base_objects

        manager = super(PolymorphicModelBase, self)._default_manager
        if not isinstance(manager, PolymorphicManager):
            warnings.warn(
                "{0}._default_manager is not a PolymorphicManager".format(
                    self.__class__.__name__
                ),
                ManagerInheritanceWarning,
            )

        return manager
