"""
Delete Cloudformation stacks and s3 files
"""

import logging
from typing import Dict, List, Optional

from botocore.exceptions import BotoCoreError, ClientError, WaiterError

from samcli.commands.delete.exceptions import CfDeleteFailedStatusError, DeleteFailedError, FetchTemplateFailedError

LOG = logging.getLogger(__name__)


class CfnUtils:
    def __init__(self, cloudformation_client):
        self._client = cloudformation_client

    def has_stack(self, stack_name: str) -> bool:
        """
        Checks if a CloudFormation stack with given name exists

        :param stack_name: Name or ID of the stack
        :return: True if stack exists. False otherwise
        """
        try:
            resp = self._client.describe_stacks(StackName=stack_name)
            if not resp["Stacks"]:
                return False

            stack = resp["Stacks"][0]
            if stack["EnableTerminationProtection"]:
                message = "Stack cannot be deleted while TerminationProtection is enabled."
                raise DeleteFailedError(stack_name=stack_name, msg=message)

            # Note: Stacks with REVIEW_IN_PROGRESS can be deleted
            # using delete_stack but get_template does not return
            # the template_str for this stack restricting deletion of
            # artifacts.
            return bool(stack["StackStatus"] != "REVIEW_IN_PROGRESS")

        except ClientError as e:
            # If a stack does not exist, describe_stacks will throw an
            # exception. Unfortunately we don't have a better way than parsing
            # the exception msg to understand the nature of this exception.

            if "Stack with id {0} does not exist".format(stack_name) in str(e):
                LOG.debug("Stack with id %s does not exist", stack_name)
                return False
            LOG.error("ClientError Exception : %s", str(e))
            raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e
        except BotoCoreError as e:
            # If there are credentials, environment errors,
            # catch that and throw a delete failed error.

            LOG.error("Botocore Exception : %s", str(e))
            raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e

    def get_stack_template(self, stack_name: str, stage: str) -> Dict:
        """
        Return the Cloudformation template of the given stack_name

        :param stack_name: Name or ID of the stack
        :param stage: The Stage of the template Original or Processed
        :return: Template body of the stack
        """
        try:
            resp = self._client.get_template(StackName=stack_name, TemplateStage=stage)
            if not resp["TemplateBody"]:
                return {}
            return dict(resp)

        except (ClientError, BotoCoreError) as e:
            # If there are credentials, environment errors,
            # catch that and throw a delete failed error.

            LOG.error("Failed to fetch template for the stack : %s", str(e))
            raise FetchTemplateFailedError(stack_name=stack_name, msg=str(e)) from e

        except Exception as e:
            # We don't know anything about this exception. Don't handle
            LOG.error("Unable to get stack details.", exc_info=e)
            raise e

    def delete_stack(self, stack_name: str, retain_resources: Optional[List] = None):
        """
        Delete the Cloudformation stack with the given stack_name

        :param stack_name: Name or ID of the stack
        :param retain_resources: List of repositories to retain if the stack has DELETE_FAILED status.
        """
        if not retain_resources:
            retain_resources = []
        try:
            self._client.delete_stack(StackName=stack_name, RetainResources=retain_resources)

        except (ClientError, BotoCoreError) as e:
            # If there are credentials, environment errors,
            # catch that and throw a delete failed error.

            LOG.error("Failed to delete stack : %s", str(e))
            raise DeleteFailedError(stack_name=stack_name, msg=str(e)) from e

        except Exception as e:
            # We don't know anything about this exception. Don't handle
            LOG.error("Failed to delete stack. ", exc_info=e)
            raise e

    def wait_for_delete(self, stack_name):
        """
        Waits until the delete stack completes

        :param stack_name:   Stack name
        """

        # Wait for Delete to Finish
        waiter = self._client.get_waiter("stack_delete_complete")
        # Remove `MaxAttempts` from waiter_config.
        # Regression: https://github.com/aws/aws-sam-cli/issues/4361
        waiter_config = {"Delay": 30}
        try:
            waiter.wait(StackName=stack_name, WaiterConfig=waiter_config)
        except WaiterError as ex:
            stack_status = ex.last_response.get("Stacks", [{}])[0].get("StackStatusReason", "")

            if "DELETE_FAILED" in str(ex):
                raise CfDeleteFailedStatusError(
                    stack_name=stack_name, stack_status=stack_status, msg="ex: {0}".format(ex)
                ) from ex

            raise DeleteFailedError(stack_name=stack_name, stack_status=stack_status, msg="ex: {0}".format(ex)) from ex
