import fnmatch
import json
import logging
import os
import sys

import boto3
from dynamodb_json import json_util as dynamodb_json


class Setup:
    def __init__(self, logging):
        # parameters
        self.logging = logging

        self._client_cloudformation = None
        self._client_dynamodb = None
        self._client_sts = None

    @property
    def client_sts(self):
        if not self._client_sts:
            self._client_sts = boto3.client("sts")
        return self._client_sts

    @property
    def region(self):
        if self.client_sts.meta.region_name != "aws-global":
            return self.client_sts.meta.region_name
        else:
            return "us-east-1"

    @property
    def client_cloudformation(self):
        if not self._client_cloudformation:
            self._client_cloudformation = boto3.client("cloudformation", self.region)
        return self._client_cloudformation

    @property
    def client_dynamodb(self):
        if not self._client_dynamodb:
            self._client_dynamodb = boto3.client("dynamodb", self.region)
        return self._client_dynamodb

    def create_stacks(self, stack_sub_dir, settings):
        """Parse a directory and and deploy all the AWS Config Rules it contains
        
        Arguments:
            stack_sub_dir {string} -- Sub-directory that houses AWS Config Rules
            settings {dictionary} -- Dictionary of settings
        """
        existing_stacks = self.get_current_stacks()
        path = f"auto_remediate_setup/data/{stack_sub_dir}"

        for file in os.listdir(path):
            if fnmatch.fnmatch(file, "*.json"):
                stack_name = file.replace(".json", "")
                template_body = None

                with open(os.path.join(path, file)) as stack:
                    template_body = str(stack.read())

                if stack_name not in existing_stacks:
                    if (
                        settings.get("rules", {})
                        .get(stack_name, {})
                        .get("deploy", True)
                    ):
                        try:
                            self.client_cloudformation.create_stack(
                                StackName=stack_name,
                                TemplateBody=template_body,
                                OnFailure="DELETE",
                                EnableTerminationProtection=True,
                            )

                            self.logging.info(
                                f"Creating AWS Config Rule '{stack_name}'."
                            )
                        except:
                            self.logging.error(
                                f"Could not create AWS Config Rule '{stack_name}'."
                            )
                            self.logging.error(sys.exc_info()[1])
                            continue
                    else:
                        self.logging.info(
                            f"AWS Config Rule '{stack_name}' deployement was skipped due to user preferences."
                        )
                else:
                    if (
                        not settings.get("rules", {})
                        .get(stack_name, {})
                        .get("deploy", True)
                    ):
                        self.client_cloudformation.update_termination_protection(
                            EnableTerminationProtection=False, StackName=stack_name
                        )
                        self.client_cloudformation.delete_stack(StackName=stack_name)
                        self.logging.info(
                            f"AWS Config Rule '{stack_name}' was deleted."
                        )
                    else:
                        self.logging.debug(
                            f"AWS Config Rule '{stack_name}' already exists."
                        )

    def get_current_stacks(self):
        """Retrieve a list of all CloudFormation Stacks currently deployed your AWS accont and region
        
        Returns:
            list -- List of currently deployed AWS Config Rules
        """
        try:
            resources = self.client_cloudformation.list_stacks().get("StackSummaries")
        except:
            self.logging.error(sys.exc_info()[1])
            return None

        existing_stacks = []
        for resource in resources:
            if resource.get("StackStatus") not in ("DELETE_COMPLETE"):
                existing_stacks.append(resource.get("StackName"))

        return existing_stacks

    def get_settings(self):
        """Return the DynamoDB aws-auto-remediate-settings table in a Python dict format
        
        Returns:
            dict -- aws-auto-remediate-settings table
        """
        settings = {}
        try:
            for record in self.client_dynamodb.scan(
                TableName=os.environ["SETTINGSTABLE"]
            )["Items"]:
                record_json = dynamodb_json.loads(record, True)

                if "key" in record_json and "value" in record_json:
                    settings[record_json.get("key")] = record_json.get("value")
        except:
            self.logging.error(
                f"Could not read DynamoDB table '{os.environ['SETTINGSTABLE']}'."
            )
            self.logging.error(sys.exc_info()[1])

        return settings

    def setup_dynamodb(self):
        """Inserts all the default settings into a DynamoDB table.
        """
        try:
            settings_data = open(
                "auto_remediate_setup/data/auto-remediate-settings.json"
            )
            settings_json = json.loads(settings_data.read())

            update_settings = False

            # get current settings version
            current_version = self.client_dynamodb.get_item(
                TableName=os.environ["SETTINGSTABLE"],
                Key={"key": {"S": "version"}},
                ConsistentRead=True,
            )

            # get new settings version
            new_version = float(settings_json[0].get("value", {}).get("N", 0.0))

            # check if settings exist and if they're older than current settings
            if "Item" in current_version:
                current_version = float(
                    current_version.get("Item").get("value").get("N")
                )
                if current_version < new_version:
                    update_settings = True
                    self.logging.info(
                        f"Existing settings with version {str(current_version)} are being updated to version "
                        f"{str(new_version)} in DynamoDB Table '{os.environ['SETTINGSTABLE']}'."
                    )
                else:
                    self.logging.debug(
                        f"Existing settings are at the lastest version {str(current_version)} in DynamoDB Table "
                        f"'{os.environ['SETTINGSTABLE']}'."
                    )
            else:
                update_settings = True
                self.logging.info(
                    f"Settings are being inserted into DynamoDB Table "
                    f"'{os.environ['SETTINGSTABLE']}' for the first time."
                )

            if update_settings:
                for setting in settings_json:
                    try:
                        self.client_dynamodb.put_item(
                            TableName=os.environ["SETTINGSTABLE"], Item=setting
                        )
                    except:
                        self.logging.error(sys.exc_info()[1])
                        continue

            settings_data.close()
        except:
            self.logging.error(sys.exc_info()[1])


def lambda_handler(event, context):
    loggger = logging.getLogger()

    if loggger.handlers:
        for handler in loggger.handlers:
            loggger.removeHandler(handler)

    # change logging levels for boto and others
    logging.getLogger("boto3").setLevel(logging.ERROR)
    logging.getLogger("botocore").setLevel(logging.ERROR)
    logging.getLogger("urllib3").setLevel(logging.ERROR)

    # set logging format
    logging.basicConfig(
        format="[%(levelname)s] %(message)s (%(filename)s, %(funcName)s(), line %(lineno)d)",
        level=os.environ.get("LOGLEVEL", "WARNING").upper(),
    )

    # instantiate class
    setup = Setup(logging)

    # run functions
    setup.setup_dynamodb()

    settings = setup.get_settings()

    setup.create_stacks("config_rules", settings)
    setup.create_stacks("custom_rules", settings)