from typing import Optional, Any, Dict, List, Union, Callable

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.reference import AwsReference
from phi.cli.console import print_info
from phi.utils.log import logger


def get_my_ip() -> str:
    """Returns the network ip"""
    import httpx

    external_ip = httpx.get("https://checkip.amazonaws.com").text.strip()
    return f"{external_ip}/32"


class InboundRule(AwsResource):
    """
    Reference:
    - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/authorize_security_group_ingress.html
    """

    name: str = "InboundRule"
    resource_type: Optional[str] = "InboundRule"
    service_name: str = "ec2"

    # What to enable ingress for.
    # The IPv4 CIDR range. You can either specify a CIDR range or a source security group, not both.
    # To specify a single IPv4 address, use the /32 prefix length.
    cidr_ip: Optional[str] = None
    # The function to get the cidr_ip
    cidr_ip_function: Optional[Callable[..., str]] = None
    # The IPv6 CIDR range. You can either specify a CIDR range or a source security group, not both.
    # To specify a single IPv6 address, use the /128 prefix length.
    cidr_ipv6: Optional[str] = None
    # The function to get the cidr_ipv6
    cidr_ipv6_function: Optional[Callable[..., str]] = None
    # The security group id to allow access from.
    security_group_id: Optional[Union[str, AwsReference]] = None
    # The security group name to allow access from.
    # For a security group in a nondefault VPC, use the security group ID.
    security_group_name: Optional[str] = None
    # A description for this security group rule
    description: Optional[str] = None

    # The port to allow access from.
    # If provided, sets both from_port and to_port.
    port: Optional[int] = None
    # The port range to allow access from.
    from_port: Optional[int] = None
    # The port range to allow access from.
    to_port: Optional[int] = None
    # The protocol to allow access from. Default is tcp.
    ip_protocol: Optional[str] = None


class OutboundRule(AwsResource):
    """
    Reference:
    - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/authorize_security_group_ingress.html
    """

    name: str = "OutboundRule"
    resource_type: Optional[str] = "OutboundRule"
    service_name: str = "ec2"

    # What to enable egress for.
    # The IPv4 CIDR range. You can either specify a CIDR range or a source security group, not both.
    # To specify a single IPv4 address, use the /32 prefix length.
    cidr_ip: Optional[str] = None
    # The function to get the cidr_ip
    cidr_ip_function: Optional[Callable[..., str]] = None
    # The IPv6 CIDR range. You can either specify a CIDR range or a source security group, not both.
    # To specify a single IPv6 address, use the /128 prefix length.
    cidr_ipv6: Optional[str] = None
    # The function to get the cidr_ipv6
    cidr_ipv6_function: Optional[Callable[..., str]] = None
    # The security group id to allow access to.
    security_group_id: Optional[Union[str, AwsReference]] = None
    # The security group name to allow access to.
    # For a security group in a nondefault VPC, use the security group ID.
    security_group_name: Optional[str] = None
    # A description for this security group rule
    description: Optional[str] = None

    # The port to allow access from.
    # If provided, sets both from_port and to_port.
    port: Optional[int] = None
    # The port range to allow access from.
    from_port: Optional[int] = None
    # The port range to allow access from.
    to_port: Optional[int] = None
    # The protocol to allow access from. Default is tcp.
    ip_protocol: Optional[str] = None


