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

from phi.aws.api_client import AwsApiClient
from phi.aws.resource.base import AwsResource
from phi.aws.resource.s3.object import S3Object
from phi.cli.console import print_info
from phi.utils.log import logger


class S3Bucket(AwsResource):
    """
    Reference:
    - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#service-resource
    """

    resource_type: str = "s3"
    service_name: str = "s3"

    # Name of the bucket
    name: str
    # The canned ACL to apply to the bucket.
    acl: Optional[Literal["private", "public-read", "public-read-write", "authenticated-read"]] = None
    grant_full_control: Optional[str] = None
    grant_read: Optional[str] = None
    grant_read_ACP: Optional[str] = None
    grant_write: Optional[str] = None
    grant_write_ACP: Optional[str] = None
    object_lock_enabled_for_bucket: Optional[bool] = None
    object_ownership: Optional[Literal["BucketOwnerPreferred", "ObjectWriter", "BucketOwnerEnforced"]] = None

    @property
    def uri(self) -> str:
        """Returns the URI of the s3.Bucket

        Returns:
            str: The URI of the s3.Bucket
        """
        return f"s3://{self.name}"

    def get_resource(self, aws_client: Optional[AwsApiClient] = None) -> Optional[Any]:
        """Returns the s3.Bucket

        Args:
            aws_client: The AwsApiClient for the current cluster
        """
        client: AwsApiClient = aws_client or self.get_aws_client()
        service_resource = self.get_service_resource(client)
        return service_resource.Bucket(name=self.name)

    def _create(self, aws_client: AwsApiClient) -> bool:
        """Creates the s3.Bucket

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

        # Step 1: Build bucket configuration
        # Bucket names are GLOBALLY unique!
        # AWS will give you the IllegalLocationConstraintException if you collide
        # with an already existing bucket if you've specified a region different than
        # the region of the already existing bucket. If you happen to guess the correct region of the
        # existing bucket it will give you the BucketAlreadyExists exception.
        bucket_configuration = None
        if aws_client.aws_region is not None and aws_client.aws_region != "us-east-1":
            bucket_configuration = {"LocationConstraint": aws_client.aws_region}

        # create a dict of args which are not null, otherwise aws type validation fails
        not_null_args: Dict[str, Any] = {}
        if bucket_configuration:
            not_null_args["CreateBucketConfiguration"] = bucket_configuration
        if self.acl:
            not_null_args["ACL"] = self.acl
        if self.grant_full_control:
            not_null_args["GrantFullControl"] = self.grant_full_control
        if self.grant_read:
            not_null_args["GrantRead"] = self.grant_read
        if self.grant_read_ACP:
            not_null_args["GrantReadACP"] = self.grant_read_ACP
        if self.grant_write:
            not_null_args["GrantWrite"] = self.grant_write
        if self.grant_write_ACP:
            not_null_args["GrantWriteACP"] = self.grant_write_ACP
        if self.object_lock_enabled_for_bucket:
            not_null_args["ObjectLockEnabledForBucket"] = self.object_lock_enabled_for_bucket
        if self.object_ownership:
            not_null_args["ObjectOwnership"] = self.object_ownership

        # Step 2: Create Bucket
        service_client = self.get_service_client(aws_client)
        try:
            response = service_client.create_bucket(
                Bucket=self.name,
                **not_null_args,
            )
            logger.debug(f"Response: {response}")
            bucket_location = response.get("Location")
            if bucket_location is not None:
                logger.debug(f"Bucket created: {bucket_location}")
                self.active_resource = 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 Bucket 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("bucket_exists")
                waiter.wait(
                    Bucket=self.name,
                    WaiterConfig={
                        "Delay": self.waiter_delay,
                        "MaxAttempts": self.waiter_max_attempts,
                    },
                )
            except Exception as e:
                logger.error("Waiter failed.")
                logger.error(e)
        return True

    def _read(self, aws_client: AwsApiClient) -> Optional[Any]:
        """Returns the s3.Bucket

        Args:
            aws_client: The AwsApiClient for the current cluster
        """
        logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}")

        from botocore.exceptions import ClientError

        try:
            service_resource = self.get_service_resource(aws_client)
            bucket = service_resource.Bucket(name=self.name)
            bucket.load()
            creation_date = bucket.creation_date
            logger.debug(f"Bucket creation_date: {creation_date}")
            if creation_date is not None:
                logger.debug(f"Bucket found: {bucket.name}")
                self.active_resource = {
                    "name": bucket.name,
                    "creation_date": creation_date,
                }
        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 s3.Bucket

        Args:
            aws_client: The AwsApiClient for the current cluster
        """
        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:
            response = service_client.delete_bucket(Bucket=self.name)
            logger.debug(f"Response: {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 get_objects(self, aws_client: Optional[AwsApiClient] = None, prefix: Optional[str] = None) -> List[Any]:
        """Returns a list of s3.Object objects for the s3.Bucket

        Args:
            aws_client: The AwsApiClient for the current cluster
            prefix: Prefix to filter objects by
        """
        bucket = self.get_resource(aws_client)
        if bucket is None:
            logger.warning(f"Could not get bucket: {self.name}")
            return []

        logger.debug(f"Getting objects for bucket: {bucket.name}")
        # Get all objects in bucket
        object_summaries = bucket.objects.all()
        all_objects: List[S3Object] = []
        for object_summary in object_summaries:
            if prefix is not None and not object_summary.key.startswith(prefix):
                continue
            all_objects.append(
                S3Object(
                    bucket_name=bucket.name,
                    name=object_summary.key,
                )
            )
        return all_objects
