#  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#  SPDX-License-Identifier: Apache-2.0
from typing import Any, Dict, List, Union

import aws_cdk as cdk
import constructs
from aws_cdk import CfnJson, Fn, RemovalPolicy, SecretValue, Tags
from aws_cdk import aws_iam as iam
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_s3 as s3
from aws_cdk import aws_secretsmanager as secretsmanager
from aws_cdk import custom_resources as cr
from res.constants import (  # type: ignore
    ENVIRONMENT_NAME_KEY,
    ENVIRONMENT_NAME_TAG_KEY,
    MODULE_ID_DIRECTORY_SERVICE,
    MODULE_NAME_DIRECTORY_SERVICE,
    RES_TAG_BACKUP_PLAN,
    RES_TAG_ENVIRONMENT_NAME,
    RES_TAG_MODULE_ID,
    RES_TAG_MODULE_NAME,
)
from res.resources import (  # type: ignore
    cluster_settings,
    email_templates,
    modules,
    permission_profiles,
    software_stacks,
)

from idea.batteries_included.parameters.parameters import BIParameters
from idea.infrastructure.install import utils
from idea.infrastructure.install.constants import RES_COMMON_LAMBDA_RUNTIME
from idea.infrastructure.install.constructs.base import ResBaseConstruct
from idea.infrastructure.install.ddb_tables.base import RESDDBTableBase
from idea.infrastructure.install.ddb_tables.list import ddb_tables_list
from idea.infrastructure.install.handlers import installer_handlers
from idea.infrastructure.install.parameters.common import CommonKey
from idea.infrastructure.install.parameters.customdomain import CustomDomainKey
from idea.infrastructure.install.parameters.directoryservice import DirectoryServiceKey
from idea.infrastructure.install.parameters.internet_proxy import InternetProxyKey
from idea.infrastructure.install.parameters.parameters import RESParameters
from idea.infrastructure.install.parameters.shared_storage import SharedStorageKey
from idea.infrastructure.install.utils import InfraUtils


