import warnings

import inflection
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.query import QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ParseError
from rest_framework.serializers import *  # noqa: F403

from rest_framework_json_api.exceptions import Conflict
from rest_framework_json_api.relations import ResourceRelatedField
from rest_framework_json_api.settings import json_api_settings
from rest_framework_json_api.utils import (
    get_included_resources,
    get_included_serializers,
    get_resource_type_from_instance,
    get_resource_type_from_model,
    get_resource_type_from_serializer
)


class ResourceIdentifierObjectSerializer(BaseSerializer):
    default_error_messages = {
        'incorrect_model_type': _(
            'Incorrect model type. Expected {model_type}, received {received_type}.'
        ),
        'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
        'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
    }

    model_class = None

    def __init__(self, *args, **kwargs):
        self.model_class = kwargs.pop('model_class', self.model_class)
        # this has no fields but assumptions are made elsewhere that self.fields exists.
        self.fields = {}
        super(ResourceIdentifierObjectSerializer, self).__init__(*args, **kwargs)

    def to_representation(self, instance):
        return {
            'type': get_resource_type_from_instance(instance),
            'id': str(instance.pk)
        }

    def to_internal_value(self, data):
        if data['type'] != get_resource_type_from_model(self.model_class):
            self.fail(
                'incorrect_model_type', model_type=self.model_class, received_type=data['type']
            )
        pk = data['id']
        try:
            return self.model_class.objects.get(pk=pk)
        except ObjectDoesNotExist:
            self.fail('does_not_exist', pk_value=pk)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data['pk']).__name__)


class SparseFieldsetsMixin(object):
    def __init__(self, *args, **kwargs):
        super(SparseFieldsetsMixin, self).__init__(*args, **kwargs)
        context = kwargs.get('context')
        request = context.get('request') if context else None

        if request:
            sparse_fieldset_query_param = 'fields[{}]'.format(
                get_resource_type_from_serializer(self)
            )
            try:
                param_name = next(
                    key for key in request.query_params if sparse_fieldset_query_param in key
                )
            except StopIteration:
                pass
            else:
                fieldset = request.query_params.get(param_name).split(',')
                # iterate over a *copy* of self.fields' underlying OrderedDict, because we may
                # modify the original during the iteration.
                # self.fields is a `rest_framework.utils.serializer_helpers.BindingDict`
                for field_name, field in self.fields.fields.copy().items():
                    if field_name == api_settings.URL_FIELD_NAME:  # leave self link there
                        continue
                    if field_name not in fieldset:
                        self.fields.pop(field_name)


class IncludedResourcesValidationMixin(object):
    def __init__(self, *args, **kwargs):
        context = kwargs.get('context')
        request = context.get('request') if context else None
        view = context.get('view') if context else None

        def validate_path(serializer_class, field_path, path):
            serializers = get_included_serializers(serializer_class)
            if serializers is None:
                raise ParseError('This endpoint does not support the include parameter')
            this_field_name = inflection.underscore(field_path[0])
            this_included_serializer = serializers.get(this_field_name)
            if this_included_serializer is None:
                raise ParseError(
                    'This endpoint does not support the include parameter for path {}'.format(
                        path
                    )
                )
            if len(field_path) > 1:
                new_included_field_path = field_path[1:]
                # We go down one level in the path
                validate_path(this_included_serializer, new_included_field_path, path)

        if request and view:
            included_resources = get_included_resources(request)
            for included_field_name in included_resources:
                included_field_path = included_field_name.split('.')
                this_serializer_class = view.get_serializer_class()
                # lets validate the current path
                validate_path(this_serializer_class, included_field_path, included_field_name)

        super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs)


class SerializerMetaclass(SerializerMetaclass):

    @classmethod
    def _get_declared_fields(cls, bases, attrs):
        fields = super()._get_declared_fields(bases, attrs)
        for field_name, field in fields.items():
            if isinstance(field, BaseSerializer) and \
                    not json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE:
                clazz = '{}.{}'.format(attrs['__module__'], attrs['__qualname__'])
                if isinstance(field, ListSerializer):
                    nested_class = type(field.child).__name__
                else:
                    nested_class = type(field).__name__

                warnings.warn(DeprecationWarning(
                    "Rendering nested serializer as relationship is deprecated. "
                    "Use `ResourceRelatedField` instead if {} in serializer {} should remain "
                    "a relationship. Otherwise set "
                    "JSON_API_SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE to True to render nested "
                    "serializer as nested json attribute".format(nested_class, clazz)))
        return fields


