import boto3
from botocore.exceptions import ClientError
from typing import Optional, Dict, List
from .base import BaseStorageProvider
from .exceptions import StorageUploadError, StorageDownloadError, StorageNotFoundError
from urllib.parse import urlparse
import base64
import time
class AWSStorage(BaseStorageProvider):
    """AWS S3 storage implementation"""

    def _initialize_client(self):
        """Initialize AWS S3 client"""
        aws_config = {
            'region_name': self.config.get('region', 'us-east-1')
        }

        aws_access_key_id = self.config.get("aws_access_key_id")
        aws_secret_access_key = self.config.get("aws_secret_access_key")
        region = self.config.get("region", "us-east-1")

        # validation
        if not aws_access_key_id:
            raise ValueError("aws_access_key_id not found in AWS storage config")

        if not aws_secret_access_key:
            raise ValueError("aws_secret_access_key not found in AWS storage config")

        self.client = boto3.client(
            "s3",
            region_name=region,
            aws_access_key_id=aws_access_key_id,
            aws_secret_access_key=aws_secret_access_key
        )

        self.region = region

    def _normalize_key12(self, remote_path: str) -> str:
        """
        Normalize path to S3 object key
        Supports:
        - key
        - bucket/key
        - full URL
        """

        # If full URL
        if remote_path.startswith("http://") or remote_path.startswith("https://"):
            parsed = urlparse(remote_path)
            key = parsed.path.lstrip("/")
        else:
            key = remote_path

        # Remove bucket prefix if present
        if key.startswith(self.bucket_name + "/"):
            key = key[len(self.bucket_name) + 1 :]

        return key

    def _normalize_key(self, remote_path: str) -> str:
        """
        Normalize path to S3 object key
        """

        key = remote_path

        # If URL
        if remote_path.startswith(("http://", "https://")):
            parsed = urlparse(remote_path)

            # If this is our bucket domain → remove it
            if self.bucket_name in parsed.netloc:
                key = parsed.path.lstrip("/")
            else:
                # keep domain + path
                key = f"{parsed.netloc}{parsed.path}"

        # Remove bucket prefix if present
        if key.startswith(f"{self.bucket_name}/"):
            key = key[len(self.bucket_name) + 1:]

        if key.startswith(f"{self.bucket_name}/"):
            key = key[len(self.bucket_name) + 1 :]

        if 'hosted_domain' in self.config:
            if self.config['hosted_domain'] !="":
                if key.startswith(f"{self.config['hosted_domain']}/"):
                    key = key[len(self.config["hosted_domain"]) + 1 :]

        return key

    def copy(self, source_path: str, destination_path: str) -> str:
        """Copy object inside S3 bucket"""
        try:
            source_key = self._normalize_key(source_path)
            destination_key = self._normalize_key(destination_path)

            copy_source = {
                "Bucket": self.bucket_name,
                "Key": source_key
            }

            self.client.copy_object(
                Bucket=self.bucket_name,
                CopySource=copy_source,
                Key=destination_key,
                MetadataDirective="COPY"
            )

            return self.get_public_url(destination_key)

        except ClientError as e:
            raise StorageUploadError(f"Failed to copy object in S3: {str(e)}")
    def get_size(self, remote_path: str) -> int:
        """Get size of S3 object in bytes"""
        try:
            key = self._normalize_key(remote_path)

            response = self.client.head_object(Bucket=self.bucket_name, Key=key)

            return response["ContentLength"]

        except ClientError as e:
            if e.response["Error"]["Code"] == "404":
                raise Exception(f"Object not found: {remote_path}")
            raise Exception(f"Failed to get S3 object size: {str(e)}")

    import base64

    def download_as_string(self, remote_path: str):
        """Download S3 object and return string (text or base64 for binary)"""
        try:
            key = self._normalize_key(remote_path)

            response = self.client.get_object(
                Bucket=self.bucket_name,
                Key=key,
            )

            content_type = response.get("ContentType", "")
            data = response["Body"].read()

            # text files
            if (
                "text" in content_type
                or "json" in content_type
                or "xml" in content_type
                or "javascript" in content_type
                or "svg" in content_type
            ):
                return data.decode("utf-8"), content_type

            # binary files -> convert to base64 string
            encoded = base64.b64encode(data).decode("utf-8")
            base64_data = f"data:{content_type};base64,{encoded}"

            return base64_data, content_type

        except Exception as e:
            raise Exception(f"Failed to download object: {e}")

    def download_as_bytes(self, remote_path: str) -> bytes:
        key = self._normalize_key(remote_path)

        response = self.client.get_object(
            Bucket=self.bucket_name,
            Key=key,
        )

        return response["Body"].read()

    def exists(self, remote_path: str) -> bool:
        """Check if S3 object exists"""
        try:
            key = self._normalize_key(remote_path)

            self.client.head_object(
                Bucket=self.bucket_name,
                Key=key,
            )

            return True

        except self.client.exceptions.NoSuchKey:
            return False
        except Exception as e:
            if "404" in str(e):
                return False
            raise Exception(f"Failed to check object existence: {e}")

    def get_content_type(self, remote_path: str) -> str:
        """Get content type of S3 object"""
        try:
            key = self._normalize_key(remote_path)

            response = self.client.head_object(
                Bucket=self.bucket_name,
                Key=key,
            )

            return response.get("ContentType")

        except Exception as e:
            raise Exception(f"Failed to get content type: {e}")

    def upload(self, data: bytes, remote_path: str,
               content_type: str = "application/octet-stream",
               metadata: Optional[Dict[str, str]] = None,
               cache_control: Optional[str] = None) -> str:
        """Upload data to S3"""
        try:
            extra_args = {
                'ContentType': content_type,
            }

            if metadata:
                extra_args['Metadata'] = metadata
            if cache_control:
                extra_args['CacheControl'] = cache_control

            self.client.put_object(
                Bucket=self.bucket_name,
                Key=remote_path,
                Body=data,
                **extra_args
            )
            try:
                if 'distribution_id' in self.config:
                    if  self.config["distribution_id"]:
                        cf = boto3.client(
                            "cloudfront",
                            aws_access_key_id=self.config.get("aws_access_key_id"),
                            aws_secret_access_key=self.config.get("aws_secret_access_key"),
                            region_name=self.config.get("region", "us-east-1"),
                        )
                        cf.create_invalidation(
                            DistributionId=self.config.get('distribution_id', ''),
                            InvalidationBatch={
                                "Paths": {"Quantity": 1, "Items": [f"/{remote_path}"]},
                                "CallerReference": str(time.time()),
                            },
                        )
            except Exception as e:
                print(e)


            return self.get_public_url(remote_path)

        except ClientError as e:
            raise StorageUploadError(f"Failed to upload to S3: {str(e)}")

    def download(self, remote_path: str) -> bytes:
        """Download data from S3"""
        try:
            response = self.client.get_object(
                Bucket=self.bucket_name,
                Key=remote_path
            )
            return response['Body'].read()

        except ClientError as e:
            if e.response['Error']['Code'] == 'NoSuchKey':
                raise StorageNotFoundError(f"Object not found: {remote_path}")
            raise StorageDownloadError(f"Failed to download from S3: {str(e)}")

    def delete(self, remote_path: str) -> bool:
        """Delete object from S3"""
        try:
            self.client.delete_object(
                Bucket=self.bucket_name,
                Key=remote_path
            )
            return True
        except ClientError as e:
            raise StorageUploadError(f"Failed to delete from S3: {str(e)}")

    def list_objects(self, prefix: str = "", max_results: int = 1000) -> List[str]:
        """List objects in S3 bucket"""
        try:
            response = self.client.list_objects_v2(
                Bucket=self.bucket_name,
                Prefix=prefix,
                MaxKeys=max_results
            )

            if 'Contents' not in response:
                return []

            return [obj['Key'] for obj in response['Contents']]

        except ClientError as e:
            raise StorageUploadError(f"Failed to list S3 objects: {str(e)}")

    def exists(self, remote_path: str) -> bool:
        """Check if object exists in S3"""
        try:
            key = self._normalize_key(remote_path)
            self.client.head_object(Bucket=self.bucket_name, Key=key)
            return True
        except ClientError:
            return False

    def get_public_url(self, remote_path: str) -> str:
        """Get public URL for S3 object"""
        return f"https://{self.bucket_name}.s3.{self.region}.amazonaws.com/{remote_path}"

    def get_signed_url(self, remote_path: str, expiration: int = 3600) -> str:
        """Generate presigned URL for S3 object"""
        try:
            url = self.client.generate_presigned_url(
                'get_object',
                Params={
                    'Bucket': self.bucket_name,
                    'Key': remote_path
                },
                ExpiresIn=expiration
            )
            return url
        except ClientError as e:
            raise StorageUploadError(f"Failed to generate presigned URL: {str(e)}")

    def rename(self, source_path: str, destination_path: str) -> str:
        """Rename object in S3 (copy + delete)"""
        try:
            source_key = self._normalize_key(source_path)
            destination_key = self._normalize_key(destination_path)

            copy_source = {
                "Bucket": self.bucket_name,
                "Key": source_key
            }

            self.client.copy_object(
                Bucket=self.bucket_name,
                CopySource=copy_source,
                Key=destination_key,
                MetadataDirective="COPY"
            )

            self.client.delete_object(
                Bucket=self.bucket_name,
                Key=source_key
            )

            return self.get_public_url(destination_key)

        except ClientError as e:
            raise Exception(f"Failed to rename S3 object: {str(e)}")
