"""
AWS S3 plugin for Daita Agents.

Simple S3 object storage operations - no over-engineering.
"""

import asyncio
import functools
import logging
import os
import io
from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
from pathlib import Path
from .base import BasePlugin
from ..core.exceptions import (
    PluginError,
    NotFoundError,
    PermissionError as DaitaPermissionError,
    AuthenticationError,
    TransientError,
)

if TYPE_CHECKING:
    from ..core.tools import AgentTool

logger = logging.getLogger(__name__)


class S3Plugin(BasePlugin):
    """
    Simple AWS S3 plugin for agents.

    Handles S3 operations with automatic format detection and focus system support.
    """

    def __init__(
        self,
        bucket: str,
        region: str = "us-east-1",
        aws_access_key_id: Optional[str] = None,
        aws_secret_access_key: Optional[str] = None,
        aws_session_token: Optional[str] = None,
        endpoint_url: Optional[str] = None,
        **kwargs,
    ):
        """
        Initialize S3 connection.

        Args:
            bucket: S3 bucket name
            region: AWS region
            aws_access_key_id: AWS access key (optional, uses env/IAM if not provided)
            aws_secret_access_key: AWS secret key (optional, uses env/IAM if not provided)
            aws_session_token: AWS session token (optional, for temporary credentials)
            endpoint_url: Custom S3 endpoint URL (for S3-compatible services)
            **kwargs: Additional boto3 parameters
        """
        if not bucket or not bucket.strip():
            raise ValueError("S3 bucket name cannot be empty")

        self.bucket = bucket
        self.region = region
        self.aws_access_key_id = aws_access_key_id
        self.aws_secret_access_key = aws_secret_access_key
        self.aws_session_token = aws_session_token
        self.endpoint_url = endpoint_url

        # Store additional config
        self.config = kwargs

        self._client = None
        self._session = None
        self._connect_lock = asyncio.Lock()

        logger.debug(f"S3 plugin configured for bucket {bucket} in region {region}")

    # ---------------------------------------------------------------------------
    # Error mapping
    # ---------------------------------------------------------------------------

    def _map_s3_error(self, error: Exception, operation: str) -> Exception:
        """Map boto3/botocore errors to framework exceptions."""
        if isinstance(error, ImportError):
            return PluginError(
                f"S3 {operation} failed: boto3 not installed. Install with: pip install 'daita-agents[aws]'",
                plugin_name="S3",
                retry_hint="permanent",
            )
        try:
            from botocore.exceptions import ClientError

            if isinstance(error, ClientError):
                code = error.response["Error"]["Code"]
                if code in ("404", "NoSuchKey", "NoSuchBucket"):
                    return NotFoundError(
                        f"S3 resource not found during {operation}",
                        resource_type="s3_object",
                    )
                elif code in ("403", "AccessDenied"):
                    return DaitaPermissionError(
                        f"Access denied to S3 bucket '{self.bucket}' during {operation}",
                        resource=self.bucket,
                        action=operation,
                    )
                elif code in (
                    "InvalidAccessKeyId",
                    "SignatureDoesNotMatch",
                    "ExpiredTokenException",
                ):
                    self._client = (
                        None  # force reconnect on next call (credential refresh)
                    )
                    return AuthenticationError(
                        f"AWS authentication failed during S3 {operation}",
                        provider="AWS S3",
                    )
                elif code in ("500", "503", "SlowDown", "RequestTimeout"):
                    return TransientError(f"S3 {operation} transient error: {error}")
                else:
                    return PluginError(
                        f"S3 {operation} failed: {error}", plugin_name="S3"
                    )
        except ImportError:
            pass
        return PluginError(f"S3 {operation} failed: {error}", plugin_name="S3")

    # ---------------------------------------------------------------------------
    # Connection management
    # ---------------------------------------------------------------------------

    async def connect(self):
        """Initialize S3 client."""
        async with self._connect_lock:
            if self._client is not None:
                return  # Already connected

            try:
                import boto3
                from botocore.exceptions import ClientError

                def _create_client():
                    session_kwargs = {"region_name": self.region}
                    if self.aws_access_key_id:
                        session_kwargs["aws_access_key_id"] = self.aws_access_key_id
                    if self.aws_secret_access_key:
                        session_kwargs["aws_secret_access_key"] = (
                            self.aws_secret_access_key
                        )
                    if self.aws_session_token:
                        session_kwargs["aws_session_token"] = self.aws_session_token

                    session = boto3.Session(**session_kwargs)
                    client_kwargs = {}
                    if self.endpoint_url:
                        client_kwargs["endpoint_url"] = self.endpoint_url
                    client = session.client("s3", **client_kwargs)
                    # list_objects_v2 is more reliable than head_bucket across boto3 versions
                    client.list_objects_v2(Bucket=self.bucket, MaxKeys=1)
                    return session, client

                loop = asyncio.get_running_loop()
                try:
                    self._session, self._client = await loop.run_in_executor(
                        None, _create_client
                    )
                    logger.info(f"Connected to S3 bucket: {self.bucket}")
                except ClientError as e:
                    raise self._map_s3_error(e, "connect") from e

            except ImportError as e:
                raise PluginError(
                    "boto3 not installed. Install with: pip install 'daita-agents[aws]'",
                    plugin_name="S3",
                    retry_hint="permanent",
                ) from e
            except (
                PluginError,
                NotFoundError,
                DaitaPermissionError,
                AuthenticationError,
                TransientError,
            ):
                raise
            except Exception as e:
                raise self._map_s3_error(e, "connect") from e

    async def disconnect(self):
        """Close S3 connection."""
        if self._client:
            # boto3 client doesn't need explicit closing
            self._client = None
            self._session = None
            logger.info("Disconnected from S3")

    # ---------------------------------------------------------------------------
    # Sync helpers (called via run_in_executor — no inline lambdas)
    # ---------------------------------------------------------------------------

    def _sync_list_objects(
        self, prefix: str, max_keys: int, continuation_token: Optional[str]
    ) -> dict:
        kwargs: Dict[str, Any] = {
            "Bucket": self.bucket,
            "Prefix": prefix,
            "MaxKeys": max_keys,
        }
        if continuation_token:
            kwargs["ContinuationToken"] = continuation_token
        return self._client.list_objects_v2(**kwargs)

    def _sync_get_object(self, key: str) -> bytes:
        response = self._client.get_object(Bucket=self.bucket, Key=key)
        return response["Body"].read()

    def _sync_put_object(self, put_args: dict) -> dict:
        return self._client.put_object(**put_args)

    def _sync_head_object(self, key: str) -> dict:
        return self._client.head_object(Bucket=self.bucket, Key=key)

    def _sync_delete_object(self, key: str) -> dict:
        return self._client.delete_object(Bucket=self.bucket, Key=key)

    def _sync_copy_object(self, copy_source: dict, dest_key: str) -> dict:
        return self._client.copy_object(
            CopySource=copy_source, Bucket=self.bucket, Key=dest_key
        )

    def _sync_download_file(self, key: str, local_path: str) -> None:
        self._client.download_file(self.bucket, key, local_path)

    def _sync_upload_file(self, local_path: str, key: str, content_type: str) -> None:
        self._client.upload_file(
            local_path, self.bucket, key, ExtraArgs={"ContentType": content_type}
        )

    # ---------------------------------------------------------------------------
    # Core operations
    # ---------------------------------------------------------------------------

    async def list_objects(
        self,
        prefix: str = "",
        max_keys: int = 1000,
        focus: Optional[str] = None,
        continuation_token: Optional[str] = None,
    ) -> Tuple[List[Dict[str, Any]], bool]:
        """
        List objects in the S3 bucket.

        Args:
            prefix: Object key prefix filter
            max_keys: Maximum number of objects to return
            focus: Focus DSL expression (e.g. "SELECT Key, Size" or "Size > 1000 | SELECT Key, Size")
            continuation_token: S3 continuation token for paginated results

        Returns:
            Tuple of (objects list, is_truncated flag)

        Example:
            objects, truncated = await s3.list_objects(prefix="data/", focus="SELECT Key, Size")
        """
        if self._client is None:
            await self.connect()

        try:
            loop = asyncio.get_running_loop()
            response = await loop.run_in_executor(
                None,
                functools.partial(
                    self._sync_list_objects, prefix, max_keys, continuation_token
                ),
            )
            objects = response.get("Contents", [])
            is_truncated = response.get("IsTruncated", False)

            if focus:
                from ..core.focus import apply_focus

                objects = apply_focus(objects, focus)

            return objects, is_truncated

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to list S3 objects: {e}")
            raise self._map_s3_error(e, "list_objects") from e

    async def get_object(
        self, key: str, format: str = "auto", focus: Optional[str] = None
    ) -> Union[bytes, str, Dict[str, Any], Any]:
        """
        Get an object from S3 with automatic format detection.

        Args:
            key: S3 object key
            format: Format type ('auto', 'bytes', 'text', 'json', 'csv', 'pandas')
            focus: Focus DSL expression applied after loading
                   (e.g. "price > 100 | SELECT name, price | LIMIT 500")

        Returns:
            Object data in requested format

        Example:
            data = await s3.get_object("reports/monthly.csv", format="pandas")
        """
        if self._client is None:
            await self.connect()

        try:
            loop = asyncio.get_running_loop()
            content = await loop.run_in_executor(
                None, functools.partial(self._sync_get_object, key)
            )

            # Auto-detect format from file extension
            if format == "auto":
                format = self._detect_format(key)

            # Process based on format — all branches assign to `data`
            if format == "bytes":
                return content  # focus not applicable to raw bytes
            elif format == "text":
                data = content.decode("utf-8")
            elif format == "json":
                import json

                data = json.loads(content.decode("utf-8"))
            elif format == "csv":
                import csv

                reader = csv.DictReader(io.StringIO(content.decode("utf-8")))
                data = list(reader)
            elif format == "pandas":
                try:
                    import pandas as pd

                    if key.endswith(".csv"):
                        data = pd.read_csv(io.BytesIO(content))
                    elif key.endswith(".json"):
                        data = pd.read_json(io.BytesIO(content))
                    elif key.endswith(".parquet"):
                        data = pd.read_parquet(io.BytesIO(content))
                    elif key.endswith(".xlsx"):
                        data = pd.read_excel(io.BytesIO(content))
                    else:
                        data = pd.read_csv(io.BytesIO(content))
                except ImportError:
                    raise ImportError(
                        "pandas not installed. Install with: pip install 'daita-agents[data]'"
                    )
            else:
                return content

            # Single apply point for focus — works for json, csv, pandas, and text
            if focus and data is not None:
                from ..core.focus import apply_focus

                return apply_focus(data, focus)
            return data

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to get S3 object {key}: {e}")
            raise self._map_s3_error(e, "get_object") from e

    async def put_object(
        self,
        key: str,
        data: Union[bytes, str, Dict[str, Any], List[Any], Any],
        content_type: Optional[str] = None,
        metadata: Optional[Dict[str, str]] = None,
    ) -> Dict[str, Any]:
        """
        Put an object to S3.

        Args:
            key: S3 object key
            data: Data to upload (bytes, string, list, dict, or pandas DataFrame)
            content_type: Content type (auto-detected if not provided)
            metadata: Object metadata

        Returns:
            Upload result metadata

        Example:
            result = await s3.put_object("data/output.json", {"result": "success"})
        """
        if self._client is None:
            await self.connect()

        try:
            # Process data based on type
            if hasattr(data, "to_csv"):  # pandas DataFrame
                buffer = io.StringIO()
                data.to_csv(buffer, index=False)
                body = buffer.getvalue().encode("utf-8")
                content_type = content_type or "text/csv"
            elif hasattr(data, "to_json"):  # pandas DataFrame to JSON
                buffer = io.StringIO()
                data.to_json(buffer, orient="records", indent=2)
                body = buffer.getvalue().encode("utf-8")
                content_type = content_type or "application/json"
            elif isinstance(data, list):
                import json

                body = json.dumps(data, indent=2).encode("utf-8")
                content_type = content_type or "application/json"
            elif isinstance(data, dict):
                import json

                body = json.dumps(data, indent=2).encode("utf-8")
                content_type = content_type or "application/json"
            elif isinstance(data, str):
                body = data.encode("utf-8")
                content_type = content_type or "text/plain"
            elif isinstance(data, bytes):
                body = data
                content_type = content_type or "application/octet-stream"
            else:
                # Try to convert to string
                body = str(data).encode("utf-8")
                content_type = content_type or "text/plain"

            # Reject payloads that exceed S3's single-PUT limit
            if len(body) > 5 * 1024**3:
                raise PluginError(
                    "Payload exceeds S3 5 GB single-PUT limit. Use upload_file() for large files.",
                    plugin_name="S3",
                    retry_hint="permanent",
                )

            # Auto-detect content type from key if not provided
            if not content_type:
                content_type = self._detect_content_type(key)

            # Prepare put_object arguments
            put_args = {
                "Bucket": self.bucket,
                "Key": key,
                "Body": body,
                "ContentType": content_type,
            }

            if metadata:
                put_args["Metadata"] = metadata

            # Upload object (synchronous — wrapped in executor)
            loop = asyncio.get_running_loop()
            response = await loop.run_in_executor(
                None, functools.partial(self._sync_put_object, put_args)
            )

            result = {
                "key": key,
                "etag": response["ETag"],
                "size": len(body),
                "content_type": content_type,
            }

            if metadata:
                result["metadata"] = metadata

            logger.info(f"Uploaded S3 object: {key} ({len(body)} bytes)")
            return result

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to put S3 object {key}: {e}")
            raise self._map_s3_error(e, "put_object") from e

    async def upload_dataframe(
        self, df: Any, key: str, format: str = "csv", **kwargs
    ) -> Dict[str, Any]:
        """
        Upload a pandas DataFrame to S3.

        Args:
            df: pandas DataFrame
            key: S3 object key
            format: Output format ('csv', 'json', 'parquet')
            **kwargs: Additional format-specific parameters

        Returns:
            Upload result metadata

        Example:
            result = await s3.upload_dataframe(df, "processed/results.parquet", format="parquet")
        """
        if self._client is None:
            await self.connect()

        try:
            if format == "csv":
                buffer = io.StringIO()
                df.to_csv(buffer, index=False, **kwargs)
                body = buffer.getvalue().encode("utf-8")
                content_type = "text/csv"
            elif format == "json":
                buffer = io.StringIO()
                df.to_json(buffer, orient="records", indent=2, **kwargs)
                body = buffer.getvalue().encode("utf-8")
                content_type = "application/json"
            elif format == "parquet":
                buffer = io.BytesIO()
                df.to_parquet(buffer, **kwargs)
                body = buffer.getvalue()
                content_type = "application/octet-stream"
            else:
                raise ValueError(f"Unsupported format: {format}")

            # Upload using put_object
            return await self.put_object(key, body, content_type)

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to upload DataFrame to S3: {e}")
            raise self._map_s3_error(e, "upload_dataframe") from e

    async def download_file(self, key: str, local_path: str) -> str:
        """
        Download an S3 object to local file.

        Args:
            key: S3 object key
            local_path: Local file path

        Returns:
            Local file path

        Example:
            path = await s3.download_file("data/input.csv", "/tmp/input.csv")
        """
        if self._client is None:
            await self.connect()

        try:
            # Ensure directory exists (dirname is empty string for bare filenames)
            parent = os.path.dirname(local_path)
            if parent:
                os.makedirs(parent, exist_ok=True)

            # Download file (synchronous — wrapped in executor)
            loop = asyncio.get_running_loop()
            await loop.run_in_executor(
                None,
                functools.partial(self._sync_download_file, key, local_path),
            )

            logger.info(f"Downloaded S3 object {key} to {local_path}")
            return local_path

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to download S3 object {key}: {e}")
            raise self._map_s3_error(e, "download_file") from e

    async def upload_file(self, local_path: str, key: str) -> Dict[str, Any]:
        """
        Upload a local file to S3.

        Args:
            local_path: Local file path
            key: S3 object key

        Returns:
            Upload result metadata

        Example:
            result = await s3.upload_file("/tmp/output.csv", "results/output.csv")
        """
        if self._client is None:
            await self.connect()

        try:
            if not os.path.exists(local_path):
                raise FileNotFoundError(f"Local file not found: {local_path}")

            # Get file size
            file_size = os.path.getsize(local_path)

            # Auto-detect content type
            content_type = self._detect_content_type(local_path)

            loop = asyncio.get_running_loop()
            await loop.run_in_executor(
                None,
                functools.partial(
                    self._sync_upload_file, local_path, key, content_type
                ),
            )

            # Get object metadata
            response = await loop.run_in_executor(
                None, functools.partial(self._sync_head_object, key)
            )

            result = {
                "key": key,
                "etag": response["ETag"],
                "size": file_size,
                "content_type": content_type,
                "local_path": local_path,
            }

            logger.info(f"Uploaded file {local_path} to S3 object {key}")
            return result

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to upload file {local_path} to S3: {e}")
            raise self._map_s3_error(e, "upload_file") from e

    async def delete_object(self, key: str) -> Dict[str, Any]:
        """
        Delete an object from S3.

        Args:
            key: S3 object key

        Returns:
            Delete result metadata

        Example:
            result = await s3.delete_object("temp/old_file.txt")
        """
        if self._client is None:
            await self.connect()

        try:
            loop = asyncio.get_running_loop()
            await loop.run_in_executor(
                None, functools.partial(self._sync_delete_object, key)
            )

            result = {"key": key, "deleted": True}

            logger.info(f"Deleted S3 object: {key}")
            return result

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to delete S3 object {key}: {e}")
            raise self._map_s3_error(e, "delete_object") from e

    async def copy_object(
        self, source_key: str, dest_key: str, source_bucket: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        Copy an object within S3.

        Args:
            source_key: Source object key
            dest_key: Destination object key
            source_bucket: Source bucket (uses same bucket if not provided)

        Returns:
            Copy result metadata

        Example:
            result = await s3.copy_object("data/input.csv", "backup/input.csv")
        """
        if self._client is None:
            await self.connect()

        try:
            source_bucket = source_bucket or self.bucket
            copy_source = {"Bucket": source_bucket, "Key": source_key}

            loop = asyncio.get_running_loop()
            response = await loop.run_in_executor(
                None,
                functools.partial(self._sync_copy_object, copy_source, dest_key),
            )

            result = {
                "source_key": source_key,
                "dest_key": dest_key,
                "source_bucket": source_bucket,
                "dest_bucket": self.bucket,
                "etag": response["CopyObjectResult"]["ETag"],
            }

            logger.info(f"Copied S3 object {source_key} to {dest_key}")
            return result

        except (
            PluginError,
            NotFoundError,
            DaitaPermissionError,
            AuthenticationError,
            TransientError,
        ):
            raise
        except Exception as e:
            logger.error(f"Failed to copy S3 object {source_key}: {e}")
            raise self._map_s3_error(e, "copy_object") from e

    # ---------------------------------------------------------------------------
    # Format helpers
    # ---------------------------------------------------------------------------

    def _detect_format(self, key: str) -> str:
        """Detect format from file extension."""
        ext = Path(key).suffix.lower()

        format_map = {
            ".json": "json",
            ".csv": "csv",
            ".txt": "text",
            ".parquet": "pandas",
            ".xlsx": "pandas",
            ".xls": "pandas",
        }

        return format_map.get(ext, "bytes")

    def _detect_content_type(self, key: str) -> str:
        """Detect content type from file extension."""
        ext = Path(key).suffix.lower()

        content_type_map = {
            ".json": "application/json",
            ".csv": "text/csv",
            ".txt": "text/plain",
            ".parquet": "application/octet-stream",
            ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            ".xls": "application/vnd.ms-excel",
            ".pdf": "application/pdf",
            ".png": "image/png",
            ".jpg": "image/jpeg",
            ".jpeg": "image/jpeg",
            ".gif": "image/gif",
        }

        return content_type_map.get(ext, "application/octet-stream")

    # ---------------------------------------------------------------------------
    # Tool definitions
    # ---------------------------------------------------------------------------

    def get_tools(self) -> List["AgentTool"]:
        """
        Expose S3 operations as agent tools.

        Returns:
            List of AgentTool instances for S3 operations
        """
        from ..core.tools import AgentTool

        return [
            AgentTool(
                name="read_s3_file",
                description="Read and parse a file from S3 bucket. Automatically detects format (CSV, JSON, Parquet, text) based on file extension.",
                parameters={
                    "type": "object",
                    "properties": {
                        "key": {
                            "type": "string",
                            "description": "S3 object key (file path within the bucket)",
                        },
                        "format": {
                            "type": "string",
                            "description": "Format hint: 'auto', 'csv', 'json', 'pandas', 'text'. Default is 'auto' which detects from extension.",
                        },
                        "focus": {
                            "type": "string",
                            "description": 'Focus DSL to filter/project results, e.g. "price > 100 | SELECT name, price"',
                        },
                    },
                    "required": ["key"],
                },
                handler=self._tool_read_file,
                category="storage",
                source="plugin",
                plugin_name="S3",
                timeout_seconds=120,
            ),
            AgentTool(
                name="write_s3_file",
                description="Write data to S3 bucket. Accepts dictionaries or lists (saved as JSON), strings (saved as text), or binary data.",
                parameters={
                    "type": "object",
                    "properties": {
                        "key": {
                            "type": "string",
                            "description": "S3 object key (file path within the bucket)",
                        },
                        "data": {
                            "description": "Data to write (dict or list for JSON, string for text, or bytes for binary)",
                        },
                    },
                    "required": ["key", "data"],
                },
                handler=self._tool_write_file,
                category="storage",
                source="plugin",
                plugin_name="S3",
                timeout_seconds=120,
            ),
            AgentTool(
                name="list_s3_objects",
                description="List objects in S3 bucket with optional prefix filter to narrow down results",
                parameters={
                    "type": "object",
                    "properties": {
                        "prefix": {
                            "type": "string",
                            "description": "Filter objects by prefix (folder path). Leave empty to list all objects.",
                        },
                        "max_keys": {
                            "type": "integer",
                            "description": "Maximum number of objects to return. Default is 100.",
                        },
                        "focus": {
                            "type": "string",
                            "description": 'Focus DSL to filter/project results, e.g. "SELECT Key, Size"',
                        },
                    },
                    "required": [],
                },
                handler=self._tool_list_objects,
                category="storage",
                source="plugin",
                plugin_name="S3",
                timeout_seconds=60,
            ),
            AgentTool(
                name="delete_s3_file",
                description="Delete a file from S3 bucket. This operation is permanent and cannot be undone.",
                parameters={
                    "type": "object",
                    "properties": {
                        "key": {
                            "type": "string",
                            "description": "S3 object key (file path) to delete",
                        }
                    },
                    "required": ["key"],
                },
                handler=self._tool_delete_file,
                category="storage",
                source="plugin",
                plugin_name="S3",
                timeout_seconds=30,
            ),
            AgentTool(
                name="head_s3_object",
                description="Get metadata for an S3 object (size, content type, last modified) without downloading it.",
                parameters={
                    "type": "object",
                    "properties": {
                        "key": {
                            "type": "string",
                            "description": "S3 object key (file path)",
                        }
                    },
                    "required": ["key"],
                },
                handler=self._tool_head_object,
                category="storage",
                source="plugin",
                plugin_name="S3",
                timeout_seconds=15,
            ),
        ]

    # ---------------------------------------------------------------------------
    # Tool handlers
    # ---------------------------------------------------------------------------

    async def _tool_read_file(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Tool handler for read_s3_file"""
        key = args.get("key")
        format_hint = args.get("format", "auto")
        focus = args.get("focus")
        detected_format = self._detect_format(key)

        # Binary formats (including pandas-read parquet/xlsx): return metadata only
        if detected_format in ("bytes", "pandas"):
            if self._client is None:
                await self.connect()
            loop = asyncio.get_running_loop()
            head = await loop.run_in_executor(
                None, functools.partial(self._sync_head_object, key)
            )
            return {
                "key": key,
                "binary": True,
                "content_type": head.get("ContentType", "application/octet-stream"),
                "size": head.get("ContentLength"),
                "last_modified": str(head.get("LastModified", "")),
                "bucket": self.bucket,
            }

        # Avoid calling _detect_format twice when format_hint is "auto"
        effective_format = detected_format if format_hint == "auto" else format_hint
        data = await self.get_object(key, format=effective_format, focus=focus)

        _MAX_CHARS = 50_000
        _MAX_ROWS = 200

        # CSV / list: cap rows
        if isinstance(data, list):
            total = len(data)
            truncated = total > _MAX_ROWS
            return {
                "key": key,
                "format": detected_format,
                "rows": data[:_MAX_ROWS],
                "total_rows": total,
                "truncated": truncated,
                "bucket": self.bucket,
            }

        # Text / JSON: cap characters
        import json as _json

        if isinstance(data, (dict, list)):
            serialized = _json.dumps(data)
        else:
            serialized = str(data) if not isinstance(data, str) else data

        truncated = len(serialized) > _MAX_CHARS
        return {
            "key": key,
            "format": detected_format,
            "content": serialized[:_MAX_CHARS],
            "truncated": truncated,
            **({"total_chars": len(serialized)} if truncated else {}),
            "bucket": self.bucket,
        }

    async def _tool_write_file(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Tool handler for write_s3_file"""
        key = args.get("key")
        data = args.get("data")

        result = await self.put_object(key, data)

        return {
            "key": key,
            "size": result.get("size"),
            "location": f"s3://{self.bucket}/{key}",
            "bucket": self.bucket,
        }

    async def _tool_list_objects(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Tool handler for list_s3_objects"""
        prefix = args.get("prefix", "")
        max_keys = args.get("max_keys", 100)
        focus = args.get("focus")

        objects, is_s3_truncated = await self.list_objects(
            prefix=prefix, max_keys=max_keys, focus=focus
        )

        # Simplify object metadata for LLM consumption
        simplified = [
            {
                "key": obj["Key"],
                "size": obj["Size"],
                "modified": str(obj.get("LastModified", "")),
            }
            for obj in objects
        ]

        # Cap at 200 objects to avoid token bloat
        total = len(simplified)
        display_truncated = total > 200
        simplified = simplified[:200]
        is_truncated = is_s3_truncated or display_truncated

        result: Dict[str, Any] = {
            "objects": simplified,
            "total_objects": total,
            "truncated": is_truncated,
            "bucket": self.bucket,
            "prefix": prefix if prefix else "(all objects)",
        }

        if is_s3_truncated:
            result["note"] = (
                "More objects exist. Use a more specific prefix to narrow results."
            )

        return result

    async def _tool_delete_file(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Tool handler for delete_s3_file"""
        key = args.get("key")

        result = await self.delete_object(key)

        return {
            "key": key,
            "deleted": result.get("deleted", True),
            "bucket": self.bucket,
        }

    async def _tool_head_object(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Tool handler for head_s3_object"""
        key = args.get("key")

        if self._client is None:
            await self.connect()

        loop = asyncio.get_running_loop()
        response = await loop.run_in_executor(
            None, functools.partial(self._sync_head_object, key)
        )

        return {
            "key": key,
            "bucket": self.bucket,
            "size": response.get("ContentLength"),
            "content_type": response.get("ContentType"),
            "last_modified": str(response.get("LastModified", "")),
            "etag": response.get("ETag"),
            "metadata": response.get("Metadata", {}),
        }

    # Context manager support
    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.disconnect()


def s3(**kwargs) -> S3Plugin:
    """Create S3 plugin with simplified interface."""
    return S3Plugin(**kwargs)
