import io
import json
import uuid

from django.apps import apps
from django.forms.models import model_to_dict
from pprint import pprint
from django.db.models.fields.related import (ManyToManyField,
                                             ForeignKey)
from django.db.models.fields import (BooleanField,
                                     NullBooleanField,
                                     IntegerField,
                                     FloatField,
                                     PositiveIntegerField)
from django.db import transaction


class BaseSerializableInstance(object):

    def childrens(self):
        return self.obj._meta.fields_map

    def has_childrens(self):
        """
        boolean
        """
        if self.childrens():
            return True

    def remove_related_field(self, related_field):
        del(self.dict['object'][related_field])
        del(self.dict['object'][related_field+'_id'])
        return related_field

    def json(self, indent=2):
        return json.dumps(self.dict, indent=indent)

    def app_model(self, app_name, model_name):
        """
        Returns model class
        """
        return apps.get_model(app_label=app_name, model_name=model_name)


class SerializableInstance(BaseSerializableInstance):
    def __init__(self, obj,
                 excluded_fields=[],
                 excluded_childrens = [],
                 auto_fields = False,
                 change_uniques = True,
                 duplicate = True):
        """
        excluded_fields = autogenerated field like 'created', 'modified'
        excluded_childrens = names of relations to exclude (es: classname1, classname2)
        auto_fields = if True doesn't export auto_created, auto_now, auto_now_add
        change_uniques = if True changes unique indexes values to random ones
                         unique_values will be modified (be warn on this!)
        duplicate = force some condition do duplicate obj
        """
        self.obj = obj
        self.model = obj._meta.model
        self.model_name = obj._meta.object_name

        self.fields = obj._meta._forward_fields_map
        self.excluded_fields = excluded_fields
        self.auto_fields = auto_fields
        self.change_uniques = change_uniques

        if duplicate:
            self.change_uniques, self.auto_fields = True, False
        else:
            self.change_uniques, self.auto_fields = False, True

        self.duplicate = duplicate
        self.excluded_childrens = excluded_childrens

        self.dict = {
            'source_pk': obj.pk, # source object pk
            # if the object has to be cloned
            'duplicate': duplicate,
            'app_name': obj._meta.app_label,
            'model_name': obj._meta.object_name,
            'object': {},
            'fields': [],
            'm2m': [], # lists m2m field names
            # 'related_field' : None # this is need only for children obj
            # these will be added in Serialized Tree
            'childrens': [],
            }

    def get_serialized_value(self, obj, ofield):
        """

        """
        # everything to string for the first time
        value = ofield.value_to_string(obj)

        # better specification for numeric types
        if type(ofield) in (BooleanField,
                            NullBooleanField,
                            IntegerField,
                            FloatField,
                            PositiveIntegerField,
                            ForeignKey):
            value = ofield.value_from_object(obj)
        # specify others types here if needed
        #

        # M2M management
        if isinstance(ofield, ManyToManyField):
            # this way value become a collection
            value = []
            for i in getattr(self.obj, ofield.attname).all():
                if not ofield.attname in self.dict['m2m']:
                    self.dict['m2m'].append(ofield.attname)
                si = self.__class__(i,
                                    excluded_fields=self.excluded_fields,
                                    duplicate=self.duplicate)
                si.serialize_obj()
                value.append(si.dict)
            return value

        # unique override if needed
        if self.change_uniques:
            if ofield.unique:
                if type(value) == str:
                    value = value +'-'+uuid.uuid4().hex
                elif type(value) == int:
                    value = uuid.uuid4().int

        return value

    def exclude_field(self, field_name):
        """
        boolean, returns True if elements was added (first time)
        otherwise means that element was already excluded
        """
        if field_name not in self.excluded_fields:
            self.excluded_fields.append(field_name)
            return True

    def prepare_duplication(self):
        """
        filters out auto filed and pk
        """
        for field in self.fields:
            ofield = self.fields[field]

            if self.duplicate:
                if ofield.primary_key:
                    self.exclude_field(field)
                    continue

            if not self.auto_fields:
                # add others if needed
                if hasattr(ofield, 'auto_now') or \
                   hasattr(ofield, 'auto_now_add'):
                   if ofield.auto_now or ofield.auto_now_add:
                        self.exclude_field(field)
                        continue

    def serialize_obj(self):
        """

        """
        if self.duplicate:
            self.prepare_duplication()

        for field in self.fields:
            if field in self.excluded_fields: continue

            value = self.get_serialized_value(self.obj, self.fields[field])
            self.dict['fields'].append(field)
            if value:
                self.dict['object'][field] = value
        return self.dict

    def remove_duplicates(self, tree={}, depth=0):
        """
        if an object has multiple same class foreign keys,
        it appears more times in the tree dict!
        this method add a parameter "save" telling to saving methods
        which item must be saved or ignored (removing duplicates).
        "depth" value is taken as reference (highest value is the best!)
        """
        if not tree: tree=self.dict
        childrens = tree['childrens']
        for child in childrens:
            self.remove_duplicates(tree=child, depth=depth+1)
            if child['source_pk'] in self.depth_reference:
                if child['depth'] < self.depth_reference[child['source_pk']]:
                    child['save'] = False

    def serialize_tree(self, depth=0, depth_reference={}):
        self.serialize_obj()
        childrens = self.childrens()
        if self.obj.pk in depth_reference:
            if depth > depth_reference[self.obj.pk]:
                depth_reference[self.obj.pk] = depth
        else:
            depth_reference[self.obj.pk] = depth
        for i in childrens:
            if i in self.excluded_childrens:
                continue
            related_field = childrens[i].field.name
            method = getattr(childrens[i], 'get_accessor_name')
            method_name = method()
            children_items = []
            method_callable = hasattr(self.obj, method_name)
            if method_callable:
                method_called = getattr(self.obj, method_name)
                children_items = getattr(method_called, 'all')()
            d = []
            for child in children_items:
                si = self.__class__(child,
                                    excluded_fields=self.excluded_fields,
                                    excluded_childrens=self.excluded_childrens,
                                    duplicate=self.duplicate)
                si.serialize_tree(depth=depth+1, depth_reference=depth_reference)
                si.dict['related_field'] = related_field
                if self.duplicate:
                    si.remove_related_field(related_field)
                d.append(si.dict)
            self.dict['childrens'].extend(d)
        # add "depth" param
        self.dict['depth'] = depth
        # add default True "save" param
        self.dict['save'] = True
        self.depth_reference = depth_reference
        return self.dict

    def __str__(self):
        return 'Serializable: {} {}'.format(self.model_name, self.obj)

    def __repr__(self):
        return self.__str__()