class ResBaseStack(ResBaseConstruct):
    def __init__(
        self,
        scope: constructs.Construct,
        shared_library_lambda_layer: lambda_.LayerVersion,
        params_transformer: cdk.CustomResource,
        parameters: Union[RESParameters, BIParameters] = RESParameters(),
    ):
        self.parameters = parameters
        self.params_transformer = params_transformer
        self.cluster_name = parameters.get_str(CommonKey.CLUSTER_NAME)
        self.shared_library_lambda_layer = shared_library_lambda_layer
        self.shared_library_arn = shared_library_lambda_layer.layer_version_arn
        super().__init__(
            self.cluster_name,
            cdk.Aws.REGION,
            "res-base",
            scope,
            self.parameters,
        )

        baseStack = self.nested_stack = cdk.NestedStack(
            scope,
            "res-base",
            description="Nested RES Base Stack",
        )

        for table in ddb_tables_list:
            RESDDBTableBase(
                self.nested_stack,
                table.id,
                self.cluster_name,
                table,
                shared_library_lambda_layer=(
                    self.shared_library_lambda_layer
                    if table.id == cluster_settings.CLUSTER_SETTINGS_TABLE_NAME
                    else None
                ),
                stream_event_handler_role=(
                    self.get_cluster_settings_table_event_handler_role()
                    if table.id == cluster_settings.CLUSTER_SETTINGS_TABLE_NAME
                    else None
                ),
            )

        dcvBrokerTableDeltionPolicy = iam.PolicyDocument(
            statements=[
                iam.PolicyStatement(
                    actions=["dynamodb:DeleteTable"],
                    resources=[
                        f"arn:{cdk.Aws.PARTITION}:dynamodb:{cdk.Aws.REGION}:{cdk.Aws.ACCOUNT_ID}:table/{self.cluster_name}.vdc.dcv-broker*"
                    ],
                ),
                iam.PolicyStatement(
                    actions=["dynamodb:ListTables"],
                    resources=[
                        f"arn:{cdk.Aws.PARTITION}:dynamodb:{cdk.Aws.REGION}:{cdk.Aws.ACCOUNT_ID}:table/*"
                    ],
                ),
            ]
        )
        dcvBrokerTableDeltionRole = iam.Role(
            self,
            "DcvBrokerTableDeltionRole",
            assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
            managed_policies=[
                iam.ManagedPolicy.from_aws_managed_policy_name(
                    "service-role/AWSLambdaBasicExecutionRole"
                )
            ],
            inline_policies={"DDBPolicy": dcvBrokerTableDeltionPolicy},
        )

        dcvBrokerTableDeletionLambda = lambda_.Function(
            baseStack,
            "dcvBrokerTableDeletionLambda",
            runtime=RES_COMMON_LAMBDA_RUNTIME,
            description="Lambda to handle deletion of the NICE DCV Broker tables",
            role=dcvBrokerTableDeltionRole,
            **utils.InfraUtils.get_handler_and_code_for_function(
                installer_handlers.delete_dcv_broker_tables
            ),
        )
        provider = cr.Provider(
            self,
            "dcvBrokerTableDeletionProvider",
            on_event_handler=dcvBrokerTableDeletionLambda,
        )
        cdk.CustomResource(
            self,
            "dcvBrokerTableDeletionCustomResource",
            service_token=provider.service_token,
            properties={"environment_name": self.cluster_name},
        )

        self.parameters.root_user_dn_secret_arn = self.get_directory_service_secret_arn(
            DirectoryServiceKey.ROOT_USER_DN
        )
        self.populator_custom_resource = self.populate_default_values()
        self.create_bucket()
        self.apply_permission_boundary(self.nested_stack)

    def get_directory_service_secret_arn(self, key: DirectoryServiceKey) -> str:
        scope = self.nested_stack
        service_account_dn_provided = cdk.CfnCondition(
            scope,
            "ServiceAccountDNProvided",
            expression=Fn.condition_not(
                Fn.condition_equals(self.parameters.get(key), ""),
            ),
        )

        secret = secretsmanager.Secret(
            scope,
            id=f"DirectoryServiceSecret{key}",
            secret_name=f"{self.cluster_name}-{MODULE_NAME_DIRECTORY_SERVICE}-{key}",
            secret_string_value=SecretValue.cfn_parameter(self.parameters.get(key)),
        )

        Tags.of(secret).add(
            key=ENVIRONMENT_NAME_TAG_KEY,
            value=self.cluster_name,
        )
        Tags.of(secret).add(
            key=RES_TAG_MODULE_NAME,
            value=MODULE_NAME_DIRECTORY_SERVICE,
        )
        Tags.of(secret).add(
            key=RES_TAG_MODULE_ID,
            value=MODULE_ID_DIRECTORY_SERVICE,
        )
        raw_secret = secret.node.default_child
        raw_secret.cfn_options.condition = service_account_dn_provided  # type: ignore

        return Fn.condition_if(
            service_account_dn_provided.logical_id,
            secret.secret_arn,
            cdk.Aws.NO_VALUE,
        ).to_string()

    def populate_default_values(self) -> cdk.CustomResource:
        lambda_name = f"{self.cluster_name}-DDBDefaultValuesPopulator"
        scope = self.nested_stack
        ddb_default_values_populator_role = iam.Role(
            scope,
            id="DDBDefaultValuesPopulatorRole",
            role_name=f"{lambda_name}Role",
            assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
        )
        ddb_default_values_populator_role_policy = iam.Policy(
            scope,
            id="DDBDefaultValuesPopulatorRolePolicy",
            policy_name=f"{lambda_name}Policy",
            statements=[
                iam.PolicyStatement(
                    actions=[
                        "logs:CreateLogGroup",
                        "logs:CreateLogStream",
                        "logs:PutLogEvents",
                        "logs:DeleteLogStream",
                    ],
                    sid="CloudWatchLogStreamPermissions",
                    resources=["*"],
                ),
                iam.PolicyStatement(
                    effect=iam.Effect.ALLOW,
                    actions=["dynamodb:Scan", "dynamodb:GetItem", "dynamodb:PutItem"],
                    resources=[
                        InfraUtils.get_ddb_table_arn(
                            self.cluster_name,
                            cluster_settings.CLUSTER_SETTINGS_TABLE_NAME,
                        ),
                        InfraUtils.get_ddb_table_arn(
                            self.cluster_name,
                            modules.MODULES_TABLE_NAME,
                        ),
                        InfraUtils.get_ddb_table_arn(
                            self.cluster_name,
                            permission_profiles.PERMISSION_PROFILE_TABLE_NAME,
                        ),
                        InfraUtils.get_ddb_table_arn(
                            self.cluster_name,
                            software_stacks.SOFTWARE_STACK_TABLE_NAME,
                        ),
                        InfraUtils.get_ddb_table_arn(
                            self.cluster_name,
                            email_templates.EMAIL_TEMPLATE_TABLE_NAME,
                        ),
                    ],
                ),
            ],
        )
        ddb_default_values_populator_role.attach_inline_policy(
            ddb_default_values_populator_role_policy
        )
        self.add_common_tags(ddb_default_values_populator_role)

        default_values_populator_handler = lambda_.Function(
            scope,
            "DDBDefaultValuesPopulator",
            function_name=lambda_name,
            runtime=RES_COMMON_LAMBDA_RUNTIME,
            timeout=cdk.Duration.seconds(300),
            environment={
                **self.generate_ddb_populator_environment_variables(),
            },
            role=ddb_default_values_populator_role,
            description="Lambda to populate default values in ddb",
            layers=[self.shared_library_lambda_layer],
            handler="lambda_functions.custom_resource.ddb_default_values_populator_lambda.handler.handler",
            code=lambda_.Code.from_asset(InfraUtils.resources_dir()),
        )
        self.add_common_tags(default_values_populator_handler)

        return cdk.CustomResource(
            scope,
            "CustomResourceDDBDefaultValuesPopulator",
            service_token=default_values_populator_handler.function_arn,
            removal_policy=cdk.RemovalPolicy.DESTROY,
            resource_type="Custom::RESDdbPopulator",
            properties={
                ENVIRONMENT_NAME_KEY: self.cluster_name,
                "shared_library_arn": self.shared_library_arn,
            },
        )

    def generate_ddb_populator_environment_variables(self) -> Dict[str, Any]:
        # create environment variables dict
        environment_variables = {
            # Essential environment variables
            "aws_partition": cdk.Aws.PARTITION,
            "aws_region": cdk.Aws.REGION,
            "aws_account_id": cdk.Aws.ACCOUNT_ID,
            "aws_dns_suffix": cdk.Aws.URL_SUFFIX,
            ###### Stack Input parameters ######
            # Environment and installer details
            ENVIRONMENT_NAME_KEY: self.cluster_name,
            "administrator_email": self.parameters.get_str(CommonKey.ADMIN_EMAIL),
            "instance_ami": self.parameters.get_str(CommonKey.INFRASTRUCTURE_HOST_AMI),
            "ssh_key_pair_name": self.parameters.get_str(CommonKey.SSH_KEY_PAIR),
            "client_ip": self.parameters.get_str(CommonKey.CLIENT_IP),
            "prefix_list": self.parameters.get_str(CommonKey.CLIENT_PREFIX_LIST),
            "permission_boundary_arn": self.parameters.get_str(
                CommonKey.IAM_PERMISSION_BOUNDARY
            ),
            # Network configuration for the RES environment
            "vpc_id": self.parameters.get_str(CommonKey.VPC_ID),
            "alb_public": self.parameters.get_str(
                CommonKey.IS_LOAD_BALANCER_INTERNET_FACING
            ),
            "load_balancer_subnet_ids": self.params_transformer.get_att_string(
                "LOAD_BALANCER_SUBNETS"
            ),
            "infrastructure_host_subnet_ids": self.params_transformer.get_att_string(
                "INFRA_SUBNETS"
            ),
            "vdi_subnet_ids": self.params_transformer.get_att_string("VDI_SUBNETS"),
            # Active Directory details
            "ad_name": self.parameters.get_str(DirectoryServiceKey.NAME),
            "ad_short_name": self.parameters.get_str(DirectoryServiceKey.AD_SHORT_NAME),
            "ldap_base": self.parameters.get_str(DirectoryServiceKey.LDAP_BASE),
            "ldap_connection_uri": self.parameters.get_str(
                DirectoryServiceKey.LDAP_CONNECTION_URI
            ),
            "service_account_credentials_secret_arn": self.parameters.get_str(
                DirectoryServiceKey.SERVICE_ACCOUNT_CREDENTIALS_SECRET_ARN
            ),
            "users_ou": self.parameters.get_str(DirectoryServiceKey.USERS_OU),
            "groups_ou": self.parameters.get_str(DirectoryServiceKey.GROUPS_OU),
            "sudoers_group_name": self.parameters.get_str(
                DirectoryServiceKey.SUDOERS_GROUP_NAME
            ),
            "computers_ou": self.parameters.get_str(DirectoryServiceKey.COMPUTERS_OU),
            "domain_tls_certificate_secret_arn": self.parameters.get_str(
                DirectoryServiceKey.DOMAIN_TLS_CERTIFICATE_SECRET_ARN
            ),
            "enable_ldap_id_mapping": self.parameters.get_str(
                DirectoryServiceKey.ENABLE_LDAP_ID_MAPPING
            ),
            "disable_ad_join": self.parameters.get_str(
                DirectoryServiceKey.DISABLE_AD_JOIN
            ),
            "root_user_dn_secret_arn": self.parameters.root_user_dn_secret_arn,
            # Shared Storage details
            "existing_home_fs_id": self.parameters.get_str(
                SharedStorageKey.SHARED_HOME_FILESYSTEM_ID
            ),
            # Custom domain details
            "webapp_custom_dns_name": self.parameters.get_str(
                CustomDomainKey.CUSTOM_DOMAIN_NAME_FOR_WEB_APP
            ),
            "acm_certificate_arn": self.parameters.get_str(
                CustomDomainKey.ACM_CERTIFICATE_ARN_FOR_WEB_APP
            ),
            "vdi_custom_dns_name": self.parameters.get_str(
                CustomDomainKey.CUSTOM_DOMAIN_NAME_FOR_VDI
            ),
            "certificate_secret_arn": self.parameters.get_str(
                CustomDomainKey.CERTIFICATE_SECRET_ARN_FOR_VDI
            ),
            "private_key_secret_arn": self.parameters.get_str(
                CustomDomainKey.PRIVATE_KEY_SECRET_ARN_FOR_VDI
            ),
            "shared_library_arn": self.shared_library_arn,
            #  Internet Proxy details
            "http_proxy_value": self.parameters.get_str(InternetProxyKey.HTTP_PROXY),
            "https_proxy_value": self.parameters.get_str(InternetProxyKey.HTTPS_PROXY),
            "no_proxy_value": self.parameters.get_str(InternetProxyKey.NO_PROXY),
        }
        return environment_variables

    def create_bucket(self) -> None:
        scope = self.nested_stack
        stack_id = cdk.Stack.of(scope).stack_id
        stack_id_suffix = cdk.Fn.select(
            0, cdk.Fn.split("-", cdk.Fn.select(2, cdk.Fn.split("/", stack_id)))
        )
        logging_bucket_name = f"log-{self.cluster_name}-cluster-{cdk.Aws.REGION}-{cdk.Aws.ACCOUNT_ID}-{stack_id_suffix}"
        logging_bucket = s3.Bucket(
            scope,
            "ClusterLoggingBucket",
            bucket_name=logging_bucket_name,
            encryption=s3.BucketEncryption.S3_MANAGED,
            removal_policy=RemovalPolicy.RETAIN,
        )

        logging_bucket.add_to_resource_policy(
            iam.PolicyStatement(
                effect=iam.Effect.ALLOW,
                actions=["s3:PutObject"],
                sid="AllowS3LogRequests",
                resources=[f"{logging_bucket.bucket_arn}/*"],
                principals=[iam.ServicePrincipal("logging.s3.amazonaws.com")],
            ),
        )

        staging_bucket_name = (
            f"{self.cluster_name}-cluster-{cdk.Aws.REGION}-{cdk.Aws.ACCOUNT_ID}"
        )
        staging_bucket = s3.Bucket(
            scope,
            "ClusterStagingBucket",
            bucket_name=staging_bucket_name,
            access_control=s3.BucketAccessControl.PRIVATE,
            encryption=s3.BucketEncryption.S3_MANAGED,
            removal_policy=RemovalPolicy.DESTROY,
            auto_delete_objects=True,
            versioned=True,
            server_access_logs_bucket=logging_bucket,
            server_access_logs_prefix="cluster-s3-bucket-logs/",
        )
        elb_principal_type = self.populator_custom_resource.get_att_string(
            "elb_principal_type"
        )
        elb_principal_value = self.populator_custom_resource.get_att_string(
            "elb_principal_value"
        )

        alb_access_logs_principal_json = CfnJson(
            self.nested_stack,
            "alb_access_logs_principal_json",
            value={elb_principal_type: elb_principal_value},
        )

        existing_staging_bucket_statement = []
        if staging_bucket.policy is not None:
            existing_staging_bucket_statement = (
                staging_bucket.policy.document.to_json().get("Statement", [])
            )
        staging_bucket_policy_document = {
            "Version": "2012-10-17",
            "Statement": existing_staging_bucket_statement
            + [
                {
                    "Sid": "IdeaAlbAccessLogs",
                    "Effect": "Allow",
                    "Principal": alb_access_logs_principal_json,
                    "Action": "s3:PutObject",
                    "Resource": f"{staging_bucket.bucket_arn}/logs/*",
                },
                {
                    "Sid": "AllowSSLRequestsOnly",
                    "Effect": "Deny",
                    "Principal": {"AWS": "*"},
                    "Action": "s3:*",
                    "Resource": [
                        f"{staging_bucket.bucket_arn}/*",
                        f"{staging_bucket.bucket_arn}",
                    ],
                    "Condition": {"Bool": {"aws:SecureTransport": "false"}},
                },
                {
                    "Sid": "IdeaNlbAccessLogs-AWSLogDeliveryWrite",
                    "Effect": "Allow",
                    "Principal": {"Service": f"delivery.logs.{cdk.Aws.URL_SUFFIX}"},
                    "Action": "s3:PutObject",
                    "Resource": f"{staging_bucket.bucket_arn}/logs/*",
                    "Condition": {
                        "StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"}
                    },
                },
                {
                    "Sid": "IdeaNlbAccessLogs-AWSLogDeliveryAclCheck",
                    "Effect": "Allow",
                    "Principal": {"Service": f"delivery.logs.{cdk.Aws.URL_SUFFIX}"},
                    "Action": "s3:GetBucketAcl",
                    "Resource": f"{staging_bucket.bucket_arn}",
                },
            ],
        }

        staging_bucket_policy = s3.CfnBucketPolicy(
            self.nested_stack,
            "ClusterStagingBucketPolicy",
            bucket=staging_bucket_name,
            policy_document=staging_bucket_policy_document,
        )
        staging_bucket_policy.apply_removal_policy(RemovalPolicy.RETAIN)

        staging_bucket.node.add_dependency(self.populator_custom_resource)
        staging_bucket_policy.node.add_dependency(self.populator_custom_resource)
        staging_bucket_policy.node.add_dependency(staging_bucket)
        cdk.Tags.of(staging_bucket).add(RES_TAG_BACKUP_PLAN, "cluster")
        cdk.Tags.of(staging_bucket).add(RES_TAG_ENVIRONMENT_NAME, self.cluster_name)
        cdk.Tags.of(logging_bucket).add(RES_TAG_ENVIRONMENT_NAME, self.cluster_name)

    def get_cluster_settings_table_event_handler_role(
        self,
    ) -> iam.Role:
        cluster_settings_table_event_handler_role = iam.Role(
            self.nested_stack,
            id="ClusterSettingsTableEventHandlerRole",
            role_name=f"{self.cluster_name}-cluster-settings-table-event-handler-role",
            assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
        )
        cluster_settings_table_event_handler_role.attach_inline_policy(
            iam.Policy(
                self.nested_stack,
                id="ClusterSettingsTableEventHandlerRolePolicy",
                policy_name=f"{self.cluster_name}-cluster-settings-table-event-handler-role-policy",
                statements=[
                    iam.PolicyStatement(
                        actions=["logs:CreateLogGroup"],
                        sid="CloudWatchLogsPermissions",
                        resources=["*"],
                    ),
                    iam.PolicyStatement(
                        actions=[
                            "logs:CreateLogStream",
                            "logs:PutLogEvents",
                            "logs:DeleteLogStream",
                        ],
                        sid="CloudWatchLogStreamPermissions",
                        resources=["*"],
                    ),
                    iam.PolicyStatement(
                        actions=[
                            "dynamodb:GetItem",
                            "dynamodb:PutItem",
                            "dynamodb:DeleteItem",
                        ],
                        sid="ADSyncLockTablePermissions",
                        resources=[
                            f"arn:{cdk.Aws.PARTITION}:dynamodb:{cdk.Aws.REGION}:{cdk.Aws.ACCOUNT_ID}:table/{self.cluster_name}.ad-sync.distributed-lock",
                        ],
                    ),
                    iam.PolicyStatement(
                        actions=[
                            "dynamodb:Query",
                            "dynamodb:Scan",
                            "dynamodb:UpdateItem",
                            "dynamodb:PutItem",
                        ],
                        sid="ADSyncStatusTablePermissions",
                        resources=[
                            f"arn:{cdk.Aws.PARTITION}:dynamodb:{cdk.Aws.REGION}:{cdk.Aws.ACCOUNT_ID}:table/{self.cluster_name}.ad-sync.status",
                        ],
                    ),
                    iam.PolicyStatement(
                        actions=[
                            "ecs:RunTask",
                            "ecs:StopTask",
                            "ecs:ListTasks",
                        ],
                        resources=["*"],
                        conditions={
                            "ArnEquals": {
                                "ecs:cluster": f"arn:{cdk.Aws.PARTITION}:ecs:{cdk.Aws.REGION}:{cdk.Aws.ACCOUNT_ID}:cluster/{self.cluster_name}-ad-sync-cluster"
                            }
                        },
                    ),
                    iam.PolicyStatement(
                        actions=["iam:PassRole"],
                        resources=[
                            f"arn:{cdk.Aws.PARTITION}:iam::{cdk.Aws.ACCOUNT_ID}:role/{self.cluster_name}-ad-sync-task-role",
                        ],
                    ),
                    iam.PolicyStatement(
                        actions=["ec2:DescribeSecurityGroups"],
                        resources=["*"],
                    ),
                ],
            )
        )

        self.add_common_tags(cluster_settings_table_event_handler_role)

        return cluster_settings_table_event_handler_role
