import copy
import logging
from datetime import datetime

from pymongo.write_concern import WriteConcern
from pymongo.read_preferences import ReadPreference
from django.conf import settings

from staff.lib.utils.diff import DictDiff
from staff.lib.mongodb import mongo, CollectionObjectGenerator

logger = logging.getLogger(__name__)

PRIMITIVE_TYPES = (int, float, str, type(None))


class MongoModelException(Exception):
    pass


class CollectionObject:
    # *args, **kwargs
    def __init__(self, collection, init_data, is_new, **kwargs):
        self.collection = collection
        self.is_new = is_new
        if is_new:
            self._old_data = {}
        else:
            self._old_data = self.get_hydrated_data(init_data)
        self.init(self._old_data, **kwargs)
        self._new_data = copy.deepcopy(self._old_data)

    @property
    def data(self):
        return self._new_data

    def get_spec(self):
        raise NotImplementedError

    def init(self, init_data, **kwargs):
        raise NotImplementedError

    def pre_save(self, **kwargs):
        pass

    def post_save(self, **kwargs):
        pass

    def pre_delete(self, **kwargs):
        pass

    def post_delete(self, **kwargs):
        pass

    def get_dehydrated_data(self):
        return self.data

    def get_hydrated_data(self, data):
        return data

    @property
    def diff(self):
        diff = DictDiff(self._old_data, self._new_data)
        return diff

    def save(self, **kw):
        self.pre_save(**kw)
        self.save_base()
        self.post_save(**kw)

    def delete(self, **kw):
        self.pre_delete(**kw)
        self.delete_base()
        self.post_delete(**kw)

    def save_base(self):
        self.collection.save(self)

    def delete_base(self):
        self.collection.delete(self)

    def update(self, data):

        def update_(old, new):
            if isinstance(new, dict):
                new = new.items()
            for num, kv in enumerate(new):
                try:
                    k, v = kv
                except TypeError:
                    msg = ('cannot convert dictionary update'
                           ' sequence element #{num} to a sequence')
                    raise TypeError(msg.format(num=num))

                if isinstance(v, list):
                    old_list = old.setdefault(k, [])

                    for index, new_item in enumerate(v):
                        try:
                            if isinstance(new_item, PRIMITIVE_TYPES):
                                old_list[index] = new_item
                            else:
                                old_item = old_list[index]
                                update_(old_item, new_item)
                        except IndexError:
                            old_list.append(new_item)

                        old[k] = old_list[:len(v)]

                elif isinstance(v, dict):
                    old_dict = old.setdefault(k, {})
                    update_(old_dict, v)

                else:
                    old[k] = v

        update_(self.data, data)


class Collection:

    def get(self, **kw):
        coll = mongo.db.get_collection(
            self.collection_name,
            read_preference=ReadPreference.PRIMARY,
        )
        data = coll.find_one(kw)

        is_new = not bool(data)

        obj = self.model(self, data, is_new=is_new, **kw)
        return obj

    def find(self, q, for_update=False):
        read_preference = ReadPreference.PRIMARY if for_update else ReadPreference.SECONDARY

        coll = mongo.db.get_collection(
            self.collection_name,
            read_preference=read_preference,
        )

        return CollectionObjectGenerator(
            collection=coll,
            wraper=lambda d: self.model(self, d, is_new=False),
            q=q
        )

    def save(self, obj):
        data = obj.get_dehydrated_data()
        spec = obj.get_spec()
        data.update(spec)

        t1 = datetime.now()

        coll = mongo.db.get_collection(
            self.collection_name,
            write_concern=WriteConcern(settings.MONGO_WRITE_CONCERN_W),
        )
        coll.replace_one(spec, data, upsert=True)

        t2 = datetime.now()
        logger.debug('Mongo save operation for %s taken %s', data, t2 - t1)

    def delete(self, obj):
        spec = obj.get_spec()

        t1 = datetime.now()

        coll = mongo.db.get_collection(self.collection_name)

        try:
            coll.delete_one(spec)
        except Exception:
            raise MongoModelException

        t2 = datetime.now()
        logger.debug('Mongo remove operation for %s takes %s', spec, t2 - t1)

    def new(self, *args, **kwargs):
        obj = self.model(self, {}, is_new=True, *args, **kwargs)
        return obj
