###################################################################################################################### 
#  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.                                           # 
#                                                                                                                    # 
#  Licensed under the Apache License Version 2.0 (the "License"). You may not use this file except in compliance     # 
#  with the License. A copy of the License is located at                                                             # 
#                                                                                                                    # 
#      http://www.apache.org/licenses/                                                                               # 
#                                                                                                                    # 
#  or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES # 
#  OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions    # 
#  and limitations under the License.                                                                                # 
######################################################################################################################
from boto3.dynamodb.conditions import Attr

from actions import *
from actions.action_base import ActionBase
from boto_retry import add_retry_methods_to_resource, get_client_with_retries
from outputs import raise_exception

PARAM_DESC_RETAIN_FAILED_TASKS = "Set to Yes to keep entries for failed tasks"
PARAM_DESC_TASK_RETENTION_HOURS = "Number of hours to keep completed entries before they are deleted from tracking table"

PARAM_LABEL_RETAIN_FAILED_TASKS = "Keep failed tasks"
PARAM_LABEL_TASK_RETENTION_HOURS = "Hours to keep entries"

PARAM_RETAIN_FAILED_TASKS = "RetainFailedTasks"
PARAM_TASK_RETENTION_HOURS = "TaskRetentionHours"
PARAM_TASK_TABLE = "TaskTable"

WARNING_DELETE_CAPACITY = "There are unprocessed items when cleaning up task items, consider raining capacity of table {}"
ERR_MISSING_ENVIRONMENT_VARIABLE_ = "Task tracking table not defined in environment variable {}"
INF_DELETE = "Deleting tasks older than {}"


class SchedulerTaskCleanupAction(ActionBase):
    properties = {

        ACTION_TITLE: "Scheduler Task Cleanup",
        ACTION_VERSION: "1.0",
        ACTION_DESCRIPTION: "Deletes old entries from task tracking table",
        ACTION_AUTHOR: "AWS",
        ACTION_ID: "6f0ac9ab-b0ea-4922-b674-253499dee6a2",

        ACTION_SERVICE: "time",
        ACTION_RESOURCES: "",
        ACTION_AGGREGATION: ACTION_AGGREGATION_RESOURCE,
        ACTION_CROSS_ACCOUNT: False,
        ACTION_INTERNAL: True,
        ACTION_MULTI_REGION: False,

        ACTION_EXECUTE_SIZE: ACTION_SIZE_ALL_WITH_ECS,

        ACTION_PARAMETERS: {

            PARAM_TASK_RETENTION_HOURS: {
                PARAM_LABEL: PARAM_LABEL_TASK_RETENTION_HOURS,
                PARAM_DESCRIPTION: PARAM_DESC_TASK_RETENTION_HOURS,
                PARAM_TYPE: type(int()),
                PARAM_MIN_VALUE: 1,
                PARAM_REQUIRED: True
            },
            PARAM_RETAIN_FAILED_TASKS: {
                PARAM_LABEL: PARAM_LABEL_RETAIN_FAILED_TASKS,
                PARAM_DESCRIPTION: PARAM_DESC_RETAIN_FAILED_TASKS,
                PARAM_TYPE: type(True),
                PARAM_REQUIRED: True
            }
        },
        ACTION_PERMISSIONS: [
            "dynamodb:Scan",
            "dynamodb:BatchWriteItem"
        ]
    }

    def __init__(self, action_arguments, action_parameters):

        ActionBase.__init__(self, action_arguments, action_parameters)

        self.task_table = os.getenv(handlers.ENV_ACTION_TRACKING_TABLE, None)
        if self.task_table is None:
            raise_exception(ERR_MISSING_ENVIRONMENT_VARIABLE_, handlers.ENV_ACTION_TRACKING_TABLE)

        # adding 48 hours as TTL is used in V2 as primary mechanism to delete items
        self.task_retention_seconds = (int(self.get(PARAM_TASK_RETENTION_HOURS)) + 48) * 3600
        self.retain_failed_tasks = self.get(PARAM_RETAIN_FAILED_TASKS, True)
        self.dryrun = self.get(ACTION_PARAM_DRYRUN, False)
        self.debug = self.get(ACTION_PARAM_DEBUG, False)

        self._client = None

    @property
    def client(self):
        if self._client is None:
            self._client = get_client_with_retries("dynamodb",
                                                   methods=[
                                                       "batch_write_item"
                                                   ],
                                                   context=self._context_,
                                                   session=self._session_,
                                                   logger=self._logger_)
        return self._client

    def execute(self):

        self._logger_.info("{}, version {}", str(self.__class__).split(".")[-2], self.properties[ACTION_VERSION])
        self._logger_.debug("Implementation {}", __name__)

        self._logger_.info("Cleanup table {}", self.task_table)

        scanned_count = 0
        deleted_count = 0

        # calculate moment from when entries can be deleted
        dt = (self._datetime_.utcnow() - datetime(1970, 1, 1)).total_seconds()
        delete_before = int(dt - self.task_retention_seconds)
        self._logger_.info(INF_DELETE, datetime.fromtimestamp(delete_before).isoformat())

        #  status of deleted items for scan expression
        delete_status = [handlers.STATUS_COMPLETED]

        if not self.retain_failed_tasks:
            delete_status.append(handlers.STATUS_FAILED)
            delete_status.append(handlers.STATUS_TIMED_OUT)

        table = self._session_.resource("dynamodb").Table(self.task_table)
        add_retry_methods_to_resource(table, ["scan"], context=self._context_)

        args = {
            "Select": "ALL_ATTRIBUTES",
            "FilterExpression": (Attr(handlers.TASK_TR_STATUS).is_in(delete_status))
        }

        self._logger_.debug("table.scan arguments {}", args)

        # scan for items to delete
        while True:

            if self.time_out():
                break

            resp = table.scan_with_retries(**args)

            self._logger_.debug("table.scan result {}", resp)

            scanned_count += resp["ScannedCount"]

            to_delete = [i[handlers.TASK_TR_ID] for i in resp.get("Items", []) if
                         int(i[handlers.TASK_TR_CREATED_TS]) < delete_before]

            deleted_count += self.delete_tasks(to_delete)

            if "LastEvaluatedKey" in resp:
                args["ExclusiveStartKey"] = resp["LastEvaluatedKey"]
            else:
                break

        return {
            "items-scanned": scanned_count,
            "items-deleted": deleted_count
        }

    def delete_tasks(self, items_to_delete):

        deleted = 0

        if not self.dryrun and len(items_to_delete) > 0:

            delete_requests = []

            # delete items in batches of max 25 items
            while len(items_to_delete) > 0:

                if self.time_out():
                    break

                delete_requests.append({
                    'DeleteRequest': {
                        'Key': {
                            handlers.TASK_TR_ID: {
                                "S": items_to_delete.pop(0)
                            }
                        }
                    }
                })

                if len(items_to_delete) == 0 or len(delete_requests) == 25:
                    self._logger_.debug("batch_write request items {}", delete_requests)
                    resp = self.client.batch_write_item_with_retries(RequestItems={self.task_table: delete_requests})

                    self._logger_.debug("batch_write response {}", resp)
                    deleted += len(delete_requests)

        return deleted