"""
Abstract port interface for S3 object storage operations.

Defines the provider-agnostic contract that all S3 adapters must implement.
"""

from abc import ABC, abstractmethod
from typing import Any, BinaryIO, Dict, Generator, List, Optional


class S3Port(ABC):
    """
    Abstract interface for S3 storage providers.

    All S3 adapters must implement this interface.
    """

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

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

        Args:
            bucket: Bucket name
            key: Object key
            body: Object content as bytes
            **kwargs: Extra params (ContentType, Metadata, etc.)

        Returns:
            Response metadata dict
        """
        ...

    @abstractmethod
    async def put_object_async(self, bucket: str, key: str, body: bytes, **kwargs) -> Dict[str, Any]:
        ...

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

        Args:
            file_path: Local file path
            bucket: Bucket name
            key: Object key
        """
        ...

    @abstractmethod
    async def upload_file_async(self, file_path: str, bucket: str, key: str, **kwargs) -> None:
        ...

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

        Args:
            stream: File-like object with read()
            bucket: Bucket name
            key: Object key
        """
        ...

    @abstractmethod
    async def upload_stream_async(self, stream: BinaryIO, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        ...

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

    @abstractmethod
    def get_object(self, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        """
        Get an object. Returns dict with 'Body' (StreamingBody), metadata, etc.

        Args:
            bucket: Bucket name
            key: Object key

        Returns:
            Response dict including 'Body', 'ContentType', 'ContentLength', etc.
        """
        ...

    @abstractmethod
    async def get_object_async(self, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            key: Object key
            file_path: Local destination path
        """
        ...

    @abstractmethod
    async def download_file_async(self, bucket: str, key: str, file_path: str, **kwargs) -> None:
        ...

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

        Args:
            bucket: Bucket name
            key: Object key

        Returns:
            File-like streaming object
        """
        ...

    @abstractmethod
    async def download_stream_async(self, bucket: str, key: str, **kwargs) -> BinaryIO:
        ...

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

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

        Args:
            bucket: Bucket name
            key: Object key

        Returns:
            Object metadata dict (ContentLength, ContentType, LastModified, etc.)
        """
        ...

    @abstractmethod
    async def head_object_async(self, bucket: str, key: str) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            key: Object key

        Returns:
            User-defined metadata dict
        """
        ...

    @abstractmethod
    async def get_object_metadata_async(self, bucket: str, key: str) -> Dict[str, str]:
        ...

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

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

        Args:
            bucket: Bucket name
            key: Object key

        Returns:
            Response metadata dict
        """
        ...

    @abstractmethod
    async def delete_object_async(self, bucket: str, key: str) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            keys: List of object keys to delete

        Returns:
            Response dict with 'Deleted' and 'Errors' lists
        """
        ...

    @abstractmethod
    async def delete_objects_async(self, bucket: str, keys: List[str]) -> Dict[str, Any]:
        ...

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

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

        Args:
            bucket: Bucket name
            prefix: Key prefix filter

        Returns:
            Response dict with 'Contents' list
        """
        ...

    @abstractmethod
    async def list_objects_async(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            prefix: Key prefix filter
            continuation_token: Token from previous response for pagination

        Returns:
            Response dict with 'Contents', 'NextContinuationToken', 'IsTruncated'
        """
        ...

    @abstractmethod
    async def list_objects_v2_async(
        self, bucket: str, prefix: str = "", continuation_token: Optional[str] = None, **kwargs
    ) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            prefix: Key prefix filter

        Returns:
            Response dict with 'Versions' and 'DeleteMarkers'
        """
        ...

    @abstractmethod
    async def list_object_versions_async(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name

        Returns:
            Response dict with 'Uploads' list
        """
        ...

    @abstractmethod
    async def list_multipart_uploads_async(self, bucket: str, **kwargs) -> Dict[str, Any]:
        ...

    @abstractmethod
    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.

        Args:
            bucket: Bucket name
            prefix: Key prefix to start from

        Yields:
            Object metadata dicts (Key, Size, LastModified, etc.)
        """
        ...

    @abstractmethod
    async def walk_async(self, bucket: str, prefix: str = ""):
        """Async version of walk. Returns an async generator."""
        ...

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

    @abstractmethod
    def create_multipart_upload(self, bucket: str, key: str, **kwargs) -> str:
        """
        Initiate a multipart upload.

        Args:
            bucket: Bucket name
            key: Object key

        Returns:
            Upload ID string
        """
        ...

    @abstractmethod
    async def create_multipart_upload_async(self, bucket: str, key: str, **kwargs) -> str:
        ...

    @abstractmethod
    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.

        Args:
            bucket: Bucket name
            key: Object key
            part_number: Part number (1-10000)
            upload_id: Upload ID from create_multipart_upload
            body: Part content as bytes

        Returns:
            Dict with 'ETag' for the uploaded part
        """
        ...

    @abstractmethod
    async def upload_part_async(
        self, bucket: str, key: str, part_number: int, upload_id: str, body: bytes
    ) -> Dict[str, Any]:
        ...

    @abstractmethod
    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 previously uploaded parts.

        Args:
            bucket: Bucket name
            key: Object key
            upload_id: Upload ID
            parts: List of dicts with 'PartNumber' and 'ETag'

        Returns:
            Response metadata dict
        """
        ...

    @abstractmethod
    async def complete_multipart_upload_async(
        self, bucket: str, key: str, upload_id: str, parts: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            key: Object key
            upload_id: Upload ID

        Returns:
            Response metadata dict
        """
        ...

    @abstractmethod
    async def abort_multipart_upload_async(self, bucket: str, key: str, upload_id: str) -> Dict[str, Any]:
        ...

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

        Args:
            bucket: Bucket name
            key: Object key
            upload_id: Upload ID

        Returns:
            Response dict with 'Parts' list
        """
        ...

    @abstractmethod
    async def list_parts_async(self, bucket: str, key: str, upload_id: str, **kwargs) -> Dict[str, Any]:
        ...

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

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

        Args:
            bucket: Bucket name
            key: Object key
            operation: S3 client method name (e.g., "get_object", "put_object")
            expiration: URL expiration time in seconds (default 3600 = 1 hour)
            **kwargs: Additional parameters for the S3 operation

        Returns:
            Presigned URL string
        """
        ...

    @abstractmethod
    async def generate_presigned_url_async(
        self,
        bucket: str,
        key: str,
        operation: str = "get_object",
        expiration: int = 3600,
        **kwargs,
    ) -> str:
        ...

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

        Optionally sets Content-Disposition to force a download with a custom filename.

        Args:
            bucket: Bucket name
            key: Object key
            expiration: URL expiration time in seconds
            filename: If provided, sets Content-Disposition header for browser download

        Returns:
            Presigned download URL string
        """
        ...

    @abstractmethod
    async def generate_presigned_download_url_async(
        self, bucket: str, key: str, expiration: int = 3600, filename: Optional[str] = None
    ) -> str:
        ...

    @abstractmethod
    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 time in seconds
            content_type: Expected Content-Type of the upload

        Returns:
            Presigned upload URL string
        """
        ...

    @abstractmethod
    async def generate_presigned_upload_url_async(
        self,
        bucket: str,
        key: str,
        expiration: int = 3600,
        content_type: Optional[str] = None,
    ) -> str:
        ...
