from django.apps import apps

from django.db import models, transaction
from django.db.models import Q

from django.db.models.fields.related import OneToOneField, OneToOneRel
from django.db.models.fields.related import ManyToManyField, ManyToManyRel
from django.db.models.fields.related import ManyToOneRel
from django.db.models.fields.related import ForeignKey

# from django.db.models.fields.related_descriptors import \
#     ReverseOneToOneDescriptor, ReverseManyToOneDescriptor

from .utils.adapter_base import AdapterBase
from .utils.column_type import ColumnType
from .utils.relation_type import RelationType


#--- start debug tool ----------------------------------------------------------
from django.db import connection
from functools import wraps

class _DebugQueries(object):
    
    def start(self):
        self.start_query_count = len(connection.queries)
        
    def end(self):
        print('+'*100)
        for i, q in enumerate(connection.queries[self.start_query_count:]):
            print('-'*80)
            print(i, q['sql'])
        print('*'*100, flush=True)
        
    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end()

dq = _DebugQueries()

def _dq_decorator(func):
    @wraps(func)
    def dq_decorated(self, *args, **kwargs):
        global dq
        with dq:
            return func(self, *args, **kwargs)
            
    return dq_decorated
#--- end debug tool ------------------------------------------------------------        


class DjangoAdapter(AdapterBase):
    """ An adapter for working with Django ORM.
    
    The `adapter_settings` is a dictionary with next values:
    
        - *using* is a database name (a key from `DATABASES` dictionary in the
          "settings.py" file of a Django project), if it isn't provided
          "default" database is used.
    
    .. note::
    
        If you are going to use transactions provided by this library,
        set `autocommit` for the database to `False`:
        
            .. code-block:: Python
        
                from django.db import transaction
                from save_to_db import Persister
                from save_to_db.adapters import DjangoAdapter
                
                persister = Persister(DjangoAdapter, adapter_settings={})
                
                # Next function takes a `using` argument which should be the
                # name of a database. If it isn’t provided, Django uses the
                # "default" database.
                transaction.set_autocommit(False, using='database_name')
                
                try:
                    persister.persist(save_to_db_item)
                    persister.commit()
                except:
                    persister.rollback()
        
        Alternativy, you can use Django transactions directly:
        
            .. code-block:: Python
            
                from django.db import transaction
                from save_to_db import Persister
                from save_to_db.adapters import DjangoAdapter
                
                persister = Persister(DjangoAdapter, adapter_settings={})
                
                with transaction.atomic():
                    persister.persist(save_to_db_item)
    """
    
    COMPOSITE_KEYS_SUPPORTED = False
    REVERSE_MODEL_AUTOUPDATE_SUPPORTED = False
    SAVE_MODEL_BEFORE_COMMIT = True
    
    
    def __init__(self, adapter_settings):
        super().__init__(adapter_settings)
        self.using = adapter_settings.get('using', 'default')
    
    
    #--- general methods -------------------------------------------------------
    
    @classmethod
    def is_usable(cls, model_cls):
        return issubclass(model_cls, models.Model)
    
    
    def commit(self):
        transaction.commit(using=self.using)


    def rollback(self):
        transaction.rollback(using=self.using)
    
    
    @classmethod
    def iter_fields(cls, model_cls):
        foreign_key_classes = (ForeignKey, ManyToOneRel,
                               OneToOneField, OneToOneRel,
                               ManyToManyField, ManyToManyRel)
        
        type_to_const = (
            (models.BinaryField, ColumnType.BINARY),
            (models.TextField, ColumnType.TEXT),
            (models.CharField, ColumnType.STRING),
            ((models.AutoField, models.IntegerField), ColumnType.INTEGER),
            ((models.BooleanField,
              models.NullBooleanField), ColumnType.BOOLEAN),
            (models.DateTimeField, ColumnType.DATETIME),
            (models.FloatField, ColumnType.FLOAT),
            (models.DecimalField, ColumnType.DECIMAL),
            (models.DateField, ColumnType.DATE),
            (models.TimeField, ColumnType.TIME),
            (models.DateTimeField, ColumnType.DATETIME),
        )
        for field in model_cls._meta.get_fields():
            if isinstance(field, foreign_key_classes):
                continue
            
            yield_type = ColumnType.OTHER
            for field_class, column_type in type_to_const:
                if isinstance(field, field_class):
                    yield_type = column_type
                    break
            yield field.name, yield_type
    
    
    @classmethod
    def iter_relations(cls, model_cls):
        foreign_key_classes = (ForeignKey, ManyToOneRel,
                               OneToOneField, OneToOneRel,
                               ManyToManyField, ManyToManyRel)
        
        type_to_const = [
            [(OneToOneField, OneToOneRel), RelationType.ONE_TO_ONE],
            [ForeignKey, RelationType.MANY_TO_ONE],
            [ManyToOneRel, RelationType.ONE_TO_MANY],
            [(ManyToManyField,
              ManyToManyRel), RelationType.MANY_TO_MANY],
        ]
        for field in model_cls._meta.get_fields():
            if not isinstance(field, foreign_key_classes):
                continue
            
            for field_class, column_type in type_to_const:
                if isinstance(field, field_class):
                    yield_type = column_type
                    break
            
            field_name = field.name
            if hasattr(field, 'get_accessor_name'):
                field_name = field.get_accessor_name()
            
            remote_field_name = None
            if not hasattr(field.remote_field, 'is_hidden') or \
                    not field.remote_field.is_hidden():
                remote_field_name = field.remote_field.name
                if hasattr(field.remote_field, 'get_accessor_name'):
                    remote_field_name = field.remote_field.get_accessor_name()
            
            yield field_name, field.related_model, yield_type, \
                remote_field_name
    
    
    @classmethod
    def iter_required_fields(cls, model_cls):
        for field in model_cls._meta.get_fields():
            if isinstance(field, (models.AutoField, ManyToManyField)):
                continue
            if not field.null and field.default is models.NOT_PROVIDED:
                yield field.name
    
    
    @classmethod
    def iter_unique_field_combinations(cls, model_cls):
        for unique_constraint_fields in model_cls._meta.unique_together:
            yield unique_constraint_fields
            
        for field in model_cls._meta.get_fields():
            if hasattr(field, 'field'):  # relation
                if isinstance(field, (OneToOneField,
                                      OneToOneRel)):
                    yield {field.name,}  # one-to-one always unique
                    continue
                field = field.field
            if field.unique:
                yield {field.name,}
    
    
    @classmethod
    def get_table_fullname(cls, model_cls):
        return model_cls._meta.db_table
    
    
    def get_model_cls_by_table_fullname(self, name):
        for models in apps.all_models.values():
            for model in models.values():
                if self.get_table_fullname(model) == name:
                    return model
    
    
    def iter_all_models(self):
        for models in apps.all_models.values():
            for model in models.values():
                yield model
    
    
    #--- methods for working with items ----------------------------------------
    

    def get(self, items_and_fkeys):

        #--- first getting item models from database ---------------------------
        if not items_and_fkeys:
            return []
        
        all_items_filters = Q()
        getters = items_and_fkeys[0][0].getters
        
        for item, fkeys in items_and_fkeys:
            one_item_filters = Q()
            for group in getters:
                group_filters = Q()
                skip_group_filters = False
                # in case related item was not in database
                for field_name in group:
                    if field_name not in item:
                        skip_group_filters = True
                        break
                    
                    if field_name in item.fields:
                        field_value = item[field_name]
                        group_filters &= Q(**{field_name: field_value})
                    elif field_name in item.relations:
                        related_models = fkeys.get(field_name)
                        if not related_models:
                            # failed to get or created related model before
                            skip_group_filters = True
                            break
                        
                        relation = item.relations[field_name]
                        if not relation['relation_type'].is_x_to_many():
                            group_filters &= Q(
                                **{field_name: related_models[0]})
                        else:
                            contains_any = Q()
                            for related_model in related_models:
                                contains_any |= Q(**{field_name: related_model})
                            group_filters &= contains_any
                
                if not skip_group_filters and len(group_filters):
                    one_item_filters |= group_filters
                
            if one_item_filters.children:
                all_items_filters |= one_item_filters
        
        if not len(all_items_filters):
            return []
        
        return item.model_cls.objects.db_manager(self.using) \
            .filter(all_items_filters).all()
    

    def delete(self, model):
        model.delete(using=self.using)
    

    def create_blank_model(self, model_cls):
        return model_cls()
    

    def add_related_models(self, model, fkey, related_models):
        getattr(model, fkey).add(*related_models)
                

    def clear_related_models(self, model, fkey):
        related = getattr(model, fkey)
        if hasattr(related, 'clear'): 
            getattr(model, fkey).clear()
        else:
            raise Exception('Cannot clear required field.')
        

    def related_x_to_many_exists(self, model, fkey):
        return getattr(model, fkey).exists()
    

    def related_x_to_many_contains(self, model, fkey, child_models):
        if not child_models:
            return []
        
        # no composite PK in Django
        pk_name = self.get_primary_key_names(model)[0]
        pk_list = [getattr(child_model, pk_name)
                   for child_model in child_models]
        contained_models = getattr(model, fkey).filter(**{
            '{}__in'.format(pk_name): pk_list
        })
        result = []
        for contained_model in contained_models:
            for child_model in child_models:
                if child_model == contained_model:
                    result.append(child_model)
                    break
                
        return result
    
    @classmethod
    def get_primary_key_names(cls, model_cls):
        return (model_cls._meta.pk.name,)
    

    def save_model(self, model):
        model.save(using=self.using)

    
    @classmethod
    def __get_select_and_keep_fielter(cls, selectors, keepers):
        total_select_filter = Q()
        for selectors_entry in selectors:
            select_filter_set = Q()
            for field_name, value in selectors_entry.items():
                select_filter_set &= Q(**{field_name: value})
            total_select_filter |= select_filter_set
        
        total_keep_filter = Q()
        for keepers_entry in keepers:
            keep_fielter_set = Q()
            for field_name, value in keepers_entry.items():
                keep_fielter_set |= ~Q(**{field_name: value})
            total_keep_filter &= keep_fielter_set
        
        if total_select_filter or total_keep_filter:
            return Q(total_select_filter, total_keep_filter)
        return None
    

    def execute_delete(self, model_cls, selectors, keepers):
        filters = self.__get_select_and_keep_fielter(selectors, keepers)
        if filters is None:
            return

        model_cls.objects.db_manager(self.using).filter(filters).delete()
    

    def execute_unref(self, parent, fkey, selectors, keepers):
        filters = self.__get_select_and_keep_fielter(selectors, keepers)
        if filters is None:
            return

        child_relation = getattr(parent, fkey)
        children = getattr(parent, fkey).filter(filters).all()
        child_relation.remove(*children)
    

    def get_related_x_to_many(self, model, fkey):
        return getattr(model, fkey).all()
    
    #--- methods for tests -----------------------------------------------------
    

    def get_all_models(self, model_cls, sort_key=None):
        models = list(model_cls.objects.db_manager(self.using).all())
        if sort_key:
            models.sort(key=sort_key)
        return models
