"""
AWS S3 adapter.

Implements S3Port using boto3 for all S3 operations.
"""

import asyncio
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from typing import Any, BinaryIO, Dict, Generator, List, Optional

import boto3
from botocore.config import Config as BotoConfig

from ..config.s3_config import S3Config
from ..ports.s3_port import S3Port
from ..exceptions.s3_exceptions import (
    ConnectionError,
    UploadError,
    DownloadError,
    DeleteError,
    ListError,
    MultipartError,
)


class AWSAdapter(S3Port):
    """
    AWS S3 adapter using boto3.

    Wraps boto3 S3 client and provides both sync and async operations.
    Async operations are delegated to a thread pool since boto3 is synchronous.
    """

    def __init__(self, config: S3Config):
        self._config = config
        self._executor = ThreadPoolExecutor(max_workers=config.max_pool_connections)
        try:
            boto_config = BotoConfig(
                max_pool_connections=config.max_pool_connections,
                connect_timeout=config.connect_timeout,
                read_timeout=config.read_timeout,
            )
            client_kwargs: Dict[str, Any] = {
                "service_name": "s3",
                "region_name": config.region,
                "config": boto_config,
                "use_ssl": config.use_ssl,
                "verify": config.verify_ssl,
            }
            if config.access_key_id and config.secret_access_key:
                client_kwargs["aws_access_key_id"] = config.access_key_id
                client_kwargs["aws_secret_access_key"] = config.secret_access_key
            if config.session_token:
                client_kwargs["aws_session_token"] = config.session_token
            if config.endpoint_url:
                client_kwargs["endpoint_url"] = config.endpoint_url

            self._client = boto3.client(**client_kwargs)
        except Exception as e:
            raise ConnectionError(f"Failed to create S3 client: {e}")

    async def _run_sync(self, func, *args, **kwargs):
        """Run a synchronous boto3 call in the thread pool."""
        loop = asyncio.get_running_loop()
        return await loop.run_in_executor(self._executor, partial(func, *args, **kwargs))

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

    def put_object(self, bucket: str, key: str, body: bytes, **kwargs) -> Dict[str, Any]:
        try:
            return self._client.put_object(Bucket=bucket, Key=key, Body=body, **kwargs)
        except Exception as e:
            raise UploadError(f"put_object failed for s3://{bucket}/{key}: {e}")

    async def put_object_async(self, bucket: str, key: str, body: bytes, **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.put_object, bucket, key, body, **kwargs)

    def upload_file(self, file_path: str, bucket: str, key: str, **kwargs) -> None:
        try:
            self._client.upload_file(Filename=file_path, Bucket=bucket, Key=key, ExtraArgs=kwargs or None)
        except Exception as e:
            raise UploadError(f"upload_file failed for {file_path} -> s3://{bucket}/{key}: {e}")

    async def upload_file_async(self, file_path: str, bucket: str, key: str, **kwargs) -> None:
        await self._run_sync(self.upload_file, file_path, bucket, key, **kwargs)

    def upload_stream(self, stream: BinaryIO, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        try:
            return self._client.put_object(Bucket=bucket, Key=key, Body=stream, **kwargs)
        except Exception as e:
            raise UploadError(f"upload_stream failed for s3://{bucket}/{key}: {e}")

    async def upload_stream_async(self, stream: BinaryIO, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.upload_stream, stream, bucket, key, **kwargs)

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

    def get_object(self, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        try:
            return self._client.get_object(Bucket=bucket, Key=key, **kwargs)
        except Exception as e:
            raise DownloadError(f"get_object failed for s3://{bucket}/{key}: {e}")

    async def get_object_async(self, bucket: str, key: str, **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.get_object, bucket, key, **kwargs)

    def download_file(self, bucket: str, key: str, file_path: str, **kwargs) -> None:
        try:
            self._client.download_file(Bucket=bucket, Key=key, Filename=file_path, ExtraArgs=kwargs or None)
        except Exception as e:
            raise DownloadError(f"download_file failed for s3://{bucket}/{key} -> {file_path}: {e}")

    async def download_file_async(self, bucket: str, key: str, file_path: str, **kwargs) -> None:
        await self._run_sync(self.download_file, bucket, key, file_path, **kwargs)

    def download_stream(self, bucket: str, key: str, **kwargs) -> BinaryIO:
        try:
            response = self._client.get_object(Bucket=bucket, Key=key, **kwargs)
            return response["Body"]
        except Exception as e:
            raise DownloadError(f"download_stream failed for s3://{bucket}/{key}: {e}")

    async def download_stream_async(self, bucket: str, key: str, **kwargs) -> BinaryIO:
        return await self._run_sync(self.download_stream, bucket, key, **kwargs)

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

    def head_object(self, bucket: str, key: str) -> Dict[str, Any]:
        try:
            return self._client.head_object(Bucket=bucket, Key=key)
        except Exception as e:
            raise DownloadError(f"head_object failed for s3://{bucket}/{key}: {e}")

    async def head_object_async(self, bucket: str, key: str) -> Dict[str, Any]:
        return await self._run_sync(self.head_object, bucket, key)

    def get_object_metadata(self, bucket: str, key: str) -> Dict[str, str]:
        try:
            response = self._client.head_object(Bucket=bucket, Key=key)
            return response.get("Metadata", {})
        except Exception as e:
            raise DownloadError(f"get_object_metadata failed for s3://{bucket}/{key}: {e}")

    async def get_object_metadata_async(self, bucket: str, key: str) -> Dict[str, str]:
        return await self._run_sync(self.get_object_metadata, bucket, key)

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

    def delete_object(self, bucket: str, key: str) -> Dict[str, Any]:
        try:
            return self._client.delete_object(Bucket=bucket, Key=key)
        except Exception as e:
            raise DeleteError(f"delete_object failed for s3://{bucket}/{key}: {e}")

    async def delete_object_async(self, bucket: str, key: str) -> Dict[str, Any]:
        return await self._run_sync(self.delete_object, bucket, key)

    def delete_objects(self, bucket: str, keys: List[str]) -> Dict[str, Any]:
        try:
            objects = [{"Key": k} for k in keys]
            return self._client.delete_objects(
                Bucket=bucket, Delete={"Objects": objects, "Quiet": False}
            )
        except Exception as e:
            raise DeleteError(f"delete_objects failed for {len(keys)} objects in {bucket}: {e}")

    async def delete_objects_async(self, bucket: str, keys: List[str]) -> Dict[str, Any]:
        return await self._run_sync(self.delete_objects, bucket, keys)

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

    def list_objects(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        try:
            params = {"Bucket": bucket, **kwargs}
            if prefix:
                params["Prefix"] = prefix
            return self._client.list_objects(**params)
        except Exception as e:
            raise ListError(f"list_objects failed for s3://{bucket}/{prefix}: {e}")

    async def list_objects_async(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.list_objects, bucket, prefix, **kwargs)

    def list_objects_v2(
        self, bucket: str, prefix: str = "", continuation_token: Optional[str] = None, **kwargs
    ) -> Dict[str, Any]:
        try:
            params: Dict[str, Any] = {"Bucket": bucket, **kwargs}
            if prefix:
                params["Prefix"] = prefix
            if continuation_token:
                params["ContinuationToken"] = continuation_token
            return self._client.list_objects_v2(**params)
        except Exception as e:
            raise ListError(f"list_objects_v2 failed for s3://{bucket}/{prefix}: {e}")

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

    def list_object_versions(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        try:
            params: Dict[str, Any] = {"Bucket": bucket, **kwargs}
            if prefix:
                params["Prefix"] = prefix
            return self._client.list_object_versions(**params)
        except Exception as e:
            raise ListError(f"list_object_versions failed for s3://{bucket}/{prefix}: {e}")

    async def list_object_versions_async(self, bucket: str, prefix: str = "", **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.list_object_versions, bucket, prefix, **kwargs)

    def list_multipart_uploads(self, bucket: str, **kwargs) -> Dict[str, Any]:
        try:
            return self._client.list_multipart_uploads(Bucket=bucket, **kwargs)
        except Exception as e:
            raise ListError(f"list_multipart_uploads failed for {bucket}: {e}")

    async def list_multipart_uploads_async(self, bucket: str, **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.list_multipart_uploads, bucket, **kwargs)

    def walk(self, bucket: str, prefix: str = "") -> Generator[Dict[str, Any], None, None]:
        paginator = self._client.get_paginator("list_objects_v2")
        params = {"Bucket": bucket}
        if prefix:
            params["Prefix"] = prefix
        try:
            for page in paginator.paginate(**params):
                for obj in page.get("Contents", []):
                    yield obj
        except Exception as e:
            raise ListError(f"walk failed for s3://{bucket}/{prefix}: {e}")

    async def walk_async(self, bucket: str, prefix: str = ""):
        """Async walk using thread pool. Returns a list (not async generator) for simplicity."""
        return await self._run_sync(self._walk_collect, bucket, prefix)

    def _walk_collect(self, bucket: str, prefix: str = "") -> List[Dict[str, Any]]:
        """Collect all walk results into a list (used by walk_async)."""
        return list(self.walk(bucket, prefix))

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

    def create_multipart_upload(self, bucket: str, key: str, **kwargs) -> str:
        try:
            response = self._client.create_multipart_upload(Bucket=bucket, Key=key, **kwargs)
            return response["UploadId"]
        except Exception as e:
            raise MultipartError(f"create_multipart_upload failed for s3://{bucket}/{key}: {e}")

    async def create_multipart_upload_async(self, bucket: str, key: str, **kwargs) -> str:
        return await self._run_sync(self.create_multipart_upload, bucket, key, **kwargs)

    def upload_part(
        self, bucket: str, key: str, part_number: int, upload_id: str, body: bytes
    ) -> Dict[str, Any]:
        try:
            response = self._client.upload_part(
                Bucket=bucket, Key=key, PartNumber=part_number, UploadId=upload_id, Body=body
            )
            return {"ETag": response["ETag"], "PartNumber": part_number}
        except Exception as e:
            raise MultipartError(
                f"upload_part #{part_number} failed for s3://{bucket}/{key} (upload {upload_id}): {e}"
            )

    async def upload_part_async(
        self, bucket: str, key: str, part_number: int, upload_id: str, body: bytes
    ) -> Dict[str, Any]:
        return await self._run_sync(self.upload_part, 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]:
        try:
            return self._client.complete_multipart_upload(
                Bucket=bucket,
                Key=key,
                UploadId=upload_id,
                MultipartUpload={"Parts": parts},
            )
        except Exception as e:
            raise MultipartError(
                f"complete_multipart_upload failed for s3://{bucket}/{key} (upload {upload_id}): {e}"
            )

    async def complete_multipart_upload_async(
        self, bucket: str, key: str, upload_id: str, parts: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        return await self._run_sync(self.complete_multipart_upload, bucket, key, upload_id, parts)

    def abort_multipart_upload(self, bucket: str, key: str, upload_id: str) -> Dict[str, Any]:
        try:
            return self._client.abort_multipart_upload(
                Bucket=bucket, Key=key, UploadId=upload_id
            )
        except Exception as e:
            raise MultipartError(
                f"abort_multipart_upload failed for s3://{bucket}/{key} (upload {upload_id}): {e}"
            )

    async def abort_multipart_upload_async(self, bucket: str, key: str, upload_id: str) -> Dict[str, Any]:
        return await self._run_sync(self.abort_multipart_upload, bucket, key, upload_id)

    def list_parts(self, bucket: str, key: str, upload_id: str, **kwargs) -> Dict[str, Any]:
        try:
            return self._client.list_parts(
                Bucket=bucket, Key=key, UploadId=upload_id, **kwargs
            )
        except Exception as e:
            raise MultipartError(
                f"list_parts failed for s3://{bucket}/{key} (upload {upload_id}): {e}"
            )

    async def list_parts_async(self, bucket: str, key: str, upload_id: str, **kwargs) -> Dict[str, Any]:
        return await self._run_sync(self.list_parts, 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:
        try:
            params = {"Bucket": bucket, "Key": key, **kwargs}
            return self._client.generate_presigned_url(
                ClientMethod=operation, Params=params, ExpiresIn=expiration
            )
        except Exception as e:
            raise UploadError(f"generate_presigned_url failed for s3://{bucket}/{key}: {e}")

    async def generate_presigned_url_async(
        self,
        bucket: str,
        key: str,
        operation: str = "get_object",
        expiration: int = 3600,
        **kwargs,
    ) -> str:
        return await self._run_sync(
            self.generate_presigned_url, bucket, key, operation, expiration, **kwargs
        )

    def generate_presigned_download_url(
        self, bucket: str, key: str, expiration: int = 3600, filename: Optional[str] = None
    ) -> str:
        try:
            params: Dict[str, Any] = {"Bucket": bucket, "Key": key}
            if filename:
                params["ResponseContentDisposition"] = f'attachment; filename="{filename}"'
            return self._client.generate_presigned_url(
                ClientMethod="get_object", Params=params, ExpiresIn=expiration
            )
        except Exception as e:
            raise DownloadError(
                f"generate_presigned_download_url failed for s3://{bucket}/{key}: {e}"
            )

    async def generate_presigned_download_url_async(
        self, bucket: str, key: str, expiration: int = 3600, filename: Optional[str] = None
    ) -> str:
        return await self._run_sync(
            self.generate_presigned_download_url, bucket, key, expiration, filename
        )

    def generate_presigned_upload_url(
        self,
        bucket: str,
        key: str,
        expiration: int = 3600,
        content_type: Optional[str] = None,
    ) -> str:
        try:
            params: Dict[str, Any] = {"Bucket": bucket, "Key": key}
            if content_type:
                params["ContentType"] = content_type
            return self._client.generate_presigned_url(
                ClientMethod="put_object", Params=params, ExpiresIn=expiration
            )
        except Exception as e:
            raise UploadError(
                f"generate_presigned_upload_url failed for s3://{bucket}/{key}: {e}"
            )

    async def generate_presigned_upload_url_async(
        self,
        bucket: str,
        key: str,
        expiration: int = 3600,
        content_type: Optional[str] = None,
    ) -> str:
        return await self._run_sync(
            self.generate_presigned_upload_url, bucket, key, expiration, content_type
        )
