# -*- coding: utf-8 -*-

"""
Модуль для работы с базой данных.
"""

from datetime import datetime
from pymongo import MongoClient, ASCENDING, DESCENDING, HASHED, ReadPreference

from stocks3.share.singleton import Singleton
from stocks3.core.sharedregion import SHARED_REGION
from stocks3.share.dateandtime import to_unixtime
from stocks3.core.withdbconfig import WithDBConfiguration


# Точность хранимых в базе значений
DB_PRECISION = 8


class WithDB(WithDBConfiguration):
    def __init__(self):
        WithDBConfiguration.__init__(self, None, None)
        self.make_config()
        self.db = Database(self.get_dbinfo())


class Database(object, metaclass=Singleton):
    """
    Реализует основные запросы к БД. Используется, например, из Saver-a.
    Метод select_previous_value используется из L{UpDownChecker}.
    При чтении из базы данных время, по возможности, забирается из unixtime.
    """

    def __init__(self, connectionstring, not_use_unixtime=False):
        self.client = MongoClient(connectionstring, readPreference='secondary')
        self.sources = self.client.stocksdb.sources

        self.sourcedata = self.client.stocksdb.sourcedata
        self.sourcedata.create_index([('composite_id', HASHED)])
        self.sourcedata.create_index([('source_inner_id', ASCENDING), ('transport_case_id', ASCENDING)])

        self.export_additions = self.client.stocksdb.export_additions
        self.export_additions.create_index([('quote_id', ASCENDING), ('region', ASCENDING)])
        self.export_additions.create_index([('quote_id', ASCENDING)])

        self.stocks = self.client.stocksdb.stocks
        self.stocks.create_index([('id', ASCENDING), ('date', DESCENDING), ('region', ASCENDING)], unique=True)
        self.stocks.create_index([('id', ASCENDING), ('region', ASCENDING)])

        # db.stocks.createIndex({"date" : 1, "id" : 1, "region" : 1, disabled: 1}, {background: true})
        # db.stocks.createIndex({"id" : 1, "region" : 1, disabled: 1}, {background: true})
        # db.stocks.createIndex({"id" : 1, disabled: 1}, {background: true})

        # disabled = выключенная котировка
        # discontinued = закончена поддержка. В экспортах она есть, но новых данных более не будет.

        self.stocks.create_index([('date', DESCENDING)])
        self.stocks.create_index([('date', ASCENDING), ('id', ASCENDING), ('region', ASCENDING)])
        self.not_use_unixtime = not_use_unixtime

    # def source_save(self, source_inner_id, source_id, updated):
    #     return self.sources.update(
    #         {'source_inner_id': source_inner_id},
    #         {
    #             'source_inner_id': source_inner_id,
    #             'source_id': source_id,
    #             'updated': updated,
    #         })

    def place_get(self, composite_id):
        return self.sourcedata.find_one(
            {'composite_id': composite_id},
            projection={'composite_id': 1, 'contenthash': 1, 'last_contenthash': 1}
        )

    def place_save(self, composite_id, content, contenthash):
        # Не исправлять на update_one!
        return self.sourcedata.update(
            {'composite_id': composite_id},
            {'$set': {'contenthash': contenthash, 'content': content}},
            upsert=True
        )

    def place_update_last_contenthash(self, composite_id, last_contenthash):
        # Не исправлять на update_one!
        return self.sourcedata.update(
            {'composite_id': composite_id},
            {'$set': {'last_contenthash': last_contenthash}}
        )

    def generate_price(self, quote, data):
        unixtime = int(data['unixtime'] or 0)
        try:
            lastupdate = data['lastupdate'].timestamp() if 'lastupdate' in data else None
        except TypeError as err:
            lastupdate = 0
        if unixtime == 0 or self.not_use_unixtime:
            date = datetime.strptime(data['date'] + ' ' + data['time'], "%Y-%m-%d %H:%M:%S")
            # Восстанавливаем Unixtime с текущей зоной
            if unixtime == 0:
                unixtime = to_unixtime(date)
        else:
            date = datetime.fromtimestamp(unixtime)
        if 'sell_value' in data.keys():
            price = quote.make_dual_price(date, data['value'], data['sell_value'], lastupdate,
                                          unconfirmed=data.get('unconfirmed', False))
        else:
            price = quote.make_price(date, data['value'], lastupdate, unconfirmed=data.get('unconfirmed', False))
        price.unixtime = unixtime
        return price

    def select(self, source, price, fields=None):
        return self.stocks.find_one({
            'id': price.quote.quote_id,
            'region': price.getRegion(SHARED_REGION),
            'date': str(price.date.date()),
            'disabled': {'$ne': True},
        }, projection=fields)

    def select_last_value(self, source, price):
        """
        Выборка предыдущего значения для price.
        """
        data = self.stocks.find_one({
            'id': price.quote.quote_id,
            'region': price.getRegion(SHARED_REGION),
            'disabled': {'$ne': True},
        },
            sort=[('date', DESCENDING)])
        if data is None:
            return None
        else:
            new_price = self.generate_price(price.quote, data)
            return new_price

    def select_previous_value(self, source, price):
        return self.select_last_value(source, price)

    def select_prices(self, quote, region, limit, desc=True):
        """
        Из базы данных выбирает самые свежие значение в количестве не более
        count штук.
        """
        return self.stocks.find({
            'id': quote.quote_id,
            'region': region,
            'disabled': {'$ne': True},
        }).sort('date', DESCENDING if desc else ASCENDING).limit(limit)

    def insert_or_update(self, source, price):
        # хитрая логика с весами и unconfirmed убрана в saver, потому можно использовать этот метод а не просто update
        stock_value = {
            '$set': {
                'unixtime': price.unixtime,
                'time': str(price.date.time()),
                'value': self._to_db_precision(price.buy.value),
                'unconfirmed': price.unconfirmed,
                'lastupdate': datetime.now(),
                'source': source.weight,
            },
            '$setOnInsert': {
                'id': price.quote.quote_id,
                'region': price.getRegion(SHARED_REGION),
                'date': str(price.date.date()),
            },
        }
        if price.is_dual_price:
            stock_value['$set']['sell_value'] = self._to_db_precision(price.sell.value)
        find_conditions = {
            'id': price.quote.quote_id,
            'region': price.getRegion(SHARED_REGION),
            'date': str(price.date.date()),
        }
        res = self.stocks.update(find_conditions, stock_value, upsert=True)
        return res['n'], res['updatedExisting']

    def update(self, source, price):
        stock_value = {
            '$set': {
                'value': self._to_db_precision(price.buy.value),
                'unixtime': price.unixtime,
                'time': str(price.date.time()),
                'unconfirmed': price.unconfirmed,
                'lastupdate': datetime.now(),
                'source': source.weight,
            }
        }
        if price.is_dual_price:
            stock_value['$set']['sell_value'] = self._to_db_precision(price.sell.value)
        find_conditions = {
            'id': price.quote.quote_id,
            'region': price.getRegion(SHARED_REGION),
            'date': str(price.date.date()),
            'source': {'$lte': source.weight},
            'unconfirmed': {'$gte': price.unconfirmed}
        }
        res = self.stocks.update(find_conditions, stock_value, upsert=False)
        return res['n'], res['updatedExisting']

    def insert(self, source, price):
        stock = {'id': price.quote.quote_id,
                 'source': source.weight,
                 'region': price.getRegion(SHARED_REGION),
                 'date': str(price.date.date()),
                 'time': str(price.date.time()),
                 'unixtime': price.unixtime,
                 'value': self._to_db_precision(price.buy.value),
                 'unconfirmed': price.unconfirmed,
                 'lastupdate': datetime.now(),
                 }
        if price.is_dual_price:
            stock['sell_value'] = self._to_db_precision(price.sell.value)
        rec_id = self.stocks.insert(stock)
        return rec_id

    def read_from_db(self, count, quote, desc):
        """
        Читает по count значений из базы данных для котировки quote для каждого региона.
        Сортирует исходя из флажка desc.
        """
        if count is None:
            count = 0
        regions = self.stocks.find({'id': quote.quote_id, 'disabled': {'$ne': True}}).distinct('region')
        prices = {}
        for region in regions:
            data = self.stocks.find({'id': quote.quote_id, 'region': region, 'disabled': {'$ne': True}},
                                    sort=[('date', DESCENDING if desc else ASCENDING)]).limit(count)
            for record in data:
                if record['region'] not in prices:
                    prices[record['region']] = []
                prices[record['region']].append(self.generate_price(quote, record))
        return quote, prices

    def select_max_unixtime(self, quote, region):
        quote = self.stocks.find_one(
            {
                'id': quote.quote_id,
                'region': region,
                'disabled': {'$ne': True},
            },
            sort=[('date', DESCENDING)])
        return int(quote['unixtime']) if quote else None

    @staticmethod
    def _to_db_precision(v):
        assert v is None or type(v) == float, "_to_db_precision: expected float"
        if v is None:
            return v
        else:
            return round(v, DB_PRECISION)

    def save_export_additions(self, quote_id, region, additions):
        additions['lastupdate'] = datetime.now()
        res = self.export_additions.update(
            {'quote_id': quote_id, 'region': region},
            {'$set': additions},
            upsert=True
        )
        return res

    cache = None

    def get_export_additions(self, quote_id, region):
        return self.export_additions.find_one({'quote_id': quote_id, 'region': region})
        # if self.cache:
        #     for row in self.cache:
        #         if row['quote_id'] == quote_id and row['region'] == region:
        #             return row
        # else:
        #     self.cache = self.export_additions.find({})
        #     return self.get_export_additions(quote_id, region)
