"""CLI command for "sync" command."""

import logging
import os
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple

import click

from samcli.cli.cli_config_file import ConfigProvider, configuration_option, save_params_option
from samcli.cli.context import Context
from samcli.cli.main import aws_creds_options, pass_context, print_cmdline_args
from samcli.cli.main import common_options as cli_framework_options
from samcli.commands._utils.cdk_support_decorators import unsupported_command_cdk
from samcli.commands._utils.click_mutex import ClickMutex
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands._utils.constants import (
    DEFAULT_BUILD_DIR,
    DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER,
    DEFAULT_CACHE_DIR,
)
from samcli.commands._utils.custom_options.replace_help_option import ReplaceHelpSummaryOption
from samcli.commands._utils.option_value_processor import process_image_options
from samcli.commands._utils.options import (
    base_dir_option,
    build_image_option,
    build_in_source_option,
    capabilities_option,
    container_env_var_file_option,
    image_repositories_option,
    image_repository_option,
    kms_key_id_option,
    metadata_option,
    notification_arns_option,
    parameter_override_option,
    role_arn_option,
    s3_bucket_option,
    s3_prefix_option,
    stack_name_option,
    tags_option,
    template_option_without_build,
    use_container_build_option,
    watch_exclude_option,
)
from samcli.commands.build.click_container import ContainerOptions
from samcli.commands.build.command import _get_mode_value_from_envvar
from samcli.commands.sync.core.command import SyncCommand
from samcli.commands.sync.sync_context import SyncContext
from samcli.lib.bootstrap.bootstrap import manage_stack
from samcli.lib.build.bundler import EsbuildBundlerManager
from samcli.lib.cli_validation.image_repository_validation import image_repository_validation
from samcli.lib.providers.provider import (
    ResourceIdentifier,
    get_all_resource_ids,
    get_unique_resource_ids,
)
from samcli.lib.providers.sam_stack_provider import SamLocalStackProvider
from samcli.lib.sync.infra_sync_executor import InfraSyncExecutor, InfraSyncResult
from samcli.lib.sync.sync_flow_executor import SyncFlowExecutor
from samcli.lib.sync.sync_flow_factory import SyncCodeResources, SyncFlowFactory
from samcli.lib.sync.watch_manager import WatchManager
from samcli.lib.telemetry.event import EventTracker, track_long_event
from samcli.lib.telemetry.metric import track_command, track_template_warnings
from samcli.lib.utils.colors import Colored
from samcli.lib.utils.version_checker import check_newer_version
from samcli.lib.warnings.sam_cli_warning import CodeDeployConditionWarning, CodeDeployWarning

if TYPE_CHECKING:  # pragma: no cover
    from samcli.commands.build.build_context import BuildContext
    from samcli.commands.deploy.deploy_context import DeployContext
    from samcli.commands.package.package_context import PackageContext

LOG = logging.getLogger(__name__)

HELP_TEXT = """
  NEW! Sync an AWS SAM Project to AWS.

"""

DESCRIPTION = """
  By default, `$sam sync` runs a full AWS Cloudformation stack update.

  Running `sam sync --watch` with `--code` will provide a way to run just code
  synchronization, speeding up start time skipping template changes.

  Remember to update the deployed stack by running
  without --code for infrastructure changes.

  `$sam sync` also supports nested stacks and nested stack resources.
"""


SYNC_INFO_TEXT = """
The SAM CLI will use the AWS Lambda, Amazon API Gateway, and AWS StepFunctions APIs to upload your code without 
performing a CloudFormation deployment. This will cause drift in your CloudFormation stack. 
**The sync command should only be used against a development stack**.
"""

SYNC_CONFIRMATION_TEXT = """
Confirm that you are synchronizing a development stack.

Enter Y to proceed with the command, or enter N to cancel:
"""


SHORT_HELP = "Sync an AWS SAM project to AWS."

