"""gludb.backends.dynamodb - backend dynamodb database module
"""

import os

import boto.exception
import boto.dynamodb2  # NOQA

from boto.dynamodb2.layer1 import DynamoDBConnection
from boto.dynamodb2.table import Table
from boto.dynamodb2.fields import HashKey, GlobalIncludeIndex
from boto.dynamodb2.items import Item
from boto.dynamodb2.exceptions import ResourceNotFoundException, ItemNotFound
from boto.exception import JSONResponseError

from ..utils import uuid


def get_conn():
    """Return a connection to DynamoDB (and handle local/debug
    possibilities)"""
    if os.environ.get('DEBUG', False) or os.environ.get('travis', False):
        # In DEBUG mode - use the local DynamoDB
        # This also works for travis since we'll be running dynalite
        conn = DynamoDBConnection(
            host='localhost',
            port=8000,
            aws_access_key_id='TEST',
            aws_secret_access_key='TEST',
            is_secure=False
        )
    else:
        # Regular old production
        conn = DynamoDBConnection()

    return conn


def gsi_name(index_name):
    """Standardize how we create a GSI name for DynamoDB from the given index
    name from the class"""
    return index_name + '_gsiindex'


class DynamoMappings(object):
    """DynamoDB has some opinions about what you can store or query in an
    attribute. We're going to use mappings to fix that.
    """

    # Yes, the bracket-ish map vals are illegal in just about any language that
    # uses bracket-type syntax. That's on purpose.
    NONE_VAL = "<{[None>}]"
    EMPTY_STR_VAL = "<{[''>}]"

    @staticmethod
    def map_index_val(index_val):
        """Xform index_val so that it can be stored/queried"""
        if index_val is None:
            return DynamoMappings.NONE_VAL

        index_val = str(index_val)
        if not index_val:
            return DynamoMappings.EMPTY_STR_VAL

        return index_val

    @staticmethod
    def unmap_stored_val(stored_val):
        """Inverse of index_val_mapping. Note that we currently don't use it
        because we don't actually read back index values (since they are
        generated by Python functions)"""
        if stored_val == DynamoMappings.NONE_VAL:
            return None
        elif stored_val == DynamoMappings.EMPTY_STR_VAL:
            return ''
        else:
            return stored_val


def delete_table(table_name):
    """Mainly for testing"""
    Table(table_name, connection=get_conn(), schema=[HashKey('id')]).delete()


class Backend(object):
    def __init__(self, **kwrds):
        pass  # No current keywords needed/used

    def table_schema_call(self, target, cls):
        """Call the callable target with the args and keywords needed for the
        table defined by cls. This is how we centralize the Table.create and
        Table ctor calls
        """
        index_defs = []
        for name in cls.index_names() or []:
            index_defs.append(GlobalIncludeIndex(
                gsi_name(name),
                parts=[HashKey(name)],
                includes=['value']
            ))

        return target(
            cls.get_table_name(),
            connection=get_conn(),
            schema=[HashKey('id')],
            global_indexes=index_defs or None
        )

    def ensure_table(self, cls):
        exists = True
        conn = get_conn()

        try:
            descrip = conn.describe_table(cls.get_table_name())
            assert descrip is not None
        except ResourceNotFoundException:
            # Expected - this is what we get if there is no table
            exists = False
        except JSONResponseError:
            # Also assuming no table
            exists = False

        if not exists:
            table = self.table_schema_call(Table.create, cls)
            assert table is not None

    def get_class_table(self, cls):
        """Return a DynamoDB table object for the given class
        """
        return self.table_schema_call(Table, cls)

    def find_one(self, cls, id):
        try:
            db_result = self.get_class_table(cls).lookup(id)
        except ItemNotFound:
            # according to docs, this shouldn't be required, but it IS
            db_result = None

        if not db_result:
            return None

        obj = cls.from_data(db_result['value'])
        return obj

    def find_all(self, cls):
        final_results = []
        table = self.get_class_table(cls)
        for db_result in table.scan():
            obj = cls.from_data(db_result['value'])
            final_results.append(obj)

        return final_results

    def find_by_index(self, cls, index_name, value):
        query_args = {
            index_name + '__eq': DynamoMappings.map_index_val(value),
            'index': gsi_name(index_name)
        }

        final_results = []
        for db_result in self.get_class_table(cls).query_2(**query_args):
            obj = cls.from_data(db_result['value'])
            final_results.append(obj)

        return final_results

    def save(self, obj):
        if not obj.id:
            obj.id = uuid()

        stored_data = {
            'id': obj.id,
            'value': obj.to_data()
        }

        index_vals = obj.indexes() or {}
        for key in obj.__class__.index_names() or []:
            val = index_vals.get(key, '')
            stored_data[key] = DynamoMappings.map_index_val(val)

        table = self.get_class_table(obj.__class__)
        item = Item(table, data=stored_data)

        item.save(overwrite=True)
