from collections import OrderedDict
from copy import deepcopy
from itertools import chain, product
import logging

from django.db import IntegrityError
from django.db.models import Q

from staff.lib.db import atomic
from staff.lib.models import smart_model
from staff.lib.utils.reflection import methods_of, classproperty

from .. import exceptions, schema
from ..fields import PlainField
from ..fields.base import DomainObjectField
from ..permissions import RoleRegistry
from ..utils import expand

from .registry import domain_object_lists


__all__ = (
    'TimeStamped', 'SoftDeleted',
    'DomainObject', 'DomainObjectList',
    'SavableDomainObject', 'CreatableDomainObjectList',
)


logger = logging.getLogger(__name__)


class Manager(object):
    domain_object_class = None

    def __meta_init__(self, class_name, attribute_name):
        self.domain_object_class = class_name

    def get_domain_object_list(self, domain_object_list_class, role_registry,
                               user):
        return domain_object_list_class(user=user, role_registry=role_registry)

    def __call__(self, user, role_registry):
        domain_object_list_class = self.get_domain_object_list_class()
        domain_object_list = self.get_domain_object_list(
            domain_object_list_class, role_registry, user)
        return domain_object_list

    def get_domain_object_list_class(self):
        try:
            return domain_object_lists[self.domain_object_class]
        except KeyError:
            raise TypeError('%s is not an enumerable domain object' %
                            self.domain_object_class)


class DomainObjectMeta(type):
    __managers__ = None
    __fields__ = None

    def __new__(mcs, name, bases, attrs):
        cls = super(DomainObjectMeta, mcs).__new__(mcs, name, bases, attrs)

        for base in bases:
            base_items = chain(getattr(base, '__fields__', {}).items(),
                               getattr(base, '__managers__', {}).items())
            for attr, obj in base_items:
                copy = deepcopy(obj)
                setattr(cls, attr, copy)

        managers = {}
        fields = OrderedDict()

        items = [
            (attr, cls.__dict__[attr])
            for attr in cls.__dict__.keys()
            if not attr.startswith('_')
        ]
        items.sort(key=lambda x: getattr(x[1], 'creation_counter', -1))
        for attr, value in items:
            if hasattr(value, '__meta_init__'):
                value.__meta_init__(name, attr)
            if isinstance(value, Manager):
                managers[attr] = value
            if isinstance(value, DomainObjectField):
                fields[attr] = value

        cls.__managers__ = managers
        cls.__fields__ = fields

        return cls


class TimeStamped(metaclass=DomainObjectMeta):

    created_at = PlainField(schema_type=schema.TYPE.STRING, null=True)
    modified_at = PlainField(schema_type=schema.TYPE.STRING, null=True)


class SoftDeleted(metaclass=DomainObjectMeta):
    is_active = PlainField(schema_type=schema.TYPE.BOOLEAN)

    def soft_delete(self):
        self.is_active = False

    def soft_restore(self):
        self.is_active = True


class DomainObject(metaclass=DomainObjectMeta):
    """
    Domain object instance controller
    """
    __search_fields__ = ()
    __search_suffixes__ = ('icontains',)

    _schema = None
    model_class = None
    primary_key_field = 'id'

    objects = Manager()

    def __init__(self, user, model, role_registry):
        self.user = user
        self.model = model
        self.role_registry = role_registry
        self._init_roles()

    def _init_roles(self):
        self.roles = self.role_registry.get(self.user, self.model)

    def __eq__(self, other):
        res = isinstance(other, self.__class__) and self.model == other.model
        logger.debug('Comparing %r with %r: %r', self, other, res)
        return res

    @classmethod
    def get_model_class(cls):
        return smart_model(cls.model_class)

    @classmethod
    def translate_lookup(cls, lookup):
        for key, val in lookup.items():
            yield cls.translate_field(key, val)

    @classmethod
    def translate_fields(cls, fields):
        for field in fields:
            yield cls.translate_field(field).children[0][0]

    @classmethod
    def translate_field(cls, name, value=None):
        source_field = name
        is_inverted = name.startswith('-')
        if is_inverted:
            name = name[1:]

        split = name.replace('__', '.').split('.')
        attr = split[0]
        if attr == 'pk':
            attr = cls.primary_key_field
        nested = split[1:]

        # getattr would call __get__: it is not what we want to happen
        field = cls.__dict__[attr]

        key, value = field.translate_lookup(nested, value, source_field)
        if is_inverted:
            key = '-' + key

        if isinstance(value, (list, tuple)):
            if len(value) == 1:
                return Q(**{key: value[0]})

            if len(value) > 1:
                query = None
                for v in value:
                    q = Q(**{key: v})
                    if query is None:
                        query = q
                    else:
                        query |= q
                return query

        return Q(**{key: value})

    # noinspection PyMethodParameters
    @classproperty
    def schema(cls):
        if cls._schema is None:
            cls._schema = cls._get_schema()
        return cls._schema

    @classmethod
    def _get_schema(cls):
        return {
            'type': 'object',
            'additionalProperties': False,
            'properties': {
                name: field.schema
                for name, field in cls.__fields__.items()
            }
        }

    def _roles(self):
        return self.role_registry.get_bool_map(self.model)