class SecurityGroup(AwsResource):
    """
    Reference:
    - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/securitygroup/index.html
    - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/client/create_security_group.html
    """

    resource_type: Optional[str] = "SecurityGroup"
    resource_type_list: List[str] = ["sg"]
    service_name: str = "ec2"

    # The name of the security group.
    name: str
    # A description for the security group.
    description: Optional[str] = None
    # The ID of the VPC for the security group.
    vpc_id: Optional[str] = None
    # Derive the vpc_id from the subnets.
    # When more than one subnet is provided, both must be in the same VPC.
    subnets: Optional[List[Union[str, Subnet]]] = None
    # The tags to assign to the security group.
    tag_specifications: Optional[list] = None
    # Checks whether you have the required permissions for the action,
    # without actually making the request, and provides an error response.
    # If you have the required permissions, the error response is DryRunOperation.
    # Otherwise, it is UnauthorizedOperation.
    dry_run: Optional[bool] = None

    # The inbound rules associated with the security group.
    inbound_rules: Optional[List[InboundRule]] = None
    # The IP permissions to authorize ingress for
    ingress_ip_permissions: Optional[List[Dict[str, Any]]] = None
    # The outbound rules associated with the security group.
    outbound_rules: Optional[List[OutboundRule]] = None
    # The IP permissions to authorize egress for
    egress_ip_permissions: Optional[List[Dict[str, Any]]] = None

    # Security Group id
    group_id: Optional[str] = None

    def _create(self, aws_client: AwsApiClient) -> bool:
        """Creates the SecurityGroup

        Args:
            aws_client: The AwsApiClient for the current Security group
        """
        print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}")

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

        # Build description
        description = self.description or "Created by phi"
        if description is not None:
            not_null_args["Description"] = description

        # Get vpc_id
        vpc_id = self.vpc_id
        if vpc_id is None and self.subnets is not None:
            from phi.aws.resource.ec2.subnet import get_vpc_id_from_subnet_ids

            subnet_ids = []
            for subnet in self.subnets:
                if isinstance(subnet, Subnet):
                    subnet_ids.append(subnet.name)
                elif isinstance(subnet, str):
                    subnet_ids.append(subnet)
            vpc_id = get_vpc_id_from_subnet_ids(subnet_ids, aws_client)
        if vpc_id is not None:
            not_null_args["VpcId"] = vpc_id

        if self.tag_specifications:
            not_null_args["TagSpecifications"] = self.tag_specifications
        if self.dry_run:
            not_null_args["DryRun"] = self.dry_run

        # Step 2: Create Security group
        service_client = self.get_service_client(aws_client)
        try:
            create_response = service_client.create_security_group(
                GroupName=self.name,
                **not_null_args,
            )
            logger.debug(f"Response: {create_response}")

            # Validate resource creation
            if create_response 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 SecurityGroup to be created
        if self.wait_for_create:
            try:
                print_info(f"Waiting for {self.get_resource_type()} to be created.")
                waiter = self.get_service_client(aws_client).get_waiter("security_group_exists")
                waiter.wait(
                    Filters=[
                        {
                            "Name": "group-name",
                            "Values": [self.name],
                        },
                    ],
                    WaiterConfig={
                        "Delay": self.waiter_delay,
                        "MaxAttempts": self.waiter_max_attempts,
                    },
                )
            except Exception as e:
                logger.error("Waiter failed.")
                logger.error(e)
                return False

        # Add inbound rules
        if self.inbound_rules is not None or self.ingress_ip_permissions:
            _success = self.add_inbound_rules(aws_client)
            if not _success:
                return False
        # Add outbound rules
        if self.outbound_rules is not None or self.egress_ip_permissions:
            _success = self.add_outbound_rules(aws_client)
            if not _success:
                return False
        return True

    def _read(self, aws_client: AwsApiClient) -> Optional[Any]:
        """Reads the SecurityGroup

        Args:
            aws_client: The AwsApiClient for the current session
        """
        from botocore.exceptions import ClientError

        logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}")
        service_client = self.get_service_client(aws_client)
        try:
            describe_response = service_client.describe_security_groups(
                Filters=[
                    {
                        "Name": "group-name",
                        "Values": [self.name],
                    },
                ],
            )
            logger.debug(f"Response: {describe_response}")
            resource_list = describe_response.get("SecurityGroups", None)

            if resource_list is not None and isinstance(resource_list, list):
                for resource in resource_list:
                    if resource.get("GroupName", None) == self.name:
                        self.active_resource = resource
        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:
        """Deletes the SecurityGroup

        Args:
            aws_client: The AwsApiClient for the current session
        """
        from botocore.exceptions import ClientError

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

        service_client = self.get_service_client(aws_client)
        self.active_resource = None

        try:
            group_id = self.get_security_group_id(aws_client)
            if group_id is not None:
                delete_response = service_client.delete_security_group(GroupId=group_id)
            else:
                delete_response = service_client.delete_security_group(GroupName=self.name)
            logger.debug(f"Response: {delete_response}")

            return True
        except ClientError as ce:
            ce_resp = ce.response
            if ce_resp is not None:
                if ce_resp.get("Error", {}).get("Code", "") == "DependencyViolation":
                    logger.warning(
                        f"SecurityGroup {self.get_resource_name()} could not be deleted "
                        f"as it is being used by another resource."
                    )
                    if ce_resp.get("Error", {}).get("Message", "") != "":
                        logger.warning(f"Error: {ce_resp.get('Error', {}).get('Message', '')}")
                    logger.warning("Please try again later or delete resources manually.")
        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 _update(self, aws_client: AwsApiClient) -> bool:
        """Updates the SecurityGroup"""

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

        # Step 1: Update inbound rules
        if self.inbound_rules is not None or self.ingress_ip_permissions:
            _success = self.add_inbound_rules(aws_client)
            if not _success:
                return False
        # Step 2: Update outbound rules
        if self.outbound_rules is not None or self.egress_ip_permissions:
            _success = self.add_outbound_rules(aws_client)
            if not _success:
                return False
        return True

    def get_security_group_id(self, aws_client: Optional[AwsApiClient] = None) -> Optional[str]:
        """Returns the security group id"""

        if self.group_id is not None:
            return self.group_id

        resource = self.read(aws_client)
        if resource is not None:
            self.group_id = resource.get("GroupId", None)

        return self.group_id

    def add_inbound_rules(self, aws_client: AwsApiClient) -> bool:
        """Adds the specified inbound (ingress) rules to a security group.

        Args:
            aws_client: The AwsApiClient for the current session
        """
        from botocore.exceptions import ClientError

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

        group_id = self.get_security_group_id(aws_client)
        if group_id is None:
            logger.warning(f"GroupId for {self.get_resource_name()} not found.")
            return False
        api_args["GroupId"] = group_id
        if self.dry_run is not None:
            api_args["DryRun"] = self.dry_run

        service_client = self.get_service_client(aws_client)

        # Add ingress_ip_permissions
        if self.ingress_ip_permissions is not None:
            try:
                response = service_client.authorize_security_group_ingress(
                    IpPermissions=self.ingress_ip_permissions, **api_args
                )
                logger.debug(f"Response: {response}")

                # Validate the response
                if response is None or response.get("Return") is False:
                    logger.error(f"Ingress rules could not be added to {self.get_resource_name()}")
                    return False
            except ClientError as ce:
                ce_resp = ce.response
                if ce_resp is not None:
                    if ce_resp.get("Error", {}).get("Code", "") == "InvalidPermission.Duplicate":
                        pass
                logger.debug(f"ClientError: {ce}")
            except Exception as e:
                logger.error(f"Ingress rules could not be added to {self.get_resource_name()}: {e}")
                return False

        # Add inbound_rules
        if self.inbound_rules is not None:
            for rule in self.inbound_rules:
                ip_permission: Dict[str, Any] = {"IpProtocol": rule.ip_protocol or "tcp"}
                if rule.from_port is not None:
                    ip_permission["FromPort"] = rule.from_port
                if rule.to_port is not None:
                    ip_permission["ToPort"] = rule.to_port
                if rule.port is not None:
                    ip_permission["FromPort"] = rule.port
                    ip_permission["ToPort"] = rule.port

                # Get cidr_ip
                _cidr_ip: Optional[str] = None
                if rule.cidr_ip is not None:
                    _cidr_ip = rule.cidr_ip
                elif rule.cidr_ip_function is not None:
                    try:
                        _cidr_ip = rule.cidr_ip_function()
                    except Exception as e:
                        logger.warning(f"Error getting cidr_ip for {self.get_resource_name()}: {e}")
                if _cidr_ip is not None:
                    ip_permission["IpRanges"] = [
                        {
                            "CidrIp": _cidr_ip,
                            "Description": rule.description or "",
                        },
                    ]

                # Get cidr_ipv6
                _cidr_ipv6: Optional[str] = None
                if rule.cidr_ipv6 is not None:
                    _cidr_ipv6 = rule.cidr_ipv6
                elif rule.cidr_ipv6_function is not None:
                    try:
                        _cidr_ipv6 = rule.cidr_ipv6_function()
                    except Exception as e:
                        logger.warning(f"Error getting cidr_ipv6 for {self.get_resource_name()}: {e}")
                if _cidr_ipv6 is not None:
                    ip_permission["Ipv6Ranges"] = [
                        {
                            "CidrIpv6": _cidr_ipv6,
                            "Description": rule.description or "",
                        },
                    ]

                if _cidr_ip is None and _cidr_ipv6 is None:
                    source_sg_id: Optional[str] = None
                    # If security_group_id is specified, use that
                    # Otherwise, use the current security group id
                    if rule.security_group_id is not None:
                        if isinstance(rule.security_group_id, str):
                            source_sg_id = rule.security_group_id
                        elif isinstance(rule.security_group_id, AwsReference):
                            source_sg_id = rule.security_group_id.get_reference(aws_client=aws_client)
                    else:
                        source_sg_id = group_id

                    # Either security_group_id or security_group_name must be specified
                    # for the rule to be valid
                    if source_sg_id is not None or rule.security_group_name is not None:
                        user_id_group_pair = {}
                        if source_sg_id is not None:
                            user_id_group_pair["GroupId"] = source_sg_id
                        if rule.security_group_name is not None:
                            user_id_group_pair["GroupName"] = rule.security_group_name
                        if rule.description is not None:
                            user_id_group_pair["Description"] = rule.description
                        ip_permission["UserIdGroupPairs"] = [user_id_group_pair]

                logger.debug(f"Adding Inbound Rule: {ip_permission}")
                try:
                    response = service_client.authorize_security_group_ingress(
                        IpPermissions=[ip_permission], **api_args
                    )
                    logger.debug(f"Response: {response}")

                    # Validate the response
                    if response is None or response.get("Return") is False:
                        logger.error(f"Ingress rules could not be added to {self.get_resource_name()}")
                        return False
                except ClientError as ce:
                    ce_resp = ce.response
                    if ce_resp is not None:
                        if ce_resp.get("Error", {}).get("Code", "") == "InvalidPermission.Duplicate":
                            pass
                    logger.debug(f"ClientError: {ce}")
                except Exception as e:
                    logger.error(f"Ingress rules could not be added to {self.get_resource_name()}: {e}")
                    return False
        return True

    def add_outbound_rules(self, aws_client: AwsApiClient) -> bool:
        """Adds the specified outbound (egress) rules to a security group.

        Args:
            aws_client: The AwsApiClient for the current session
        """
        from botocore.exceptions import ClientError

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

        group_id = self.get_security_group_id(aws_client)
        if group_id is None:
            logger.warning(f"GroupId for {self.get_resource_name()} not found.")
            return False
        api_args["GroupId"] = group_id
        if self.dry_run is not None:
            api_args["DryRun"] = self.dry_run

        service_client = self.get_service_client(aws_client)

        # Add egress_ip_permissions
        if self.egress_ip_permissions is not None:
            try:
                response = service_client.authorize_security_group_egress(
                    IpPermissions=self.egress_ip_permissions, **api_args
                )
                logger.debug(f"Response: {response}")

                # Validate the response
                if response is None or response.get("Return") is False:
                    logger.error(f"Egress rules could not be added to {self.get_resource_name()}")
                    return False
            except ClientError as ce:
                ce_resp = ce.response
                if ce_resp is not None:
                    if ce_resp.get("Error", {}).get("Code", "") == "InvalidPermission.Duplicate":
                        pass
                logger.debug(f"ClientError: {ce}")
            except Exception as e:
                logger.error(f"Egress rules could not be added to {self.get_resource_name()}: {e}")
                return False

        # Add outbound_rules
        if self.outbound_rules is not None:
            for rule in self.outbound_rules:
                ip_permission: Dict[str, Any] = {"IpProtocol": rule.ip_protocol or "tcp"}
                if rule.from_port is not None:
                    ip_permission["FromPort"] = rule.from_port
                if rule.to_port is not None:
                    ip_permission["ToPort"] = rule.to_port
                if rule.port is not None:
                    ip_permission["FromPort"] = rule.port
                    ip_permission["ToPort"] = rule.port

                # Get cidr_ip
                _cidr_ip: Optional[str] = None
                if rule.cidr_ip is not None:
                    _cidr_ip = rule.cidr_ip
                elif rule.cidr_ip_function is not None:
                    try:
                        _cidr_ip = rule.cidr_ip_function()
                    except Exception as e:
                        logger.warning(f"Error getting cidr_ip for {self.get_resource_name()}: {e}")
                if _cidr_ip is not None:
                    ip_permission["IpRanges"] = [
                        {
                            "CidrIp": _cidr_ip,
                            "Description": rule.description or "",
                        },
                    ]

                # Get cidr_ipv6
                _cidr_ipv6: Optional[str] = None
                if rule.cidr_ipv6 is not None:
                    _cidr_ipv6 = rule.cidr_ipv6
                elif rule.cidr_ipv6_function is not None:
                    try:
                        _cidr_ipv6 = rule.cidr_ipv6_function()
                    except Exception as e:
                        logger.warning(f"Error getting cidr_ipv6 for {self.get_resource_name()}: {e}")
                if _cidr_ipv6 is not None:
                    ip_permission["Ipv6Ranges"] = [
                        {
                            "CidrIpv6": _cidr_ipv6,
                            "Description": rule.description or "",
                        },
                    ]

                if _cidr_ip is None and _cidr_ipv6 is None:
                    destination_sg_id: Optional[str] = None
                    if isinstance(rule.security_group_id, str):
                        destination_sg_id = rule.security_group_id
                    elif isinstance(rule.security_group_id, AwsReference):
                        destination_sg_id = rule.security_group_id.get_reference(aws_client=aws_client)

                    user_id_group_pair = {}
                    if destination_sg_id is not None:
                        user_id_group_pair["GroupId"] = destination_sg_id
                    if rule.security_group_name is not None:
                        user_id_group_pair["GroupName"] = rule.security_group_name
                    if rule.description is not None:
                        user_id_group_pair["Description"] = rule.description
                    ip_permission["UserIdGroupPairs"] = [user_id_group_pair]

                logger.debug(f"Adding Outbound Rule: {ip_permission}")
                try:
                    response = service_client.authorize_security_group_egress(IpPermissions=[ip_permission], **api_args)
                    logger.debug(f"Response: {response}")

                    # Validate the response
                    if response is None or response.get("Return") is False:
                        logger.error(f"Egress rules could not be added to {self.get_resource_name()}")
                        return False
                except ClientError as ce:
                    ce_resp = ce.response
                    if ce_resp is not None:
                        if ce_resp.get("Error", {}).get("Code", "") == "InvalidPermission.Duplicate":
                            pass
                    logger.debug(f"ClientError: {ce}")
                except Exception as e:
                    logger.error(f"Egress rules could not be added to {self.get_resource_name()}: {e}")
                    return False
        return True
