import json

from django.db.models import ForeignKey
from django.utils.functional import cached_property
from rest_framework.serializers import ListSerializer, ModelSerializer


class PropertySerializerOptions(object):
    """Meta class options for PropertySerializer"""
    def __init__(self, meta):
        self.model = getattr(meta, 'model', None)
        self.read_only_props = getattr(meta, 'read_only_props', [])
        self.json_props = getattr(meta, 'json_props', [])
        self.exclude = ('id',)
        self.list_serializer_class = ListToDictSerializer

    @cached_property
    def parent_obj_field(self):
        # Automagically get the name of field containing relation to parent
        for field in self.model._meta.fields:
            if isinstance(field, ForeignKey):
                return field.name

        raise ValueError(
            '{0} misses relation to parent model'.format(self.model))


class ListToDictSerializer(ListSerializer):
    """This is how the DRF 3.x works. For many=True case it automatically
    returns a "List" serializer instead of original one
    (__new__ method is overriden). Thus, we need to teach it to work with
    {<prop_name>: <prop_value>,...} dict instead of standard
    [{'name': <prop_name>, 'value': <prop_value>}...].
    """
    default_error_messages = []

    @property
    def data(self):
        return super(ListSerializer, self).data

    def to_representation(self, value):
        # Since this is ListSerializer, it makes everything a list. We need
        # dict (<name>: <value>}, so convert it to dictionary.
        result_list = super(
            ListToDictSerializer, self).to_representation(value)

        result_dict = {}
        for pair in result_list:
            result_dict.update(pair)

        return result_dict

    def to_internal_value(self, data):
        data_list = [{name: value} for (name, value) in data.items()]
        return super(ListToDictSerializer, self).to_internal_value(data_list)

    def save(self):
        return [
            self.child.save(property_data)
            for property_data in self.validated_data
        ]


class PropertySerializer(ModelSerializer):
    """Allows to operate on properties of certain resource as dictionary

    Intended to be used as a base class for Property-like model serializer.

    We consider Property as a model of the following structure:

    class Property(model):
        parent_model: ForeignKey(...)
        name = CharField()
        value = CharField()

    After converting collection of properties to representation we'll get the
    following dict:

    {
        prop1.name: prop1.value,
        prop2.name: prop2.value,
        ....
    }

    And an input dictionary of the above structure will be converted to
    the collection of Property instances
    """

    _options_class = PropertySerializerOptions

    def __init__(self, *args, **kwargs):
        super(PropertySerializer, self).__init__(*args, **kwargs)
        self.opts = self._options_class(self.Meta)
        self.Meta.list_serializer_class = ListToDictSerializer

    def to_representation(self, obj):
        return {obj.name: self._get_value(obj)}

    def _get_value(self, obj):
        if obj.name in self.opts.json_props:
            return json.loads(obj.value)
        return obj.value

    def save(self, property_data=None):
        property_data = property_data or self.validated_data
        prop_name = property_data['name']

        # Try to find property by it's name.
        parent_obj_field = self.opts.parent_obj_field
        filters = {
            parent_obj_field: property_data[parent_obj_field],
            'name': prop_name
        }

        # If it already exists - update it's value. Otherwise - create a new
        # property.
        prop = self.Meta.model.objects.filter(**filters).first()

        if prop_name in self.opts.read_only_props:
            return prop

        if prop:
            prop.value = property_data['value']
            prop.save()
        else:
            prop = self.Meta.model.objects.create(**property_data)

        return prop

    def to_internal_value(self, data):
        data = self._to_extended_dict(data)

        # Handle JSON fields (value encoded as JSON).
        if data['name'] in self.opts.json_props:
            data['value'] = json.dumps(data['value'])

        data = self._add_parent_obj_field(data)
        return super(PropertySerializer, self).to_internal_value(data)

    def _to_extended_dict(self, data):
        """ Convert dictionary of properties:
        {<prop_name>: <prop_value>} ->
            {'name': <prop_name>, 'value': <prop_value>}
        """
        prop_name, prop_value = data.items()[0]
        return {'name': prop_name, 'value': prop_value}

    def _add_parent_obj_field(self, data):
        # Property requires ID of parent object (properties owner, e.g. User
        # for user's properties case). Take it from context, which is passed
        # from view.
        parent_obj_field = self.opts.parent_obj_field
        parent_id_field = parent_obj_field + '_id'
        data[parent_obj_field] = self.context.get(
            'parent_id') or self.context['view'].kwargs.get(
                parent_id_field)
        return data


class PropertiesOwnerSerializer(ModelSerializer):
    """Base class for "parent" serializers.

    If you want to use PropertySerializer with a parent model (e.g. as a
    nested serializer for User serializer), and have this field writable  -
    you have to inherit you parent serializer from this class, not
    ModelSerializer.
    """
    def create(self, validated_data):
        # Standard DRF .create() prohibits nested writable serializers, so
        # we do a trick - first remove properties from validated data, save
        # parent object, and then save properties separately.
        validated_data_minus_properties = dict(validated_data)

        # We can theoretically have more than one properties field.
        properties_fields = []
        for field in validated_data:
            if self._is_properties_field(field):
                del validated_data_minus_properties[field]
                properties_fields.append(field)

        instance = super(PropertiesOwnerSerializer, self).create(
            validated_data_minus_properties)

        # Now save properties separately.
        [self._save_properties(instance, field)
            for field in properties_fields]

        return instance

    def _is_properties_field(self, field_name):
        serializer = self.fields[field_name]
        return isinstance(serializer, ListSerializer) and isinstance(
            serializer.child, PropertySerializer)

    def update(self, instance, validated_data):
        for field, value in validated_data.items():
            if self._is_properties_field(field):
                self._save_properties(instance, field)
                continue
            setattr(instance, field, value)

        instance.save()
        return instance

    def _save_properties(self, instance, field):
        # Get correct serializer class.
        serializer = self.fields[field].child
        for property_data in self.validated_data[field]:
            property_data[serializer.opts.parent_obj_field] = instance
            serializer.save(property_data)

@@ -5941,50 +5941,8 @@
     
-# We can theoretically have more than one 
 prop
@@ -5951,46 +5951,21 @@
 ties
- 
+_
 field
-.%0A        properties_fields = %5B%5D
+ = None
 %0A   
@@ -6142,23 +6142,16 @@
 ield
-s.append(
+ = 
 field
-)
 %0A%0A  
@@ -6310,17 +6310,16 @@
         
-%5B
 self._sa
@@ -6345,40 +6345,8 @@
 nce,
- field)%0A            for field in
  pro
@@ -6362,10 +6362,9 @@
 ield
-s%5D
+)
 %0A%0A  
@@ -6976,48 +6976,8 @@
 d):%0A
-        # Get correct serializer class.%0A
     

Stop supporting several properties fields in ine parent serializer.