# If user imports serializer from here we can catch class definition and check
# nested serializers for depricated use.
class Serializer(
    IncludedResourcesValidationMixin, SparseFieldsetsMixin, Serializer,
    metaclass=SerializerMetaclass
):
    pass


class HyperlinkedModelSerializer(
        IncludedResourcesValidationMixin, SparseFieldsetsMixin, HyperlinkedModelSerializer,
        metaclass=SerializerMetaclass
):
    """
    A type of `ModelSerializer` that uses hyperlinked relationships instead
    of primary key relationships. Specifically:

    * A 'url' field is included instead of the 'id' field.
    * Relationships to other instances are hyperlinks, instead of primary keys.

    Included Mixins:

    * A mixin class to enable sparse fieldsets is included
    * A mixin class to enable validation of included resources is included
    """


class ModelSerializer(IncludedResourcesValidationMixin, SparseFieldsetsMixin, ModelSerializer,
                      metaclass=SerializerMetaclass):
    """
    A `ModelSerializer` is just a regular `Serializer`, except that:

    * A set of default fields are automatically populated.
    * A set of default validators are automatically populated.
    * Default `.create()` and `.update()` implementations are provided.

    The process of automatically determining a set of serializer fields
    based on the model fields is reasonably complex, but you almost certainly
    don't need to dig into the implementation.

    If the `ModelSerializer` class *doesn't* generate the set of fields that
    you need you should either declare the extra/differing fields explicitly on
    the serializer class, or simply use a `Serializer` class.


    Included Mixins:

    * A mixin class to enable sparse fieldsets is included
    * A mixin class to enable validation of included resources is included
    """
    serializer_related_field = ResourceRelatedField

    def get_field_names(self, declared_fields, info):
        """
        We override the parent to omit explicity defined meta fields (such
        as SerializerMethodFields) from the list of declared fields
        """
        meta_fields = getattr(self.Meta, 'meta_fields', [])

        declared = OrderedDict()
        for field_name in set(declared_fields.keys()):
            field = declared_fields[field_name]
            if field_name not in meta_fields:
                declared[field_name] = field
        fields = super(ModelSerializer, self).get_field_names(declared, info)
        return list(fields) + list(getattr(self.Meta, 'meta_fields', list()))

    def to_representation(self, instance):
        """
        Object instance -> Dict of primitive datatypes.
        """
        ret = OrderedDict()
        readable_fields = [
            field for field in self.fields.values()
            if not field.write_only
        ]

        for field in readable_fields:
            try:
                field_representation = self._get_field_representation(field, instance)
                ret[field.field_name] = field_representation
            except SkipField:
                continue

        return ret

    def _get_field_representation(self, field, instance):
        request = self.context.get('request')
        is_included = field.source in get_included_resources(request)
        render_nested_as_attribute = json_api_settings.SERIALIZE_NESTED_SERIALIZERS_AS_ATTRIBUTE
        if not is_included and \
                isinstance(field, ModelSerializer) and \
                hasattr(instance, field.source + '_id') and \
                not render_nested_as_attribute:
            attribute = getattr(instance, field.source + '_id')

            if attribute is None:
                return None

            resource_type = get_resource_type_from_serializer(field)
            if resource_type:
                return OrderedDict([('type', resource_type), ('id', attribute)])

        attribute = field.get_attribute(instance)

        # We skip `to_representation` for `None` values so that fields do
        # not have to explicitly deal with that case.
        #
        # For related fields with `use_pk_only_optimization` we need to
        # resolve the pk value.
        check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
        if check_for_none is None:
            return None
        else:
            return field.to_representation(attribute)


class PolymorphicSerializerMetaclass(SerializerMetaclass):
    """
    This metaclass ensures that the `polymorphic_serializers` is correctly defined on a
    `PolymorphicSerializer` class and make a cache of model/serializer/type mappings.
    """

    def __new__(cls, name, bases, attrs):
        new_class = super(PolymorphicSerializerMetaclass, cls).__new__(cls, name, bases, attrs)

        # Ensure initialization is only performed for subclasses of PolymorphicModelSerializer
        # (excluding PolymorphicModelSerializer class itself).
        parents = [b for b in bases if isinstance(b, PolymorphicSerializerMetaclass)]
        if not parents:
            return new_class

        polymorphic_serializers = getattr(new_class, 'polymorphic_serializers', None)
        if not polymorphic_serializers:
            raise NotImplementedError(
                "A PolymorphicModelSerializer must define a `polymorphic_serializers` attribute.")
        serializer_to_model = {
            serializer: serializer.Meta.model for serializer in polymorphic_serializers}
        model_to_serializer = {
            serializer.Meta.model: serializer for serializer in polymorphic_serializers}
        type_to_serializer = {
            get_resource_type_from_serializer(serializer): serializer for
            serializer in polymorphic_serializers}
        new_class._poly_serializer_model_map = serializer_to_model
        new_class._poly_model_serializer_map = model_to_serializer
        new_class._poly_type_serializer_map = type_to_serializer
        new_class._poly_force_type_resolution = True

        # Flag each linked polymorphic serializer to force type resolution based on instance
        for serializer in polymorphic_serializers:
            serializer._poly_force_type_resolution = True

        return new_class


