import logging
from .storage import Storage
import boto3
from boto3.dynamodb.conditions import Key
import datetime
from shortuuid import ShortUUID
import copy


class DynamoDBStorage(Storage):
    _logger = logging.getLogger("flask-blogging")

    def __init__(self, table_prefix="", region_name=None,
                 endpoint_url=None):
        self._client = boto3.client('dynamodb',
                                    region_name=region_name,
                                    endpoint_url=endpoint_url)
        self._db = boto3.resource("dynamodb",
                                  region_name=region_name,
                                  endpoint_url=endpoint_url)
        self._table_prefix = table_prefix
        self._create_all_tables()
        self._uuid = ShortUUID()

    def save_post(self, title, text, user_id, tags, draft=False,
                  post_date=None, last_modified_date=None, meta_data=None,
                  post_id=None):
        try:
            current_datetime = datetime.datetime.utcnow()
            post_date = post_date or current_datetime
            post_date = self._to_timestamp(post_date)
            last_modified_date = last_modified_date or current_datetime
            tags = self.normalize_tags(tags)
            draft = 1 if draft else 0
            r = {'title': title,
                 'text': text,
                 'user_id': user_id,
                 'tags': tags,
                 'draft': draft,
                 'post_date': post_date,
                 'last_modified_date': self._to_timestamp(last_modified_date),
                 'meta_data': meta_data
                 }
            if post_id is not None:
                response = self._blog_posts_table.get_item(
                    Key={'post_id': post_id})
                r0 = response.get("Item")
                post_id = r0['post_id'] if r0 else None

            if post_id is None:
                post_id = self._uuid.uuid()
                r['post_id'] = post_id
                self._blog_posts_table.put_item(Item=r)
                self._insert_tags(tags, post_id, post_date, draft)
            else:
                expr = 'SET title = :title, #t = :text, user_id = :user_id, '\
                       'tags = :tags, draft = :draft, '\
                       'post_date = :post_date, '\
                       'last_modified_date = :last_modified_date, '\
                       'meta_data = :meta_data'
                self._blog_posts_table.update_item(
                    Key={'post_id': post_id},
                    UpdateExpression=expr,
                    ExpressionAttributeValues={
                        ':title': r['title'],
                        ':text': r['text'],
                        ':user_id': r['user_id'],
                        ':tags': r['tags'],
                        ':draft': r['draft'],
                        ':post_date': r['post_date'],
                        ':last_modified_date': r["last_modified_date"],
                        ':meta_data': r['meta_data']
                    },
                    ExpressionAttributeNames={'#t': 'text'},
                    ReturnValues="ALL_NEW"
                )

                tag_inserts = set(r['tags']) - set(r0['tags'])
                tag_deletes = set(r0['tags']) - set(r['tags'])
                self._insert_tags(tag_inserts, post_id, post_date, draft)
                self._delete_tags(tag_deletes, post_id)
        except Exception as e:
            self._logger.exception(str(e))
            post_id = None
        return post_id

    def get_posts(self, count=10, offset=0, recent=True, tag=None,
                  user_id=None, include_draft=False):
        try:
            post_ids = self._get_post_ids(count=count, offset=offset,
                                          recent=recent,
                                          tag=tag, user_id=user_id,
                                          include_draft=include_draft)
        except Exception as e:
            self._logger.exception(str(e))
            post_ids = []
        return [self.get_post_by_id(p) for p in post_ids]

    def _get_post_ids(self, count=10, offset=0, recent=True, tag=None,
                      user_id=None, include_draft=False):
        # include_draft is not supported yet
        kwargs = dict(ProjectionExpression='post_id',
                      ScanIndexForward=not recent)
        if count:
            kwargs['Limit'] = count
        table = self._blog_posts_table
        if user_id:
            kwargs.update(
                dict(IndexName='user_id_index',
                     KeyConditionExpression=Key('user_id').eq(user_id))
            )

        elif tag:
            table = self._tag_posts_table
            norm_tag = self.normalize_tag(tag)
            kwargs.update(
                dict(IndexName='tag_index',
                     KeyConditionExpression=Key('tag').eq(norm_tag))
            )
        else:
            kwargs.update(
                dict(IndexName='post_index',
                     KeyConditionExpression=Key('draft').eq(0))
            )
        if offset and offset > 0:
            kwargs2 = copy.deepcopy(kwargs)
            kwargs2['Limit'] = offset
            response = getattr(table, "query")(**kwargs2)
            last_key = response.get('LastEvaluatedKey')
        else:
            last_key = None

        if last_key:
            kwargs["ExclusiveStartKey"] = last_key
        response = getattr(table, "query")(**kwargs)
        return [p['post_id'] for p in response['Items']]

    def count_posts(self, tag=None, user_id=None, include_draft=False):
        try:
            post_ids = self._get_post_ids(count=None, offset=0, tag=tag,
                                          user_id=user_id,
                                          include_draft=include_draft)
            result = len(post_ids)
        except Exception as e:
            self._logger.exception(str(e))
            result = 0
        return result

    def get_post_by_id(self, post_id):
        try:
            response = self._blog_posts_table.get_item(
                Key={'post_id': post_id}
            )
            item = response.get('Item')
            if item:
                r = item
                r['post_date'] = self._from_timestamp(r['post_date'])
                r['last_modified_date'] = \
                    self._from_timestamp(r['last_modified_date'])
                r["draft"] = bool(r["draft"])
            else:
                r = None
        except Exception as e:
            self._logger.exception(str(e))
            r = None
        return r

    def delete_post(self, post_id):
        try:
            r = self.get_post_by_id(post_id)
            if r:
                response = self._blog_posts_table.delete_item(
                    Key={'post_id': post_id})
                self._delete_tags(r["tags"], post_id)
                return True
            else:
                return False
        except Exception as e:
            self._logger.exception(str(e))
            return False

    @staticmethod
    def _to_timestamp(date_time):
        return date_time.isoformat()

    @staticmethod
    def _from_timestamp(timestamp):
        return datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")

    def _table_name(self, table_name):
        return self._table_prefix + table_name

    def _create_all_tables(self):
        response = self._client.list_tables()
        table_names = response["TableNames"]
        self._create_blog_posts_table(table_names)
        self._create_tag_posts_table(table_names)

    def _create_blog_posts_table(self, table_names):
        bp_table_name = self._table_name("blog_posts")
        if bp_table_name not in table_names:
            self._client.create_table(
                TableName=bp_table_name,
                KeySchema=[{
                    'AttributeName': 'post_id',
                    'KeyType': 'HASH'
                }
                ],
                GlobalSecondaryIndexes=[
                    {
                        'IndexName': "user_id_index",
                        'KeySchema': [
                            {
                                'AttributeName': 'user_id',
                                'KeyType': 'HASH',
                            },
                            {
                                'AttributeName': 'post_date',
                                'KeyType': 'RANGE',
                            }
                        ],
                        'Projection': {
                            'ProjectionType': 'ALL'
                        },
                        'ProvisionedThroughput': {
                            'ReadCapacityUnits': 2,
                            'WriteCapacityUnits': 2
                        }
                    },
                    {
                        'IndexName': "post_index",
                        'KeySchema': [
                            {
                                'AttributeName': 'draft',
                                'KeyType': 'HASH',
                            },
                            {
                                'AttributeName': 'post_date',
                                'KeyType': 'RANGE',
                            }
                        ],
                        'Projection': {
                            'ProjectionType': 'ALL'
                        },
                        'ProvisionedThroughput': {
                            'ReadCapacityUnits': 2,
                            'WriteCapacityUnits': 2
                        }
                    }
                ],
                AttributeDefinitions=[
                    {
                        'AttributeName': 'post_id',
                        'AttributeType': 'S'
                    },
                    {
                        'AttributeName': 'user_id',
                        'AttributeType': 'S'
                    },
                    {
                        'AttributeName': 'post_date',
                        'AttributeType': 'S'
                    },
                    {
                        'AttributeName': 'draft',
                        'AttributeType': 'N'
                    },
                ],
                ProvisionedThroughput={
                    'ReadCapacityUnits': 10,
                    'WriteCapacityUnits': 10
                }
            )
        self._blog_posts_table = self._db.Table(bp_table_name)

    def _create_tag_posts_table(self, table_names):
        tp_table_name = self._table_name("tag_posts")
        if tp_table_name not in table_names:
            self._client.create_table(
                TableName=tp_table_name,
                KeySchema=[{
                    'AttributeName': 'tag_id',
                    'KeyType': 'HASH'
                }
                ],
                GlobalSecondaryIndexes=[
                    {
                        'IndexName': "tag_index",
                        'KeySchema': [
                            {
                                'AttributeName': 'tag',
                                'KeyType': 'HASH',
                            },
                            {
                                'AttributeName': 'post_date',
                                'KeyType': 'RANGE',
                            }
                        ],
                        'Projection': {
                            'ProjectionType': 'ALL'
                        },
                        'ProvisionedThroughput': {
                            'ReadCapacityUnits': 2,
                            'WriteCapacityUnits': 2
                        }
                    },
                ],
                AttributeDefinitions=[
                    {
                        'AttributeName': 'tag_id',
                        'AttributeType': 'S'
                    },
                    {
                        'AttributeName': 'tag',
                        'AttributeType': 'S'
                    },
                    {
                        'AttributeName': 'post_date',
                        'AttributeType': 'S'
                    }
                ],
                ProvisionedThroughput={
                    'ReadCapacityUnits': 10,
                    'WriteCapacityUnits': 10
                }
            )
        self._tag_posts_table = self._db.Table(tp_table_name)

    def _insert_tags(self, tags, post_id, post_date, draft):
        for t in tags:
            tag_id = "%s_%s" % (t, post_id)
            _ = self._tag_posts_table.put_item(
                Item={'tag_id': tag_id, 'tag': t, 'post_date': post_date,
                      'post_id': post_id, 'draft': draft}
            )

    def _delete_tags(self, tags, post_id):
        for t in tags:
            tag_id = "%s_%s" % (t, post_id)
            _ = self._tag_posts_table.delete_item(
                Key={'tag_id': tag_id}
            )