DEFAULT_TEMPLATE_NAME = "template.yaml"
DEFAULT_CAPABILITIES = ("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND")


# TODO(sriram-mv): Move context settings to be global such as width.
@click.command(
    "sync",
    cls=SyncCommand,
    help=HELP_TEXT,
    short_help=SHORT_HELP,
    description=DESCRIPTION,
    requires_credentials=True,
    context_settings={"max_content_width": 120},
)
@configuration_option(provider=ConfigProvider(section="parameters"))
@template_option_without_build
@click.option(
    "--code",
    is_flag=True,
    help="Sync ONLY code resources. This includes Lambda Functions, API Gateway, and Step Functions.",
    cls=ClickMutex,
)
@click.option(
    "--watch/--no-watch",
    is_flag=True,
    help="Watch local files and automatically sync with cloud.",
    cls=ClickMutex,
)
@click.option(
    "--resource-id",
    multiple=True,
    help="Sync code for all the resources with the ID. To sync a resource within a nested stack, "
    "use the following pattern {ChildStack}/{logicalId}.",
)
@click.option(
    "--resource",
    multiple=True,
    cls=ReplaceHelpSummaryOption,
    type=click.Choice(SyncCodeResources.values(), case_sensitive=True),
    replace_help_option="--resource RESOURCE",
    help=f"Sync code for all resources of the given resource type. Accepted values are {SyncCodeResources.values()}",
)
@click.option(
    "--dependency-layer/--no-dependency-layer",
    default=True,
    is_flag=True,
    help="Separate dependencies of individual function into a Lambda layer for improved performance.",
)
@click.option(
    "--skip-deploy-sync/--no-skip-deploy-sync",
    default=True,
    is_flag=True,
    help="This option will skip the initial infrastructure deployment if it is not required"
    " by comparing the local template with the template deployed in cloud.",
)
@container_env_var_file_option(cls=ContainerOptions)
@watch_exclude_option
@stack_name_option(required=True)  # pylint: disable=E1120
@base_dir_option
@use_container_build_option
@build_in_source_option
@build_image_option(cls=ContainerOptions)
@image_repository_option
@image_repositories_option
@s3_bucket_option(disable_callback=True)  # pylint: disable=E1120
@s3_prefix_option
@kms_key_id_option
@role_arn_option
@parameter_override_option
@cli_framework_options
@aws_creds_options
@metadata_option
@notification_arns_option
@tags_option
@capabilities_option(default=DEFAULT_CAPABILITIES)  # pylint: disable=E1120
@save_params_option
@pass_context
@track_command
@track_long_event("SyncUsed", "Start", "SyncUsed", "End")
@image_repository_validation(support_resolve_image_repos=False)
@track_template_warnings([CodeDeployWarning.__name__, CodeDeployConditionWarning.__name__])
@check_newer_version
@print_cmdline_args
@unsupported_command_cdk()
@command_exception_handler
def cli(
    ctx: Context,
    template_file: str,
    code: bool,
    watch: bool,
    resource_id: Optional[List[str]],
    resource: Optional[List[str]],
    dependency_layer: bool,
    skip_deploy_sync: bool,
    stack_name: str,
    base_dir: Optional[str],
    parameter_overrides: dict,
    image_repository: str,
    image_repositories: Optional[List[str]],
    s3_bucket: str,
    s3_prefix: str,
    kms_key_id: str,
    capabilities: Optional[List[str]],
    role_arn: Optional[str],
    notification_arns: Optional[List[str]],
    tags: dict,
    metadata: dict,
    use_container: bool,
    container_env_var_file: Optional[str],
    save_params: bool,
    config_file: str,
    config_env: str,
    build_image: Optional[Tuple[str]],
    build_in_source: Optional[bool],
    watch_exclude: Optional[Dict[str, List[str]]],
) -> None:
    """
    `sam sync` command entry point
    """
    mode = _get_mode_value_from_envvar("SAM_BUILD_MODE", choices=["debug"])
    # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

    do_cli(
        template_file,
        code,
        watch,
        resource_id,
        resource,
        dependency_layer,
        skip_deploy_sync,
        stack_name,
        ctx.region,
        ctx.profile,
        base_dir,
        parameter_overrides,
        mode,
        image_repository,
        image_repositories,
        s3_bucket,
        s3_prefix,
        kms_key_id,
        capabilities,
        role_arn,
        notification_arns,
        tags,
        metadata,
        use_container,
        container_env_var_file,
        build_image,
        config_file,
        config_env,
        build_in_source,
        watch_exclude,
    )  # pragma: no cover


