import boto3
import botocore
import logging
import pickle
import re
import string
import time
import threading
import warnings

from contextlib import contextmanager, suppress
from datetime import date, datetime, timedelta, timezone
from functools import wraps
from uuid import UUID, uuid4

from .abstract_base_store import AbstractBaseStore, Condition, Index
from .exceptions import *
from .heartbeat import interval
from .fn_serialize import build_fn_key, dumps_fn, loads_fn


logger = logging.getLogger(__name__)


_DYNAMODB_ALLOWED_CHARS = string.ascii_lowercase + string.ascii_uppercase + '_'
def _encode_dynamodb(name):
  '''
  A one-way encoding to translate a 'name' in to a dynamo-friendly string. Note that we preserve some characters to
  add a modicum of legibility to our table names - this is largely for simplifying development purposes.
  '''
  output = 'op_'
  for char in name:
    if char in _DYNAMODB_ALLOWED_CHARS:
      output += char
    else:
      output += str(ord(char))
  return output


class DynamoDBStore(AbstractBaseStore):


  def _wait_table_active(self, table_name):
    # TODO: add a timeout in here
    while True:
      time.sleep(5)
      response = self._dynamodb.describe_table(Table=table_name)
      if (response['Table']['TableStatus'] == 'ACTIVE'):
        return


  def __init__(self, aws_config):
    self._dynamodb = boto3.client('dynamodb', **aws_config)

    # The '_schema' stores all the data types and indexes that we know about. It is a dictionary structured as follows:
    # {
    #   # I tend to refer to this object as a 'data type schema' - see the '_get_data_type_schema' function
    #   <data_type>: {
    #     'table': str, # The name of the corresponding table in the DB - see '_encode_pg'
    #     'update_indexes_lock': lock, # A lock object that ensures thread safety when updating indexes
    #     'indexes': [
    #       # I tend to refer to this object as an 'index schema'
    #       {
    #         'name: str, # The name of the index
    #         'id': int, # The ID of this index data in the 'op_store.op_store_metadata' table
    #         'key': callable, # The single-argument index function - given the object data, it returns the index value
    #         ['discovered']: bool # If present and true, indicates that we did not explicitly initialize this index
    #       },
    #       ...
    #     }
    #   },
    #   ...
    # }
    self._schema = {}

    self._dynamodb.create_table(
      TableName='_op_storage_metadata',
      AttributeDefinitions=[
        { 'AttributeName': 'data_type', 'AttributeType': 'S' },
        { 'AttributeName': 'id', 'AttributeType': 'S' },
        { 'AttributeName': 'index_fn', 'AttributeType': 'B' },
        { 'AttributeName': 'index_fn_key', 'AttributeType': 'B' },
        { 'AttributeName': 'last_used', 'AttributeType': 'S' }
      ],
      KeySchema=[
        { 'AttributeName': 'data_type', 'KeyType': 'HASH' },
        { 'AttributeName': 'id', 'KeyType': 'RANGE' }
      ]
    )
    self._wait_table_active('_op_storage_metadata')


  def __del__(self):
    pass


  def describe(self):
    output = {}
    for data_type, data_type_schema in self._schema.items():
      indexes = []
      for index_schema in data_type_schema['indexes']:
        index = {
          'name': index_schema['name'],
          'key': index_schema['key'],
          'discovered': index_schema.get('discovered', False)
        }
        indexes.append(index)
      output[data_type] = indexes
    return output


  def _get_data_type_schema(self, data_type):
    try:
      return self._schema[data_type]
    except KeyError as e:
      raise MissingDataTypeException("Data type '%s' has not been initialized" % data_type) from e


  def _get_initialized_index_schema(self, data_type, index_name):
    data_type_schema = self._get_data_type_schema(data_type)
    for index_schema in data_type_schema['indexes']:
      if index_schema['name'] == index_name and not index_schema.get('discovered', False):
        return index_schema
    raise MissingIndexException("Index '%s' has not been initialized for data type '%s'" % (index_name, data_type))


  @contextmanager
  def transaction(self):
    raise NotImplemented()


  def _create_table(self, table_name):
    response = self._dynamodb.create_table(
      TableName=table_name,
      AttributeDefinitions=[
        { 'AttributeName': 'id', 'AttributeType': 'B' }
      ],
      KeySchema=[
        { 'AttributeName': 'id', 'KeyType': 'HASH' }
      ]
    )

    table_status = response['TableDescription']['TableStatus']
    while (table_status != 'ACTIVE'):
      time.sleep(5)
      response = self._dynamodb.describe_table(Table=table_name)
      table_status = response['Table']['TableStatus']


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

    # # From the caller perspective, the 'indexes' name and abstraction make sense, but we rename it here to match our
    # # perspective
    # index_definitions = indexes

    # table_name = _encode_dynamodb(data_type)
    # self._create_table(table_name)

    # self._schema[data_type] = { 'table': table_name, 'indexes': [] }

    # indexes = self._get_indexes(data_type)
    # stale_index_names = []
    # for index in indexes:
    #   if _is_stale(index):
    #     stale_index_names.append(index['index_name'])

    # self._drop_stale_indexes(data_type)


    # indexes = self.metadata.get_current_indexes()

    # {
    #   'index_name': '0',
    #   'index_fn': ...,
    #   'index_fn_key': ...,
    #   'last_used': ...
    # }







  def create(self, data_type, data):
    pass


  def update(self, data_type, data_id, data):
    # Should fail if the thing does not already exist
    pass


  def get(self, data_type, data_id):
    pass


  def get_batch(self, data_type, data_ids):
    pass


  def delete(self, data_type, data_id):
    pass


  def iter(self, data_type, *conditions, order_by=None, descending=False, limit=None):
    pass


  def list(self, data_type, *conditions, order_by=None, descending=False, limit=None):
    pass



# These are the credentials for op-dev
aws_config = {
  "aws_access_key_id": "AKIAW5I5ZPVYBGGRM6OZ",
  "aws_secret_access_key": "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",
  "region_name": "us-west-2"
}
db = DynamoDBStore(aws_config)
db.init('user', indexes=[ 'age' ])
