from typing import Optional, Any, Dict, List, Union
from typing_extensions import Literal

from phi.aws.api_client import AwsApiClient
from phi.aws.resource.base import AwsResource
from phi.aws.resource.ec2.subnet import Subnet
from phi.aws.resource.ec2.security_group import SecurityGroup
from phi.aws.resource.ecs.cluster import EcsCluster
from phi.aws.resource.ecs.task_definition import EcsTaskDefinition
from phi.aws.resource.elb.target_group import TargetGroup
from phi.cli.console import print_info
from phi.utils.log import logger


class EcsService(AwsResource):
    """
    Reference:
    - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html
    """

    resource_type: Optional[str] = "Service"
    service_name: str = "ecs"

    # Name for the service.
    name: str
    # Name for the service.
    # Use name if not provided.
    ecs_service_name: Optional[str] = None

    # EcsCluster for the service.
    # Can be
    # - string: The short name or full Amazon Resource Name (ARN) of the cluster
    # - EcsCluster
    # If you do not specify a cluster, the default cluster is assumed.
    cluster: Optional[Union[EcsCluster, str]] = None

    # EcsTaskDefinition for the service.
    # Can be
    # - string: The family and revision (family:revision ) or full ARN of the task definition.
    # - EcsTaskDefinition
    # If a revision isn't specified, the latest ACTIVE revision is used.
    task_definition: Optional[Union[EcsTaskDefinition, str]] = None

    # A load balancer object representing the load balancers to use with your service.
    load_balancers: Optional[List[Dict[str, Any]]] = None

    # We can generate the load_balancers dict using
    # the target_group, target_container_name and target_container_port
    # Target group to attach to a service.
    target_group: Optional[TargetGroup] = None
    # Target container name for the service.
    target_container_name: Optional[str] = None
    target_container_port: Optional[int] = None

    # The network configuration for the service. This parameter is required for task definitions that
    # use the awsvpc network mode to receive their own elastic network interface
    network_configuration: Optional[Dict[str, Any]] = None
    subnets: Optional[List[Union[str, Subnet]]] = None
    security_groups: Optional[List[Union[str, SecurityGroup]]] = None
    assign_public_ip: Optional[bool] = None

    # The configuration for this service to discover and connect to services,
    # and be discovered by, and connected from, other services within a namespace.
    service_connect_configuration: Optional[Dict[str, Any]] = None

    # The details of the service discovery registries to assign to this service.
    service_registries: Optional[List[Dict[str, Any]]] = None
    # The number of instantiations of the specified task definition to place and keep running on your cluster.
    # This is required if schedulingStrategy is REPLICA or isn't specified.
    # If schedulingStrategy is DAEMON then this isn't required.
    desired_count: Optional[int] = None
    # An identifier that you provide to ensure the idempotency of the request. It must be unique and is case-sensitive.
    client_token: Optional[str] = None
    # The infrastructure that you run your service on.
    launch_type: Optional[Union[str, Literal["EC2", "FARGATE", "EXTERNAL"]]] = None
    # The capacity provider strategy to use for the service.
    capacity_provider_strategy: Optional[List[Dict[str, Any]]] = None
    platform_version: Optional[str] = None
    role: Optional[str] = None
    deployment_configuration: Optional[Dict[str, Any]] = None
    placement_constraints: Optional[List[Dict[str, Any]]] = None
    placement_strategy: Optional[List[Dict[str, Any]]] = None
    health_check_grace_period_seconds: Optional[int] = None
    scheduling_strategy: Optional[Literal["REPLICA", "DAEMON"]] = None
    deployment_controller: Optional[Dict[str, Any]] = None
    tags: Optional[List[Dict[str, Any]]] = None
    enable_ecsmanaged_tags: Optional[bool] = None
    propagate_tags: Optional[Literal["TASK_DEFINITION", "SERVICE", "NONE"]] = None
    enable_execute_command: Optional[bool] = None

    force_delete: Optional[bool] = None
    # Force a new deployment of the service on update.
    # By default, deployments aren't forced.
    # You can use this option to start a new deployment with no service
    # definition changes. For example, you can update a service's
    # tasks to use a newer Docker image with the same
    # image/tag combination (my_image:latest ) or
    # to roll Fargate tasks onto a newer platform version.
    force_new_deployment: Optional[bool] = None

    wait_for_create: bool = False

    def get_ecs_service_name(self):
        return self.ecs_service_name or self.name

    def get_ecs_cluster_name(self):
        if self.cluster is not None:
            if isinstance(self.cluster, EcsCluster):
                return self.cluster.get_ecs_cluster_name()
            else:
                return self.cluster

    def get_ecs_task_definition(self):
        if self.task_definition is not None:
            if isinstance(self.task_definition, EcsTaskDefinition):
                return self.task_definition.get_task_family()
            else:
                return self.task_definition

    def _create(self, aws_client: AwsApiClient) -> bool:
        """Create EcsService"""
        print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}")

        # create a dict of args which are not null, otherwise aws type validation fails
        not_null_args: Dict[str, Any] = {}

        cluster_name = self.get_ecs_cluster_name()
        if cluster_name is not None:
            not_null_args["cluster"] = cluster_name

        network_configuration = self.network_configuration
        if network_configuration is None and (self.subnets is not None or self.security_groups is not None):
            aws_vpc_config: Dict[str, Any] = {}
            if self.subnets is not None:
                subnet_ids = []
                for subnet in self.subnets:
                    if isinstance(subnet, Subnet):
                        subnet_ids.append(subnet.name)
                    elif isinstance(subnet, str):
                        subnet_ids.append(subnet)
                aws_vpc_config["subnets"] = subnet_ids
            if self.security_groups is not None:
                security_group_ids = []
                for sg in self.security_groups:
                    if isinstance(sg, SecurityGroup):
                        security_group_ids.append(sg.get_security_group_id(aws_client))
                    else:
                        security_group_ids.append(sg)
                aws_vpc_config["securityGroups"] = security_group_ids
            if self.assign_public_ip:
                aws_vpc_config["assignPublicIp"] = "ENABLED"
            network_configuration = {"awsvpcConfiguration": aws_vpc_config}
        if network_configuration is not None:
            not_null_args["networkConfiguration"] = network_configuration

        if self.service_connect_configuration is not None:
            not_null_args["serviceConnectConfiguration"] = self.service_connect_configuration

        if self.service_registries is not None:
            not_null_args["serviceRegistries"] = self.service_registries
        if self.desired_count is not None:
            not_null_args["desiredCount"] = self.desired_count
        if self.client_token is not None:
            not_null_args["clientToken"] = self.client_token
        if self.launch_type is not None:
            not_null_args["launchType"] = self.launch_type
        if self.capacity_provider_strategy is not None:
            not_null_args["capacityProviderStrategy"] = self.capacity_provider_strategy
        if self.platform_version is not None:
            not_null_args["platformVersion"] = self.platform_version
        if self.role is not None:
            not_null_args["role"] = self.role
        if self.deployment_configuration is not None:
            not_null_args["deploymentConfiguration"] = self.deployment_configuration
        if self.placement_constraints is not None:
            not_null_args["placementConstraints"] = self.placement_constraints
        if self.placement_strategy is not None:
            not_null_args["placementStrategy"] = self.placement_strategy
        if self.health_check_grace_period_seconds is not None:
            not_null_args["healthCheckGracePeriodSeconds"] = self.health_check_grace_period_seconds
        if self.scheduling_strategy is not None:
            not_null_args["schedulingStrategy"] = self.scheduling_strategy
        if self.deployment_controller is not None:
            not_null_args["deploymentController"] = self.deployment_controller
        if self.tags is not None:
            not_null_args["tags"] = self.tags
        if self.enable_ecsmanaged_tags is not None:
            not_null_args["enableECSManagedTags"] = self.enable_ecsmanaged_tags
        if self.propagate_tags is not None:
            not_null_args["propagateTags"] = self.propagate_tags
        if self.enable_execute_command is not None:
            not_null_args["enableExecuteCommand"] = self.enable_execute_command

        if self.load_balancers is not None:
            not_null_args["loadBalancers"] = self.load_balancers
        elif self.target_group is not None and self.target_container_name is not None:
            not_null_args["loadBalancers"] = [
                {
                    "targetGroupArn": self.target_group.get_arn(aws_client),
                    "containerName": self.target_container_name,
                    "containerPort": self.target_container_port,
                }
            ]

        # Register EcsService
        service_client = self.get_service_client(aws_client)
        try:
            create_response = service_client.create_service(
                serviceName=self.get_ecs_service_name(),
                taskDefinition=self.get_ecs_task_definition(),
                **not_null_args,
            )
            logger.debug(f"EcsService: {create_response}")
            resource_dict = create_response.get("service", {})

            # Validate resource creation
            if resource_dict is not None:
                self.active_resource = create_response
                return True
        except Exception as e:
            logger.error(f"{self.get_resource_type()} could not be created.")
            logger.error(e)
        return False

    def post_create(self, aws_client: AwsApiClient) -> bool:
        # Wait for EcsService to be created
        if self.wait_for_create:
            try:
                cluster_name = self.get_ecs_cluster_name()
                if cluster_name is not None:
                    print_info(f"Waiting for {self.get_resource_type()} to be available.")
                    waiter = self.get_service_client(aws_client).get_waiter("services_stable")
                    waiter.wait(
                        cluster=cluster_name,
                        services=[self.get_ecs_service_name()],
                        WaiterConfig={
                            "Delay": self.waiter_delay,
                            "MaxAttempts": self.waiter_max_attempts,
                        },
                    )
                else:
                    logger.warning("Skipping waiter, no Service found")
            except Exception as e:
                logger.error("Waiter failed.")
                logger.error(e)
        return True

    def _read(self, aws_client: AwsApiClient) -> Optional[Any]:
        """Read EcsService"""
        from botocore.exceptions import ClientError

        logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}")

        # create a dict of args which are not null, otherwise aws type validation fails
        not_null_args: Dict[str, Any] = {}

        cluster_name = self.get_ecs_cluster_name()
        if cluster_name is not None:
            not_null_args["cluster"] = cluster_name

        service_client = self.get_service_client(aws_client)
        try:
            service_name: str = self.get_ecs_service_name()
            describe_response = service_client.describe_services(services=[service_name], **not_null_args)
            logger.debug(f"EcsService: {describe_response}")
            resource_list = describe_response.get("services", None)

            if resource_list is not None and isinstance(resource_list, list):
                for resource in resource_list:
                    _service_name: str = resource.get("serviceName", None)
                    if _service_name == service_name:
                        _service_status = resource.get("status", None)
                        if _service_status == "ACTIVE":
                            self.active_resource = resource
                            break
        except ClientError as ce:
            logger.debug(f"ClientError: {ce}")
        except Exception as e:
            logger.error(f"Error reading {self.get_resource_type()}.")
            logger.error(e)
        return self.active_resource

    def _delete(self, aws_client: AwsApiClient) -> bool:
        """Delete EcsService"""
        print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}")

        # create a dict of args which are not null, otherwise aws type validation fails
        not_null_args: Dict[str, Any] = {}

        cluster_name = self.get_ecs_cluster_name()
        if cluster_name is not None:
            not_null_args["cluster"] = cluster_name
        if self.force_delete is not None:
            not_null_args["force"] = self.force_delete

        service_client = self.get_service_client(aws_client)
        self.active_resource = None
        try:
            delete_response = service_client.delete_service(
                service=self.get_ecs_service_name(),
                **not_null_args,
            )
            logger.debug(f"EcsService: {delete_response}")
            return True
        except Exception as e:
            logger.error(f"{self.get_resource_type()} could not be deleted.")
            logger.error("Please try again or delete resources manually.")
            logger.error(e)
        return False

    def post_delete(self, aws_client: AwsApiClient) -> bool:
        # Wait for EcsService to be deleted
        if self.wait_for_delete:
            try:
                cluster_name = self.get_ecs_cluster_name()
                if cluster_name is not None:
                    print_info(f"Waiting for {self.get_resource_type()} to be deleted.")
                    waiter = self.get_service_client(aws_client).get_waiter("services_inactive")
                    waiter.wait(
                        cluster=cluster_name,
                        services=[self.get_ecs_service_name()],
                        WaiterConfig={
                            "Delay": self.waiter_delay,
                            "MaxAttempts": self.waiter_max_attempts,
                        },
                    )
                else:
                    logger.warning("Skipping waiter, no Service found")
            except Exception as e:
                logger.error("Waiter failed.")
                logger.error(e)
        return True

    def _update(self, aws_client: AwsApiClient) -> bool:
        """Updates the EcsService

        Args:
            aws_client: The AwsApiClient for the current cluster
        """

        print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}")

        # create a dict of args which are not null, otherwise aws type validation fails
        not_null_args: Dict[str, Any] = {}

        cluster_name = self.get_ecs_cluster_name()
        if cluster_name is not None:
            not_null_args["cluster"] = cluster_name

        network_configuration = self.network_configuration
        if network_configuration is None and (self.subnets is not None or self.security_groups is not None):
            aws_vpc_config: Dict[str, Any] = {}
            if self.subnets is not None:
                subnet_ids = []
                for subnet in self.subnets:
                    if isinstance(subnet, Subnet):
                        subnet_ids.append(subnet.name)
                    elif isinstance(subnet, str):
                        subnet_ids.append(subnet)
                aws_vpc_config["subnets"] = subnet_ids
            if self.security_groups is not None:
                security_group_ids = []
                for sg in self.security_groups:
                    if isinstance(sg, SecurityGroup):
                        security_group_ids.append(sg.get_security_group_id(aws_client))
                    else:
                        security_group_ids.append(sg)
                aws_vpc_config["securityGroups"] = security_group_ids
            if self.assign_public_ip:
                aws_vpc_config["assignPublicIp"] = "ENABLED"
            network_configuration = {"awsvpcConfiguration": aws_vpc_config}
        if self.network_configuration is not None:
            not_null_args["networkConfiguration"] = network_configuration

        if self.desired_count is not None:
            not_null_args["desiredCount"] = self.desired_count
        if self.capacity_provider_strategy is not None:
            not_null_args["capacityProviderStrategy"] = self.capacity_provider_strategy
        if self.deployment_configuration is not None:
            not_null_args["deploymentConfiguration"] = self.deployment_configuration
        if self.placement_constraints is not None:
            not_null_args["placementConstraints"] = self.placement_constraints
        if self.placement_strategy is not None:
            not_null_args["placementStrategy"] = self.placement_strategy
        if self.platform_version is not None:
            not_null_args["platformVersion"] = self.platform_version
        if self.force_new_deployment is not None:
            not_null_args["forceNewDeployment"] = self.force_new_deployment
        if self.health_check_grace_period_seconds is not None:
            not_null_args["healthCheckGracePeriodSeconds"] = self.health_check_grace_period_seconds
        if self.enable_execute_command is not None:
            not_null_args["enableExecuteCommand"] = self.enable_execute_command
        if self.enable_ecsmanaged_tags is not None:
            not_null_args["enableECSManagedTags"] = self.enable_ecsmanaged_tags
        if self.load_balancers is not None:
            not_null_args["loadBalancers"] = self.load_balancers
        if self.propagate_tags is not None:
            not_null_args["propagateTags"] = self.propagate_tags
        if self.service_registries is not None:
            not_null_args["serviceRegistries"] = self.service_registries

        try:
            # Update EcsService
            service_client = self.get_service_client(aws_client)
            update_response = service_client.update_service(
                service=self.get_ecs_service_name(),
                taskDefinition=self.get_ecs_task_definition(),
                **not_null_args,
            )
            logger.debug(f"update_response: {update_response}")

            self.active_resource = update_response.get("service", None)
            if self.active_resource is not None:
                print_info(f"{self.get_resource_type()}: {self.get_resource_name()} updated")
                return True
        except Exception as e:
            logger.error(f"{self.get_resource_type()} could not be updated.")
            logger.error("Please try again or update resources manually.")
            logger.error(e)
        return False