def do_cli(
    template_file: str,
    code: bool,
    watch: bool,
    resource_id: Optional[List[str]],
    resource: Optional[List[str]],
    dependency_layer: bool,
    skip_deploy_sync: bool,
    stack_name: str,
    region: str,
    profile: str,
    base_dir: Optional[str],
    parameter_overrides: dict,
    mode: Optional[str],
    image_repository: str,
    image_repositories: Optional[List[str]],
    s3_bucket: str,
    s3_prefix: str,
    kms_key_id: str,
    capabilities: Optional[List[str]],
    role_arn: Optional[str],
    notification_arns: Optional[List[str]],
    tags: dict,
    metadata: dict,
    use_container: bool,
    container_env_var_file: Optional[str],
    build_image: Optional[Tuple[str]],
    config_file: str,
    config_env: str,
    build_in_source: Optional[bool],
    watch_exclude: Optional[Dict[str, List[str]]],
) -> None:
    """
    Implementation of the ``cli`` method
    """
    from samcli.cli.global_config import GlobalConfig
    from samcli.commands.build.build_context import BuildContext
    from samcli.commands.deploy.deploy_context import DeployContext
    from samcli.commands.package.package_context import PackageContext
    from samcli.lib.utils import osutils

    global_config = GlobalConfig()
    if not global_config.is_accelerate_opt_in_stack(template_file, stack_name):
        if not click.confirm(Colored().yellow(SYNC_INFO_TEXT + SYNC_CONFIRMATION_TEXT), default=True):
            return
        global_config.set_accelerate_opt_in_stack(template_file, stack_name)
    else:
        LOG.info(Colored().color_log(msg=SYNC_INFO_TEXT, color="yellow"), extra=dict(markup=True))

    s3_bucket_name = s3_bucket or manage_stack(profile=profile, region=region)

    if dependency_layer is True:
        dependency_layer = check_enable_dependency_layer(template_file)

    # Note: ADL with use-container is not supported yet. Remove this logic once its supported.
    if use_container and dependency_layer:
        LOG.info(
            "Note: Automatic Dependency Layer is not yet supported with use-container. \
            sam sync will be run without Automatic Dependency Layer."
        )
        dependency_layer = False

    build_dir = DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER if dependency_layer else DEFAULT_BUILD_DIR
    LOG.debug("Using build directory as %s", build_dir)
    EventTracker.track_event("UsedFeature", "Accelerate")

    processed_build_images = process_image_options(build_image)

    with BuildContext(
        resource_identifier=None,
        template_file=template_file,
        base_dir=base_dir,
        build_dir=build_dir,
        cache_dir=DEFAULT_CACHE_DIR,
        clean=True,
        use_container=use_container,
        container_env_var_file=container_env_var_file,
        cached=True,
        parallel=True,
        parameter_overrides=parameter_overrides,
        mode=mode,
        create_auto_dependency_layer=dependency_layer,
        stack_name=stack_name,
        print_success_message=False,
        locate_layer_nested=True,
        build_in_source=build_in_source,
        build_images=processed_build_images,
    ) as build_context:
        built_template = os.path.join(build_dir, DEFAULT_TEMPLATE_NAME)

        with osutils.tempfile_platform_independent() as output_template_file:
            with PackageContext(
                template_file=built_template,
                s3_bucket=s3_bucket_name,
                image_repository=image_repository,
                image_repositories=image_repositories,
                s3_prefix=s3_prefix,
                kms_key_id=kms_key_id,
                output_template_file=output_template_file.name,
                no_progressbar=True,
                metadata=metadata,
                region=region,
                profile=profile,
                use_json=False,
                force_upload=True,
            ) as package_context:
                # 500ms of sleep time between stack checks and describe stack events.
                DEFAULT_POLL_DELAY = 0.5
                try:
                    poll_delay = float(os.getenv("SAM_CLI_POLL_DELAY", str(DEFAULT_POLL_DELAY)))
                except ValueError:
                    poll_delay = DEFAULT_POLL_DELAY
                if poll_delay <= 0:
                    poll_delay = DEFAULT_POLL_DELAY

                with DeployContext(
                    template_file=output_template_file.name,
                    stack_name=stack_name,
                    s3_bucket=s3_bucket_name,
                    image_repository=image_repository,
                    image_repositories=image_repositories,
                    no_progressbar=True,
                    s3_prefix=s3_prefix,
                    kms_key_id=kms_key_id,
                    parameter_overrides=parameter_overrides,
                    capabilities=capabilities,
                    role_arn=role_arn,
                    notification_arns=notification_arns,
                    tags=tags,
                    region=region,
                    profile=profile,
                    no_execute_changeset=True,
                    fail_on_empty_changeset=True,
                    confirm_changeset=False,
                    use_changeset=False,
                    force_upload=True,
                    signing_profiles=None,
                    disable_rollback=False,
                    poll_delay=poll_delay,
                    on_failure=None,
                    max_wait_duration=60,
                ) as deploy_context:
                    with SyncContext(
                        dependency_layer, build_context.build_dir, build_context.cache_dir, skip_deploy_sync
                    ) as sync_context:
                        if watch:
                            watch_excludes_filter = watch_exclude or {}

                            execute_watch(
                                template=template_file,
                                build_context=build_context,
                                package_context=package_context,
                                deploy_context=deploy_context,
                                sync_context=sync_context,
                                auto_dependency_layer=dependency_layer,
                                disable_infra_syncs=code,
                                watch_exclude=watch_excludes_filter,
                            )
                        elif code:
                            execute_code_sync(
                                template=template_file,
                                build_context=build_context,
                                deploy_context=deploy_context,
                                sync_context=sync_context,
                                resource_ids=resource_id,
                                resource_types=resource,
                                auto_dependency_layer=dependency_layer,
                            )
                        else:
                            infra_sync_result = execute_infra_contexts(
                                build_context, package_context, deploy_context, sync_context
                            )
                            code_sync_resources = infra_sync_result.code_sync_resources

                            if not infra_sync_result.infra_sync_executed and code_sync_resources:
                                resource_ids = [str(resource) for resource in code_sync_resources]

                                LOG.info("Queuing up code sync for the resources that require an update")
                                LOG.debug("The following resources will be code synced for an update: %s", resource_ids)
                                execute_code_sync(
                                    template=template_file,
                                    build_context=build_context,
                                    deploy_context=deploy_context,
                                    sync_context=sync_context,
                                    resource_ids=resource_ids,
                                    resource_types=None,
                                    auto_dependency_layer=dependency_layer,
                                    use_built_resources=True,
                                )


