"""
Deploy a SAM stack
"""

# Copyright 2015 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://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import logging
import os
from typing import Dict, List, Optional

import boto3
import click

from samcli.commands.deploy import exceptions as deploy_exceptions
from samcli.commands.deploy.auth_utils import auth_per_resource
from samcli.commands.deploy.utils import (
    hide_noecho_parameter_overrides,
    print_deploy_args,
    sanitize_parameter_overrides,
)
from samcli.lib.deploy.deployer import Deployer
from samcli.lib.deploy.utils import FailureMode
from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable
from samcli.lib.package.s3_uploader import S3Uploader
from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent
from samcli.yamlhelper import yaml_parse

LOG = logging.getLogger(__name__)


class DeployContext:
    MSG_SHOWCASE_CHANGESET = "\nChangeset created successfully. {changeset_id}\n"

    MSG_EXECUTE_SUCCESS = "\nSuccessfully created/updated stack - {stack_name} in {region}\n"

    MSG_CONFIRM_CHANGESET = "Deploy this changeset?"
    MSG_CONFIRM_CHANGESET_HEADER = "\nPreviewing CloudFormation changeset before deployment"

    def __init__(
        self,
        template_file,
        stack_name,
        s3_bucket,
        image_repository,
        image_repositories,
        force_upload,
        no_progressbar,
        s3_prefix,
        kms_key_id,
        parameter_overrides,
        capabilities,
        no_execute_changeset,
        role_arn,
        notification_arns,
        fail_on_empty_changeset,
        tags,
        region,
        profile,
        confirm_changeset,
        signing_profiles,
        use_changeset,
        disable_rollback,
        poll_delay,
        on_failure,
    ):
        self.template_file = template_file
        self.stack_name = stack_name
        self.s3_bucket = s3_bucket
        self.image_repository = image_repository
        self.image_repositories = image_repositories
        self.force_upload = force_upload
        self.no_progressbar = no_progressbar
        self.s3_prefix = s3_prefix
        self.kms_key_id = kms_key_id
        self.parameter_overrides = parameter_overrides
        # Override certain CloudFormation pseudo-parameters based on values provided by customer
        self.global_parameter_overrides: Optional[Dict] = None
        if region:
            self.global_parameter_overrides = {IntrinsicsSymbolTable.AWS_REGION: region}
        self.capabilities = capabilities
        self.no_execute_changeset = no_execute_changeset
        self.role_arn = role_arn
        self.notification_arns = notification_arns
        self.fail_on_empty_changeset = fail_on_empty_changeset
        self.tags = tags
        self.region = region
        self.profile = profile
        self.s3_uploader = None
        self.deployer = None
        self.confirm_changeset = confirm_changeset
        self.signing_profiles = signing_profiles
        self.use_changeset = use_changeset
        self.disable_rollback = disable_rollback
        self.poll_delay = poll_delay
        self.on_failure = FailureMode(on_failure) if on_failure else FailureMode.ROLLBACK
        self._max_template_size = 51200

    def __enter__(self):
        return self

    def __exit__(self, *args):
        pass

    def run(self):
        """
        Execute deployment based on the argument provided by customers and samconfig.toml.
        """

        # Parse parameters
        with open(self.template_file, "r") as handle:
            template_str = handle.read()

        template_dict = yaml_parse(template_str)

        if not isinstance(template_dict, dict):
            raise deploy_exceptions.DeployFailedError(
                stack_name=self.stack_name, msg="{} not in required format".format(self.template_file)
            )

        parameters = self.merge_parameters(template_dict, self.parameter_overrides)

        template_size = os.path.getsize(self.template_file)
        if template_size > self._max_template_size and not self.s3_bucket:
            raise deploy_exceptions.DeployBucketRequiredError()
        boto_config = get_boto_config_with_user_agent()
        cloudformation_client = boto3.client(
            "cloudformation", region_name=self.region if self.region else None, config=boto_config
        )

        s3_client = None
        if self.s3_bucket:
            s3_client = boto3.client("s3", region_name=self.region if self.region else None, config=boto_config)

            self.s3_uploader = S3Uploader(
                s3_client, self.s3_bucket, self.s3_prefix, self.kms_key_id, self.force_upload, self.no_progressbar
            )

        self.deployer = Deployer(cloudformation_client, client_sleep=self.poll_delay)

        region = s3_client._client_config.region_name if s3_client else self.region  # pylint: disable=W0212
        display_parameter_overrides = hide_noecho_parameter_overrides(template_dict, self.parameter_overrides)
        print_deploy_args(
            self.stack_name,
            self.s3_bucket,
            self.image_repositories if isinstance(self.image_repositories, dict) else self.image_repository,
            region,
            self.capabilities,
            display_parameter_overrides,
            self.confirm_changeset,
            self.signing_profiles,
            self.use_changeset,
            self.disable_rollback,
        )
        return self.deploy(
            self.stack_name,
            template_str,
            parameters,
            self.capabilities,
            self.no_execute_changeset,
            self.role_arn,
            self.notification_arns,
            self.s3_uploader,
            [{"Key": key, "Value": value} for key, value in self.tags.items()] if self.tags else [],
            region,
            self.fail_on_empty_changeset,
            self.confirm_changeset,
            self.use_changeset,
            self.disable_rollback,
        )

    def deploy(
        self,
        stack_name: str,
        template_str: str,
        parameters: List[dict],
        capabilities: List[str],
        no_execute_changeset: bool,
        role_arn: str,
        notification_arns: List[str],
        s3_uploader: S3Uploader,
        tags: List[str],
        region: str,
        fail_on_empty_changeset: bool = True,
        confirm_changeset: bool = False,
        use_changeset: bool = True,
        disable_rollback: bool = False,
    ):
        """
        Deploy the stack to cloudformation.
        - if changeset needs confirmation, it will prompt for customers to confirm.
        - if no_execute_changeset is True, the changeset won't be executed.

        Parameters
        ----------
        stack_name : str
            name of the stack
        template_str : str
            the string content of the template
        parameters : List[Dict]
            List of parameters
        capabilities : List[str]
            List of capabilities
        no_execute_changeset : bool
            A bool indicating whether to execute changeset
        role_arn : str
            the Arn of the role to create changeset
        notification_arns : List[str]
            Arns for sending notifications
        s3_uploader : S3Uploader
            S3Uploader object to upload files to S3 buckets
        tags : List[str]
            List of tags passed to CloudFormation
        region : str
            AWS region to deploy the stack to
        fail_on_empty_changeset : bool
            Should fail when changeset is empty
        confirm_changeset : bool
            Should wait for customer's confirm before executing the changeset
        use_changeset : bool
            Involve creation of changesets, false when using sam sync
        disable_rollback : bool
            Preserves the state of previously provisioned resources when an operation fails
        """
        stacks, _ = SamLocalStackProvider.get_stacks(
            self.template_file,
            parameter_overrides=sanitize_parameter_overrides(self.parameter_overrides),
            global_parameter_overrides=self.global_parameter_overrides,
        )
        auth_required_per_resource = auth_per_resource(stacks)

        for resource, authorization_required in auth_required_per_resource:
            if not authorization_required:
                click.secho(f"{resource} has no authentication.", fg="yellow")

        if use_changeset:
            try:
                result, changeset_type = self.deployer.create_and_wait_for_changeset(
                    stack_name=stack_name,
                    cfn_template=template_str,
                    parameter_values=parameters,
                    capabilities=capabilities,
                    role_arn=role_arn,
                    notification_arns=notification_arns,
                    s3_uploader=s3_uploader,
                    tags=tags,
                )
                click.echo(self.MSG_SHOWCASE_CHANGESET.format(changeset_id=result["Id"]))

                if no_execute_changeset:
                    return

                if confirm_changeset:
                    click.secho(self.MSG_CONFIRM_CHANGESET_HEADER, fg="yellow")
                    click.secho("=" * len(self.MSG_CONFIRM_CHANGESET_HEADER), fg="yellow")
                    if not click.confirm(f"{self.MSG_CONFIRM_CHANGESET}", default=False):
                        return

                self.deployer.execute_changeset(result["Id"], stack_name, disable_rollback)
                self.deployer.wait_for_execute(stack_name, changeset_type, disable_rollback, self.on_failure)
                click.echo(self.MSG_EXECUTE_SUCCESS.format(stack_name=stack_name, region=region))

            except deploy_exceptions.ChangeEmptyError as ex:
                if fail_on_empty_changeset:
                    raise
                click.echo(str(ex))
            except deploy_exceptions.DeployFailedError:
                # Failed to deploy, check for DELETE action otherwise skip
                if self.on_failure != FailureMode.DELETE:
                    raise

                self.deployer.rollback_delete_stack(stack_name)

        else:
            try:
                result = self.deployer.sync(
                    stack_name=stack_name,
                    cfn_template=template_str,
                    parameter_values=parameters,
                    capabilities=capabilities,
                    role_arn=role_arn,
                    notification_arns=notification_arns,
                    s3_uploader=s3_uploader,
                    tags=tags,
                    on_failure=self.on_failure,
                )
                LOG.debug(result)

            except deploy_exceptions.DeployFailedError as ex:
                LOG.error(str(ex))
                raise

    @staticmethod
    def merge_parameters(template_dict: Dict, parameter_overrides: Dict) -> List[Dict]:
        """
        CloudFormation CreateChangeset requires a value for every parameter
        from the template, either specifying a new value or use previous value.
        For convenience, this method will accept new parameter values and
        generates a dict of all parameters in a format that ChangeSet API
        will accept

        :param template_dict:
        :param parameter_overrides:
        :return:
        """
        parameter_values: List[Dict] = []

        if not isinstance(template_dict.get("Parameters", None), dict):
            return parameter_values

        for key, _ in template_dict["Parameters"].items():
            obj = {"ParameterKey": key}

            if key in parameter_overrides:
                obj["ParameterValue"] = parameter_overrides[key]
            else:
                obj["UsePreviousValue"] = True

            parameter_values.append(obj)

        return parameter_values