class PolymorphicModelSerializer(ModelSerializer, metaclass=PolymorphicSerializerMetaclass):
    """
    A serializer for polymorphic models.
    Useful for "lazy" parent models. Leaves should be represented with a regular serializer.
    """
    def get_fields(self):
        """
        Return an exhaustive list of the polymorphic serializer fields.
        """
        if self.instance not in (None, []):
            if not isinstance(self.instance, QuerySet):
                serializer_class = self.get_polymorphic_serializer_for_instance(self.instance)
                return serializer_class(self.instance, context=self.context).get_fields()
            else:
                raise Exception("Cannot get fields from a polymorphic serializer given a queryset")
        return super(PolymorphicModelSerializer, self).get_fields()

    @classmethod
    def get_polymorphic_serializer_for_instance(cls, instance):
        """
        Return the polymorphic serializer associated with the given instance/model.
        Raise `NotImplementedError` if no serializer is found for the given model. This usually
        means that a serializer is missing in the class's `polymorphic_serializers` attribute.
        """
        try:
            return cls._poly_model_serializer_map[instance._meta.model]
        except KeyError:
            raise NotImplementedError(
                "No polymorphic serializer has been found for model {}".format(
                    instance._meta.model.__name__))

    @classmethod
    def get_polymorphic_model_for_serializer(cls, serializer):
        """
        Return the polymorphic model associated with the given serializer.
        Raise `NotImplementedError` if no model is found for the given serializer. This usually
        means that a serializer is missing in the class's `polymorphic_serializers` attribute.
        """
        try:
            return cls._poly_serializer_model_map[serializer]
        except KeyError:
            raise NotImplementedError(
                "No polymorphic model has been found for serializer {}".format(serializer.__name__))

    @classmethod
    def get_polymorphic_serializer_for_type(cls, obj_type):
        """
        Return the polymorphic serializer associated with the given type.
        Raise `NotImplementedError` if no serializer is found for the given type. This usually
        means that a serializer is missing in the class's `polymorphic_serializers` attribute.
        """
        try:
            return cls._poly_type_serializer_map[obj_type]
        except KeyError:
            raise NotImplementedError(
                "No polymorphic serializer has been found for type {}".format(obj_type))

    @classmethod
    def get_polymorphic_model_for_type(cls, obj_type):
        """
        Return the polymorphic model associated with the given type.
        Raise `NotImplementedError` if no model is found for the given type. This usually
        means that a serializer is missing in the class's `polymorphic_serializers` attribute.
        """
        return cls.get_polymorphic_model_for_serializer(
            cls.get_polymorphic_serializer_for_type(obj_type))

    @classmethod
    def get_polymorphic_types(cls):
        """
        Return the list of accepted types.
        """
        return cls._poly_type_serializer_map.keys()

    def to_representation(self, instance):
        """
        Retrieve the appropriate polymorphic serializer and use this to handle representation.
        """
        serializer_class = self.get_polymorphic_serializer_for_instance(instance)
        return serializer_class(instance, context=self.context).to_representation(instance)

    def to_internal_value(self, data):
        """
        Ensure that the given type is one of the expected polymorphic types, then retrieve the
        appropriate polymorphic serializer and use this to handle internal value.
        """
        received_type = data.get('type')
        expected_types = self.get_polymorphic_types()
        if received_type not in expected_types:
            raise Conflict(
                'Incorrect relation type. Expected on of [{expected_types}], '
                'received {received_type}.'.format(
                    expected_types=', '.join(expected_types), received_type=received_type))
        serializer_class = self.get_polymorphic_serializer_for_type(received_type)
        self.__class__ = serializer_class
        return serializer_class(self.instance, data, context=self.context,
                                partial=self.partial).to_internal_value(data)
