'''
TODO: actually implement the 'transaction' function
'''


import threading

from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from uuid import uuid4

from ..abstract_base_db import AbstractBaseDB
from ..condition import Condition
from ..exceptions import *
from ..index import Index
from ..util.heartbeat import interval
from ..util.fn_serialize import build_fn_key


_INDEX_IN_USE_HEARTBEAT_INTERVAL_S = 3600

_INDEX_STALE_TIMEDELTA = timedelta(hours=2)

_store_data = {}
_schema = {}


class DB(AbstractBaseDB):


  def __init__(self):
    self._schema = {}


  def __del__(self):
    self._heartbeat.cancel()


  def describe(self, data_type=None):
    return { i: [ x['name'] for x in j if not x.get('discovered') ] for i, j in self._schema.items() }


  @interval(_INDEX_IN_USE_HEARTBEAT_INTERVAL_S)
  def _heartbeat(self):
    for data_type in self._schema:
      # Update the 'last_used' property for all the indexes we explicitly initialized
      for index in self._schema[data_type]:
        if index['discovered']:
          key = (index['name'], index['key'] and build_fn_key(index['key']))
          _schema[data_type][key]['last_used'] = datetime.now(timezone.utc)


  @contextmanager
  def transaction(self):
    # TODO: this
    yield


  def init(self, data_type, indexes=[]):
    if data_type in self._schema:
      raise DuplicateDataTypeException("Data type '%s' has already been initialized" % data_type)

    if data_type not in _store_data:
      _store_data[data_type] = {}

    index_definitions = indexes
    indexes = []
    for index_definition in index_definitions:
      if type(index_definition) == str:
        index_definition = Index(index_definition)
      indexes.append({ 'name': index_definition.name, 'key': index_definition.key, 'discovered': False })
    self._schema[data_type] = indexes

    index_names = set()

    if data_type not in _schema:
      _schema[data_type] = {}
    for index in self._schema[data_type]:
      if index['name'] in index_names:
        raise DuplicateIndexException("Index '%s' was listed twice")
      index_names.add(index['name'])
      key = (index['name'], index['key'] and build_fn_key(index['key']))
      if key not in _schema[data_type]:
        _schema[data_type][key] = {
          'name': index['name'],
          'key': index['key'],
          'last_used': datetime.now(timezone.utc)
        }

    # Clean up any stale indexes
    stale_datetime = datetime.now(timezone.utc) - _INDEX_STALE_TIMEDELTA
    keys_to_delete = []
    for key, index in _schema[data_type].items():
      if index['last_used'] < stale_datetime:
        keys_to_delete.append(key)
    for key in keys_to_delete:
      del _schema[data_type][key]


  def _discover_new_indexes(self, data_type):
    known_index_keys = set()
    for index in self._schema[data_type]:
      key = (index['name'], index['key'] and build_fn_key(index['key']))
      known_index_keys.add(key)

    for key, index in _schema[data_type].items():
      if key not in known_index_keys:
        self._schema[data_type].append({ 'name': index['name'], 'key': index['key'], 'discovered': True })


  def create(self, data_type, data):
    self._discover_new_indexes(data_type)
    new_id = uuid4()
    _store_data[data_type][new_id] = data
    return new_id


  def update(self, data_type, data_id, data):
    self._discover_new_indexes(data_type)
    _store_data[data_type][data_id] = data


  def get(self, data_type, data_id):
    if data_type not in self._schema:
      raise MissingDataTypeException()
    if data_id not in _store_data[data_type]:
      raise NotFoundException()
    return _store_data[data_type][data_id]


  def delete(self, data_type, data_id):
    del _store_data[data_type][data_id]


  def _get_index(self, data_type, index_name):
    for index in self._schema[data_type]:
      if index['name'] == index_name:
        return index
    raise MissingIndexException("Index '%s' has not been initialized for data type '%s'" % (index_name, data_type))


  def _matches_condition(self, data_type, data, condition):
    if not isinstance(condition, Condition):
      raise MalformedConditionException('Expression is not a condition: %r' % condition)
    index = self._get_index(data_type, condition.index_name)
    value = index['key'](data) if index['key'] else data[index['name']]
    if value is None:
      return False
    for operator, operand in condition.filter.items():
      if (operator == '<') and (not value < operand):
        return False
      if (operator == '<=') and (not value <= operand):
        return False
      if (operator == '>') and (not value > operand):
        return False
      if (operator == '>=') and (not value >= operand):
        return False
      if (operator == '==') and (not value == operand):
        return False
      if (operator == '!=') and (not value != operand):
        return False
    return True


  def iter(self, data_type, condition=None, descending=False, limit=None):
    items = []
    for data_id, data in _store_data[data_type].items():
      if (not condition) or self._matches_condition(data_type, data, condition):
        items.append((data_id, data))
    if condition:
      index = self._get_index(data_type, condition.index_name)
      key = lambda item: (index['key'](item[1]) if index['key'] else item[1][index['name']], item[0])
    else:
      key = lambda item: item[0]
    items.sort(key=key, reverse=descending)
    yield from items[:limit]


  def _remove_indexes(self):
    global _schema
    _schema = {}

  def _remove_data_type(self, data_type):
    global _store_data, _schema
    _schema.pop(data_type, None)
    _store_data.pop(data_type, None)