def execute_infra_contexts(
    build_context: "BuildContext",
    package_context: "PackageContext",
    deploy_context: "DeployContext",
    sync_context: "SyncContext",
) -> InfraSyncResult:
    """Executes the sync for infra.

    Parameters
    ----------
    build_context : BuildContext
    package_context : PackageContext
    deploy_context : DeployContext
    sync_context : SyncContext

    Returns
    -------
    InfraSyncResult
        Data class that contains infra sync execution result
    """
    infra_sync_executor = InfraSyncExecutor(build_context, package_context, deploy_context, sync_context)
    return infra_sync_executor.execute_infra_sync(first_sync=True)


def execute_code_sync(
    template: str,
    build_context: "BuildContext",
    deploy_context: "DeployContext",
    sync_context: "SyncContext",
    resource_ids: Optional[List[str]],
    resource_types: Optional[List[str]],
    auto_dependency_layer: bool,
    use_built_resources: bool = False,
) -> None:
    """Executes the sync flow for code.

    Parameters
    ----------
    template : str
        Template file name
    build_context : BuildContext
        BuildContext
    deploy_context : DeployContext
        DeployContext
    sync_context: SyncContext
        SyncContext object that obtains sync information.
    resource_ids : List[str]
        List of resource IDs to be synced.
    resource_types : List[str]
        List of resource types to be synced.
    auto_dependency_layer: bool
        Boolean flag to whether enable certain sync flows for auto dependency layer feature
    use_built_resources: bool
        Boolean flag to whether to use pre-build resources from BuildContext or build resources from scratch
    """
    stacks = SamLocalStackProvider.get_stacks(template)[0]
    factory = SyncFlowFactory(build_context, deploy_context, sync_context, stacks, auto_dependency_layer)
    factory.load_physical_id_mapping()
    executor = SyncFlowExecutor()

    sync_flow_resource_ids: Set[ResourceIdentifier] = (
        get_unique_resource_ids(stacks, resource_ids, resource_types)
        if resource_ids or resource_types
        else set(get_all_resource_ids(stacks))
    )

    for resource_id in sync_flow_resource_ids:
        built_result = build_context.build_result if use_built_resources else None
        sync_flow = factory.create_sync_flow(resource_id, built_result)
        if sync_flow:
            executor.add_sync_flow(sync_flow)
        else:
            LOG.warning("Cannot create SyncFlow for %s. Skipping.", resource_id)
    executor.execute()


