"""
S3Manager - Public API for S3 object storage operations.

This is the main entrypoint that users import and use for all S3 operations.
"""

from typing import Any, BinaryIO, Dict, Generator, List, Optional, Union

from .config.s3_config import S3Config
from .ports.s3_port import S3Port
from .adapters.aws_adapter import AWSAdapter
from .exceptions.s3_exceptions import S3ManagerError, ConfigurationError


class S3Manager:
    """
    Public API for S3 object storage operations.

    Example:
        s3 = S3Manager({"region": "us-east-1"})
        s3.put_object("my-bucket", "hello.txt", b"Hello World")
        s3.download_file("my-bucket", "hello.txt", "/tmp/hello.txt")

        # Multipart upload
        upload_id = s3.create_multipart_upload("my-bucket", "large-file.bin")
        part = s3.upload_part("my-bucket", "large-file.bin", 1, upload_id, chunk)
        s3.complete_multipart_upload("my-bucket", "large-file.bin", upload_id, [part])
    """

    _ADAPTERS = {
        "aws": AWSAdapter,
    }

    def __init__(self, config: Optional[Union[S3Config, Dict[str, Any]]] = None):
        if config is None:
            config = S3Config()
        elif isinstance(config, dict):
            try:
                config = S3Config(**config)
            except Exception as e:
                raise ConfigurationError(f"Invalid configuration: {e}")

        self.config = config
        self._adapter: S3Port = self._create_adapter(config)

    def _create_adapter(self, config: S3Config) -> S3Port:
        adapter_cls = self._ADAPTERS.get(config.provider)
        if not adapter_cls:
            raise ConfigurationError(
                f"Unsupported provider: {config.provider}. "
                f"Supported: {', '.join(self._ADAPTERS.keys())}"
            )
        return adapter_cls(config)

    # ===== Object CRUD - Upload =====

    def put_object(self, bucket: str, key: str, body: bytes, **kwargs) -> Dict[str, Any]:
        """Upload an object from bytes."""
        return self._adapter.put_object(bucket, key, body, **kwargs)

    async def put_object_async(self, bucket: str, key: str, body: bytes, **kwargs) -> Dict[str, Any]:
        """Upload an object from bytes (async)."""
        return await self._adapter.put_object_async(bucket, key, body, **kwargs)

    def upload_file(self, file_path: str, bucket: str, key: str, **kwargs) -> None:
        """Upload a local file to S3."""
        self._adapter.upload_file(file_path, bucket, key, **kwargs)

    async def upload_file_async(self, file_path: str, bucket: str, key: str, **kwargs) -> None:
        """Upload a local file to S3 (async)."""
        await self._adapter.upload_file_async(file_path, bucket, key, **kwargs)

    def upload_stream(self, stream: BinaryIO, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        """Upload from a file-like stream."""
        return self._adapter.upload_stream(stream, bucket, key, **kwargs)

    async def upload_stream_async(self, stream: BinaryIO, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        """Upload from a file-like stream (async)."""
        return await self._adapter.upload_stream_async(stream, bucket, key, **kwargs)

    # ===== Object CRUD - Download =====

    def get_object(self, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        """Get an object with body and metadata."""
        return self._adapter.get_object(bucket, key, **kwargs)

    async def get_object_async(self, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        """Get an object with body and metadata (async)."""
        return await self._adapter.get_object_async(bucket, key, **kwargs)

    def download_file(self, bucket: str, key: str, file_path: str, **kwargs) -> None:
        """Download an object to a local file."""
        self._adapter.download_file(bucket, key, file_path, **kwargs)

    async def download_file_async(self, bucket: str, key: str, file_path: str, **kwargs) -> None:
        """Download an object to a local file (async)."""
        await self._adapter.download_file_async(bucket, key, file_path, **kwargs)

    def download_stream(self, bucket: str, key: str, **kwargs) -> BinaryIO:
        """Download an object as a streaming body."""
        return self._adapter.download_stream(bucket, key, **kwargs)

    async def download_stream_async(self, bucket: str, key: str, **kwargs) -> BinaryIO:
        """Download an object as a streaming body (async)."""
        return await self._adapter.download_stream_async(bucket, key, **kwargs)

    # ===== Object CRUD - Metadata =====

    def head_object(self, bucket: str, key: str) -> Dict[str, Any]:
        """Get object metadata without downloading the body."""
        return self._adapter.head_object(bucket, key)

    async def head_object_async(self, bucket: str, key: str) -> Dict[str, Any]:
        """Get object metadata without downloading the body (async)."""
        return await self._adapter.head_object_async(bucket, key)

    def get_object_metadata(self, bucket: str, key: str) -> Dict[str, str]:
        """Get only the user-defined metadata of an object."""
        return self._adapter.get_object_metadata(bucket, key)

    async def get_object_metadata_async(self, bucket: str, key: str) -> Dict[str, str]:
        """Get only the user-defined metadata of an object (async)."""
        return await self._adapter.get_object_metadata_async(bucket, key)

    # ===== Object CRUD - Delete =====

    def delete_object(self, bucket: str, key: str) -> Dict[str, Any]:
        """Delete a single object."""
        return self._adapter.delete_object(bucket, key)

    async def delete_object_async(self, bucket: str, key: str) -> Dict[str, Any]:
        """Delete a single object (async)."""
        return await self._adapter.delete_object_async(bucket, key)

    def delete_objects(self, bucket: str, keys: List[str]) -> Dict[str, Any]:
        """Delete multiple objects in a single request."""
        return self._adapter.delete_objects(bucket, keys)

    async def delete_objects_async(self, bucket: str, keys: List[str]) -> Dict[str, Any]:
        """Delete multiple objects in a single request (async)."""
        return await self._adapter.delete_objects_async(bucket, keys)

    # ===== Listing & Discovery =====

    def list_objects(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        """List objects in a bucket with optional prefix."""
        return self._adapter.list_objects(bucket, prefix, **kwargs)

    async def list_objects_async(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        """List objects in a bucket with optional prefix (async)."""
        return await self._adapter.list_objects_async(bucket, prefix, **kwargs)

    def list_objects_v2(
        self, bucket: str, prefix: str = "", continuation_token: Optional[str] = None, **kwargs
    ) -> Dict[str, Any]:
        """List objects using v2 API with pagination."""
        return self._adapter.list_objects_v2(bucket, prefix, continuation_token, **kwargs)

    async def list_objects_v2_async(
        self, bucket: str, prefix: str = "", continuation_token: Optional[str] = None, **kwargs
    ) -> Dict[str, Any]:
        """List objects using v2 API with pagination (async)."""
        return await self._adapter.list_objects_v2_async(bucket, prefix, continuation_token, **kwargs)

    def list_object_versions(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        """List object versions in a versioned bucket."""
        return self._adapter.list_object_versions(bucket, prefix, **kwargs)

    async def list_object_versions_async(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        """List object versions in a versioned bucket (async)."""
        return await self._adapter.list_object_versions_async(bucket, prefix, **kwargs)

    def list_multipart_uploads(self, bucket: str, **kwargs) -> Dict[str, Any]:
        """List in-progress multipart uploads."""
        return self._adapter.list_multipart_uploads(bucket, **kwargs)

    async def list_multipart_uploads_async(self, bucket: str, **kwargs) -> Dict[str, Any]:
        """List in-progress multipart uploads (async)."""
        return await self._adapter.list_multipart_uploads_async(bucket, **kwargs)

    def walk(self, bucket: str, prefix: str = "") -> Generator[Dict[str, Any], None, None]:
        """Recursively list all objects under a prefix, like os.walk for S3."""
        return self._adapter.walk(bucket, prefix)

    async def walk_async(self, bucket: str, prefix: str = ""):
        """Recursively list all objects under a prefix (async)."""
        return await self._adapter.walk_async(bucket, prefix)

    # ===== Multipart Upload =====

    def create_multipart_upload(self, bucket: str, key: str, **kwargs) -> str:
        """Initiate a multipart upload. Returns upload ID."""
        return self._adapter.create_multipart_upload(bucket, key, **kwargs)

    async def create_multipart_upload_async(self, bucket: str, key: str, **kwargs) -> str:
        """Initiate a multipart upload (async). Returns upload ID."""
        return await self._adapter.create_multipart_upload_async(bucket, key, **kwargs)

    def upload_part(
        self, bucket: str, key: str, part_number: int, upload_id: str, body: bytes
    ) -> Dict[str, Any]:
        """Upload a single part of a multipart upload."""
        return self._adapter.upload_part(bucket, key, part_number, upload_id, body)

    async def upload_part_async(
        self, bucket: str, key: str, part_number: int, upload_id: str, body: bytes
    ) -> Dict[str, Any]:
        """Upload a single part of a multipart upload (async)."""
        return await self._adapter.upload_part_async(bucket, key, part_number, upload_id, body)

    def complete_multipart_upload(
        self, bucket: str, key: str, upload_id: str, parts: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Complete a multipart upload by assembling uploaded parts."""
        return self._adapter.complete_multipart_upload(bucket, key, upload_id, parts)

    async def complete_multipart_upload_async(
        self, bucket: str, key: str, upload_id: str, parts: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """Complete a multipart upload by assembling uploaded parts (async)."""
        return await self._adapter.complete_multipart_upload_async(bucket, key, upload_id, parts)

    def abort_multipart_upload(self, bucket: str, key: str, upload_id: str) -> Dict[str, Any]:
        """Abort a multipart upload and discard uploaded parts."""
        return self._adapter.abort_multipart_upload(bucket, key, upload_id)

    async def abort_multipart_upload_async(self, bucket: str, key: str, upload_id: str) -> Dict[str, Any]:
        """Abort a multipart upload and discard uploaded parts (async)."""
        return await self._adapter.abort_multipart_upload_async(bucket, key, upload_id)

    def list_parts(self, bucket: str, key: str, upload_id: str, **kwargs) -> Dict[str, Any]:
        """List uploaded parts for a multipart upload."""
        return self._adapter.list_parts(bucket, key, upload_id, **kwargs)

    async def list_parts_async(self, bucket: str, key: str, upload_id: str, **kwargs) -> Dict[str, Any]:
        """List uploaded parts for a multipart upload (async)."""
        return await self._adapter.list_parts_async(bucket, key, upload_id, **kwargs)

    # ===== Presigned URLs =====

    def generate_presigned_url(
        self,
        bucket: str,
        key: str,
        operation: str = "get_object",
        expiration: int = 3600,
        **kwargs,
    ) -> str:
        """
        Generate a presigned URL for any S3 operation.

        Args:
            bucket: Bucket name
            key: Object key
            operation: S3 client method name ("get_object", "put_object", etc.)
            expiration: URL expiration in seconds (default 1 hour)

        Returns:
            Presigned URL string
        """
        return self._adapter.generate_presigned_url(bucket, key, operation, expiration, **kwargs)

    async def generate_presigned_url_async(
        self,
        bucket: str,
        key: str,
        operation: str = "get_object",
        expiration: int = 3600,
        **kwargs,
    ) -> str:
        """Generate a presigned URL for any S3 operation (async)."""
        return await self._adapter.generate_presigned_url_async(
            bucket, key, operation, expiration, **kwargs
        )

    def generate_presigned_download_url(
        self, bucket: str, key: str, expiration: int = 3600, filename: Optional[str] = None
    ) -> str:
        """
        Generate a presigned URL for downloading an object.

        Args:
            bucket: Bucket name
            key: Object key
            expiration: URL expiration in seconds
            filename: Optional filename for Content-Disposition header

        Returns:
            Presigned download URL
        """
        return self._adapter.generate_presigned_download_url(bucket, key, expiration, filename)

    async def generate_presigned_download_url_async(
        self, bucket: str, key: str, expiration: int = 3600, filename: Optional[str] = None
    ) -> str:
        """Generate a presigned URL for downloading an object (async)."""
        return await self._adapter.generate_presigned_download_url_async(
            bucket, key, expiration, filename
        )

    def generate_presigned_upload_url(
        self,
        bucket: str,
        key: str,
        expiration: int = 3600,
        content_type: Optional[str] = None,
    ) -> str:
        """
        Generate a presigned URL for uploading an object via HTTP PUT.

        Args:
            bucket: Bucket name
            key: Object key
            expiration: URL expiration in seconds
            content_type: Expected Content-Type of the upload

        Returns:
            Presigned upload URL
        """
        return self._adapter.generate_presigned_upload_url(bucket, key, expiration, content_type)

    async def generate_presigned_upload_url_async(
        self,
        bucket: str,
        key: str,
        expiration: int = 3600,
        content_type: Optional[str] = None,
    ) -> str:
        """Generate a presigned URL for uploading an object via HTTP PUT (async)."""
        return await self._adapter.generate_presigned_upload_url_async(
            bucket, key, expiration, content_type
        )