class DomainObjectList(object):
    """
    Domain object list controller
    """

    domain_object_class = None
    role_registry_class = RoleRegistry
    lookup = None

    def __init__(self, user, role_registry, queryset=None, **kwargs):
        assert isinstance(role_registry, RoleRegistry)
        self.user = user
        self.role_registry = role_registry
        if queryset is not None:
            self.queryset = queryset
        else:
            self.queryset = self.get_base_queryset(user)
        self.context = kwargs

    @classmethod
    def get_base_queryset(cls, user):
        query = cls.role_registry_class.get_base_query(
            user, cls.domain_object_class.model_class
        )
        return query.get_queryset(cls.domain_object_class.get_model_class())

    @classmethod
    def _clone(cls, user=None, instance=None, role_registry=None):
        assert (user is not None) or (instance is not None)
        if instance is not None:
            return cls(
                user=instance.user,
                role_registry=instance.role_registry,
                queryset=instance.queryset,
                **instance.context
            )
        else:
            base_qs = cls.get_base_queryset(user)
            assert role_registry
            instance = cls(
                user=user,
                role_registry=role_registry,
                queryset=base_qs,
            )
            return instance

    def all(self):
        return self._clone(instance=self)

    def select_related(self, *fields):
        # TODO Костыльно сделал, только,
        # что бы починить тестинг, надо доработать
        clone = self._clone(instance=self)
        clone.queryset = clone.queryset.select_related(*fields)
        return clone

    def filter(self, **kwargs):
        clone = self._clone(instance=self)
        lookup = list(self.domain_object_class.translate_lookup(kwargs))
        clone.queryset = clone.queryset.filter(*lookup)
        return clone

    def exclude(self, **kwargs):
        clone = self._clone(instance=self)
        lookup = self.domain_object_class.translate_lookup(kwargs)
        clone.queryset = clone.queryset.exclude(*lookup)
        return clone

    def search(self, needle):
        if not needle:
            return self

        clone = self._clone(instance=self)
        queries = (
            Q(**{'{}__{}'.format(f, suffix): needle})
            for f, suffix
            in product(self.domain_object_class.__search_fields__,
                       self.domain_object_class.__search_suffixes__)
        )
        lookup = Q()
        for query in queries:
            lookup |= query
        clone.queryset = clone.queryset.filter(lookup)
        return clone

    def get(self, **kwargs):
        try:
            return self.filter(**kwargs)[0]
        except IndexError:
            raise exceptions.DoesNotExist(
                'No such object or insufficient permissions',
                data={
                    'lookup': kwargs, 'class': self.domain_object_class
                })

    def distinct(self):
        clone = self._clone(instance=self)
        clone.queryset = clone.queryset.distinct()
        return clone

    def order_by(self, *fields):
        clone = self._clone(instance=self)
        translated = list(self.domain_object_class.translate_fields(fields))
        clone.queryset = clone.queryset.order_by(*translated)
        return clone

    def __len__(self):
        return len(self.queryset)

    def wrap(self, model):
        return self.domain_object_class(self.user, model, self.role_registry)

    def __getitem__(self, item):
        model_or_list = self.queryset[item]
        if isinstance(model_or_list, list):
            return list(map(self.wrap, model_or_list))
        return self.wrap(model_or_list)

    def __iter__(self):
        for model in self.queryset:
            yield self.wrap(model)


class SavableDomainObject(DomainObject):
    id = PlainField(schema_type=schema.TYPE.NUMBER, null=True)

    def validate(self):
        for _, method in methods_of(self, r'^validate_\w+'):
            method()

    def validate_fields(self):
        for name, field in self.__fields__.items():
            value = getattr(self, name)
            field.validate(value)

    def save(self, force_insert=False, force_update=False):
        self.validate()
        try:
            # logger.debug('Going to save %s(%s)',
            #              self.model.__class__.__name__, self.model.__dict__)
            with atomic():
                self.pre_save(
                    force_insert=force_insert,
                    force_update=force_update,
                )
                self.model.save(
                    force_insert=force_insert,
                    force_update=force_update,
                )
                self.post_save(
                    force_insert=force_insert,
                    force_update=force_update,
                )
        except IntegrityError as e:
            if e.args[0].startswith('duplicate key'):  # postgres
                raise exceptions.DuplicateEntry(cause=e, data=e.args[0])
            elif 'null' in e.args[0]:  # postgres
                raise exceptions.ColumnCannotBeNull(cause=e)
            else:
                raise
        else:
            self.model._is_new_ = False
            self._init_roles()

    def pre_save(self, force_insert=False, force_update=False):
        self._save_related(
            force_insert=force_insert,
            force_update=force_update,
        )

    def post_save(self, force_insert=False, force_update=False):
        pass

    def update(self, data=None, **kwargs):
        data = expand(data or kwargs)
        unknown_fields = set(data) - set(self.__fields__)
        if unknown_fields:
            raise TypeError('Unknown fields: %s' % unknown_fields)

        for field_name in self.__fields__:
            try:
                value = data[field_name]
            except KeyError:
                continue
            else:
                setattr(self, field_name, value)

    def _get_related_saves(self):
        return sum(
            (field.get_related_saves(self)
             for field in self.__fields__.values()),
            ()
        )

    def _save_related(self, force_insert=False, force_update=False):
        for obj in self._get_related_saves():
            obj.save(force_insert=force_insert, force_update=force_update)


class CreatableDomainObjectList(DomainObjectList):
    def create(self, **kwargs):
        model_class = self.domain_object_class.get_model_class()
        model = model_class()
        model._is_new_ = True
        domain_object = self.domain_object_class(
            self.user, model, self.role_registry
        )
        domain_object.update(kwargs)
        domain_object.save(force_insert=True)
        domain_object._init_roles()
        self.role_registry.fill()
        return domain_object

    def restore_or_create(self, defaults=None, **kwargs):
        # to restore
        try:
            instance = (
                self.domain_object_class.objects(
                    self.user, self.role_registry
                )
                .get(**kwargs)
            )
        # Then try to create
        except exceptions.DoesNotExist:
            return self.create(**dict(
                chain(kwargs.items(), (defaults or {}).items())
            )), True

        else:
            instance.restore()
            return instance, False