def execute_watch(
    template: str,
    build_context: "BuildContext",
    package_context: "PackageContext",
    deploy_context: "DeployContext",
    sync_context: "SyncContext",
    auto_dependency_layer: bool,
    disable_infra_syncs: bool,
    watch_exclude: Dict[str, List[str]],
):
    """Start sync watch execution

    Parameters
    ----------
    template : str
        Template file path
    build_context : BuildContext
        BuildContext
    package_context : PackageContext
        PackageContext
    deploy_context : DeployContext
        DeployContext
    sync_context: SyncContext
        SyncContext object that obtains sync information.
    auto_dependency_layer: bool
        Boolean flag to whether enable certain sync flows for auto dependency layer feature.
    disable_infra_syncs: bool
        Boolean flag to determine if sam sync only executes code syncs.
    """
    # Note: disable_infra_syncs  is different from skip_deploy_sync,
    # disable_infra_syncs completely disables infra syncs and
    # skip_deploy_sync skips the initial infra sync if it's not required.
    watch_manager = WatchManager(
        template,
        build_context,
        package_context,
        deploy_context,
        sync_context,
        auto_dependency_layer,
        disable_infra_syncs,
        watch_exclude,
    )
    watch_manager.start()


def check_enable_dependency_layer(template_file: str):
    """
    Check if auto dependency layer should be enabled
    :param template_file: template file string
    :return: True if ADL should be enabled, False otherwise
    """
    stacks, _ = SamLocalStackProvider.get_stacks(template_file)
    for stack in stacks:
        esbuild = EsbuildBundlerManager(stack)
        if esbuild.esbuild_configured():
            # Disable ADL if esbuild is configured. esbuild already makes the package size
            # small enough to ensure that ADL isn't needed to improve performance
            click.secho("esbuild is configured, disabling auto dependency layer.", fg="yellow")
            return False
    return True
