import logging
from copy import deepcopy
from pymongo.errors import OperationFailure

from glasskit import ctx
from glasskit.uorm.errors import DoNotSave
from ..db import db
from .fields import ObjectIdField
from .meta import MetaModel
from .hooks import BaseModelHook


class BaseModel(MetaModel):

    __hooks_classes__ = None

    USE_INITIAL_STATE = False

    _id: ObjectIdField()

    def __init__(self, attrs=None, **kwargs):
        if attrs is None:
            attrs = {}

        for field in self.__fields__:
            value = attrs.get(field, self.__def_values__.get(field, None))
            if callable(value):
                value = value()
            self.__dict__[field] = value
        self._initial_state = None
        self.__set_initial_state()
        self.__hooks__ = []
        if self.__hooks_classes__:
            for hook_class in self.__hooks_classes__:
                hook_inst = hook_class.on_model_init(self)
                if hook_inst:
                    self.__hooks__.append(hook_inst)

    def validate(self):
        for name, func in self.__validators__.items():
            value = self.__dict__[name]
            func(value)

    @property
    def is_new(self):
        return self._id is None

    def __set_initial_state(self):
        if self.USE_INITIAL_STATE:
            self.__dict__["_initial_state"] = self.setup_initial_state()

    def setup_initial_state(self):
        # this method is highly ineffective and is higlhy recommended
        # to be overriden
        return deepcopy(self.to_dict(self.__fields__))

    @classmethod
    def register_model_hook(cls, model_hook_class, *args, **kwargs):
        if not issubclass(model_hook_class, BaseModelHook):
            raise TypeError("Invalid hook class")
        if cls.__hooks_classes__ is None:
            cls.__hooks_classes__ = set()
        if model_hook_class not in cls.__hooks_classes__:
            cls.__hooks_classes__.add(model_hook_class)
            model_hook_class.on_hook_register(cls, *args, **kwargs)
            ctx.log.debug(
                "Registered hook %s for model %s",
                model_hook_class.__name__,
                cls.__name__,
            )

    @classmethod
    def unregister_model_hook(cls, model_hook_class):
        if cls.__hooks_classes__ is None:
            return
        if model_hook_class in cls.__hooks_classes__:
            cls.__hooks_classes__.remove(model_hook_class)
            model_hook_class.on_hook_unregister(cls)

    @classmethod
    def clear_hooks(cls):
        if cls.__hooks_classes__ is None:
            return
        for hook_class in cls.__hooks_classes__.copy():
            cls.unregister_model_hook(hook_class)

    def _before_save(self):
        pass

    def _before_validation(self):
        pass

    def _before_delete(self):
        pass

    def _after_save(self, is_new):
        pass

    def _after_delete(self):
        pass

    def _save_to_db(self):
        pass

    def _delete_from_db(self):
        pass

    def invalidate(self, **kwargs):
        pass

    def destroy(self, skip_callback=False, invalidate_cache=True):
        if self.is_new:
            return
        if not skip_callback:
            self._before_delete()

        if invalidate_cache:
            self.invalidate()

        self._delete_from_db()
        if not skip_callback:
            self._after_delete()

        self._id = None
        for hook in self.__hooks__:
            try:
                hook.on_model_destroy(self)
            except Exception as e:
                ctx.log.error(
                    "error executing destroy hook %s on model %s(%s): %s",
                    hook.__class__.__name__,
                    self.__class__.__name__,
                    self._id,
                    e,
                )
        return self

    def save(self, skip_callback=False, invalidate_cache=True):
        is_new = self.is_new

        if not skip_callback:
            try:
                self._before_validation()
            except DoNotSave:
                return
        self.validate()

        if not skip_callback:
            try:
                self._before_save()
            except DoNotSave:
                return

        if invalidate_cache:
            self.invalidate()

        self._save_to_db()

        for hook in self.__hooks__:
            try:
                hook.on_model_save(self, is_new)
            except Exception as e:
                ctx.log.error(
                    "error executing save hook %s on model %s(%s): %s",
                    hook.__class__.__name__,
                    self.__class__.__name__,
                    self._id,
                    e,
                )

        self.__set_initial_state()
        if not skip_callback:
            self._after_save(is_new)

        return self

    def __repr__(self):
        attributes = ["%s=%r" % (a, getattr(self, a)) for a in self.__fields__]
        return "%s(\n    %s\n)" % (self.__class__.__name__, ",\n    ".join(attributes))

    def __eq__(self, other):
        if self.__class__ != other.__class__:
            return False
        for field in self.__fields__:
            if hasattr(self, field):
                if not hasattr(other, field):
                    return False
                if getattr(self, field) != getattr(other, field):
                    return False
            elif hasattr(other, field):
                return False
        return True

    def __ne__(self, other):
        return not self.__eq__(other)

    def to_dict(self, fields=None, include_restricted=False):
        if fields is None:
            fields = self.__fields__

        result = {}
        for field in fields:
            if field in self.__restricted_fields__ and not include_restricted:
                continue
            if not hasattr(self, field):
                continue
            value = getattr(self, field)
            if callable(value):
                continue
            result[field] = value
        return result

    @classmethod
    def create(cls, **attrs):
        return cls(attrs)

    @classmethod
    def _ctor(cls, attrs, **kwargs):
        return cls(attrs, **kwargs)

    @classmethod
    def __get_possible_databases(cls):
        return [db.meta]

    @classmethod
    def ensure_indexes(cls, loud=False, overwrite=False):
        for keys, options in cls.__def_indexes__:
            for db in cls.__get_possible_databases():
                try:
                    db.conn[cls.collection].create_index(keys, **options)
                except OperationFailure as e:
                    if (
                        e.details.get("codeName") == "IndexOptionsConflict"
                        or e.details.get("code") == 85
                    ):
                        if overwrite:
                            if loud:
                                ctx.log.debug("Dropping index %s as conflicting", keys)
                            db.conn[cls.collection].drop_index(keys)
                            if loud:
                                ctx.log.debug(
                                    "Creating index with options: %s, %s", keys, options
                                )
                            db.conn[cls.collection].create_index(keys, **options)
                        else:
                            ctx.log.error(
                                "Index %s conflicts with an existing one, use overwrite param to fix it",
                                keys,
                            )

    def _reload_from_obj(self, obj):
        for field in self.__fields__:
            if field == "_id":
                continue
            value = getattr(obj, field)
            self.__dict__[field] = value