class ImportableSerializedInstance(BaseSerializableInstance):

    def __init__(self, serialized_obj):
        if isinstance(serialized_obj, dict):
            self.dict = serialized_obj
        elif isinstance(serialized_obj, str):
            self.dict = json.loads(sertialized_obj)
        elif isinstance(serialized_obj, io.TextIOWrapper):
            self.dict = json.loads(sertialized_obj.read())
        else:
            raise Exception('Unknown serialized object format')

    def get_save_dict(self, model_obj, obj_dict):
        save_dict = {}
        for ofield in model_obj._meta.fields:
            # is it a relation?
            if isinstance(ofield, ForeignKey) and \
               ofield.attname in obj_dict['object'].keys() and \
               ofield.name in obj_dict['object'].keys():
                   save_dict[ofield.name] = ofield.related_model.objects.get(pk=obj_dict['object'][ofield.attname])
            elif ofield.name in obj_dict['object'].keys():
                save_dict[ofield.name] = obj_dict['object'][ofield.name]
        return save_dict

    def save_m2m(self, obj, m2ms):
        for m2m_key in m2ms:
            for m2m_child in m2ms[m2m_key]:
                m2m_app_name = m2m_child['app_name']
                m2m_model_name = m2m_child['model_name']
                m2m_model_obj = self.app_model(m2m_app_name, m2m_model_name)
                # print(m2m_model_obj, )
                m2m_child_obj = m2m_model_obj.objects.get(**m2m_child['object'])
                getattr(obj, m2m_key).add(m2m_child_obj)
                print('saved m2m: {} {} ({})'.format(m2m_app_name, m2m_model_name,obj))

    def save_object(self,
                    obj_dict,
                    custom_values={},
                    recursive_custom_values={},
                    **kwargs):
        """
        saves the single instance
        if parents: checks if children object has parent_name(key):None
        then made substitutes None with parent object if available, otherwise save without them
        returns ORM model object

        custom_values: values to save in object
        recursive_custom_values: values to save in object and childs
        """
        app_name = obj_dict['app_name']
        model_name = obj_dict['model_name']
        model_obj = self.app_model(app_name, model_name)

        # kwargs
        related_fields = kwargs.get('related_fields', {}).copy()
        parent_obj = kwargs.get('parent_obj', None)

        # se uno degli attributi ha oggetti innestati e type == m2m rimuovere attr, salvare e usare .add() sull'obj salvato per aggiungere gli m2m
        # move m2m definition to a private collection and then purge them from original object
        m2ms = { i:[e for e in obj_dict['object'][i]] for i in obj_dict['m2m']}

        # relation to the father
        if parent_obj:
            obj_dict['object'][obj_dict['related_field']] = parent_obj

        # save obj without optional m2m
        print('saving:', obj_dict['object'])
        # detect and fetch fk
        save_dict=self.get_save_dict(model_obj, obj_dict)

        print(save_dict)

        save_dict.update(custom_values)

        # save recursive custom values
        for k,v in recursive_custom_values.items():
            if k in obj_dict['fields']:
                save_dict[k] = v

        # if duplication create new object
        if obj_dict.get('duplicate'):
            obj = model_obj.objects.create(**save_dict)
        # else update existent object with dict data
        else:
            obj = model_obj.objects.get(pk=obj_dict.get('source_pk'))
            obj.__dict__.update(**save_dict)
            obj.save()

        # save parent related fields (if object hasn't them)
        for k,v in related_fields.items():
            if k == obj_dict.get('related_field'): continue
            # if k not in obj_dict['object']: continue
            setattr(obj, k, v)
            obj.save()

        print('saved: {} {} ({})'.format(app_name, model_name, obj.__dict__))
        print()

        # save each m2m
        self.save_m2m(obj, m2ms)

        # set related fields and pass them to childs
        if obj_dict.get('related_field'):
            related_fields[obj_dict['related_field']] = getattr(obj, obj_dict['related_field'])

        # recursive call
        for child in obj_dict['childrens']:
            # only if save==True, object is saved.
            # else it is ignored, is a duplicated of same item!
            if child['save']:
                self.save_object(child,
                                 recursive_custom_values=recursive_custom_values,
                                 parent_obj=obj,
                                 related_fields=related_fields)
        return obj

    @transaction.atomic
    def save(self, custom_values={}, recursive_custom_values={}):
        """
        save all the object tree in a transaction
        """
        # save_object and pass it in parents=[] for every children
        obj = self.save_object(obj_dict=self.dict,
                               custom_values=custom_values,
                               recursive_custom_values=recursive_custom_values)
        return obj
