# pylint: disable=too-many-positional-arguments
"""
S3-compatible object storage implementation.

Works with AWS S3, MinIO, Cloudflare R2, and any S3-compatible service.
Uses boto3 client for compatibility across all providers.
"""

import logging
from typing import BinaryIO, Dict, List, Optional

import boto3
from botocore.exceptions import ClientError

from mini_videorag.core.storage import BaseObjectStorage, StorageObject


class S3Storage(BaseObjectStorage):
    """
    S3-compatible object storage implementation.

    This implementation works with any S3-compatible storage backend by
    configuring the endpoint URL. No code changes needed to switch between
    AWS S3, MinIO, Cloudflare R2, etc.

    Examples:
        # AWS S3 (default)
        storage = S3Storage(
            bucket_name="my-bucket",
            region="us-east-1"
        )

        # MinIO (local development)
        storage = S3Storage(
            bucket_name="videos",
            endpoint_url="http://localhost:9000",
            access_key="minioadmin",
            secret_key="minioadmin"
        )

        # Cloudflare R2
        storage = S3Storage(
            bucket_name="my-bucket",
            endpoint_url="https://<account-id>.r2.cloudflarestorage.com",
            access_key="<access-key>",
            secret_key="<secret-key>"
        )

    Attributes:
        bucket_name: Name of the S3 bucket to use
        client: Boto3 S3 client instance
        logger: Logger for this storage instance
    """

    def __init__(
        self,
        bucket_name: str,
        endpoint_url: Optional[str] = None,
        region: str = "us-east-1",
        access_key: Optional[str] = None,
        secret_key: Optional[str] = None,
    ):
        """
        Initialize S3 storage.

        Args:
            bucket_name: S3 bucket name
            endpoint_url: Custom endpoint URL (for MinIO, R2, etc.). None = AWS S3
            region: AWS region (ignored for non-AWS endpoints)
            access_key: AWS access key ID (None = use default credentials)
            secret_key: AWS secret access key (None = use default credentials)
        """
        self.bucket_name = bucket_name
        self.logger = logging.getLogger(__name__)

        client_config = {
            "region_name": region,
        }

        if endpoint_url:
            client_config["endpoint_url"] = endpoint_url

        if access_key and secret_key:
            client_config["aws_access_key_id"] = access_key
            client_config["aws_secret_access_key"] = secret_key

        self.client = boto3.client("s3", **client_config)

        self.logger.info(
            f"Initialized S3 storage: bucket={bucket_name}, "
            f"endpoint={endpoint_url or 'AWS S3'}, region={region}"
        )

    def upload(
        self,
        file: BinaryIO,
        key: str,
        metadata: Optional[Dict[str, str]] = None,
        content_type: Optional[str] = None,
    ) -> str:
        """
        Upload a file to S3.

        Args:
            file: File-like object to upload
            key: S3 object key (path)
            metadata: Optional metadata to attach
            content_type: MIME type of the file

        Returns:
            The S3 key where object was stored

        Raises:
            Exception: If upload fails
        """
        try:
            extra_args = {}
            if metadata:
                extra_args["Metadata"] = metadata
            if content_type:
                extra_args["ContentType"] = content_type

            self.client.upload_fileobj(
                file,
                self.bucket_name,
                key,
                ExtraArgs=extra_args if extra_args else None,
            )

            self.logger.info(f"Uploaded object to S3: s3://{self.bucket_name}/{key}")
            return key

        except ClientError as e:
            self.logger.error(f"Failed to upload to S3: {e}")
            raise

    def download(self, key: str, file: BinaryIO) -> None:
        """
        Download a file from S3.

        Args:
            key: S3 object key
            file: File-like object to write to

        Raises:
            ClientError: If download fails or object doesn't exist
        """
        try:
            self.client.download_fileobj(self.bucket_name, key, file)
            self.logger.debug(f"Downloaded object from S3: s3://{self.bucket_name}/{key}")

        except ClientError as e:
            if e.response["Error"]["Code"] == "404":
                raise KeyError(f"Object not found: s3://{self.bucket_name}/{key}") from e
            self.logger.error(f"Failed to download from S3: {e}")
            raise

    def delete(self, key: str) -> bool:
        """
        Delete an object from S3.

        Args:
            key: S3 object key to delete

        Returns:
            True if object was deleted, False if it didn't exist

        Raises:
            ClientError: If deletion fails
        """
        try:
            if not self.exists(key):
                return False

            self.client.delete_object(Bucket=self.bucket_name, Key=key)
            self.logger.info(f"Deleted object from S3: s3://{self.bucket_name}/{key}")
            return True

        except ClientError as e:
            self.logger.error(f"Failed to delete from S3: {e}")
            raise

    def exists(self, key: str) -> bool:
        """
        Check if an object exists in S3.

        Args:
            key: S3 object key to check

        Returns:
            True if object exists, False otherwise
        """
        try:
            self.client.head_object(Bucket=self.bucket_name, Key=key)
            return True
        except ClientError as e:
            if e.response["Error"]["Code"] == "404":
                return False
            raise

    def list(self, prefix: str = "") -> List[StorageObject]:
        """
        List objects in S3 bucket with optional prefix filter.

        Args:
            prefix: Optional prefix to filter objects (e.g., "videos/")

        Returns:
            List of StorageObject instances

        Raises:
            ClientError: If listing fails
        """
        try:
            objects = []

            paginator = self.client.get_paginator("list_objects_v2")
            page_iterator = paginator.paginate(Bucket=self.bucket_name, Prefix=prefix)

            for page in page_iterator:
                if "Contents" not in page:
                    continue

                for obj in page["Contents"]:
                    objects.append(
                        StorageObject(
                            key=obj["Key"],
                            size=obj["Size"],
                            last_modified=obj["LastModified"].isoformat(),
                            content_type=None,  # Not included in list response
                        )
                    )

            self.logger.debug(f"Listed {len(objects)} objects with prefix '{prefix}'")
            return objects

        except ClientError as e:
            self.logger.error(f"Failed to list S3 objects: {e}")
            raise

    def get_url(self, key: str, expires_in: int = 3600) -> str:
        """
        Generate a presigned URL for accessing an object.

        Args:
            key: S3 object key
            expires_in: URL expiration time in seconds (default 1 hour)

        Returns:
            Presigned URL string

        Raises:
            ClientError: If URL generation fails
        """
        try:
            if not self.exists(key):
                raise KeyError(f"Object not found: s3://{self.bucket_name}/{key}")

            url = self.client.generate_presigned_url(
                "get_object",
                Params={"Bucket": self.bucket_name, "Key": key},
                ExpiresIn=expires_in,
            )

            self.logger.debug(f"Generated presigned URL for {key}, expires in {expires_in}s")
            return url

        except ClientError as e:
            self.logger.error(f"Failed to generate presigned URL: {e}")
            raise

    def create_bucket_if_not_exists(self) -> None:
        """
        Create the bucket if it doesn't exist.

        Useful for local development with MinIO or testing.
        In production, buckets should typically be pre-created.

        Raises:
            ClientError: If bucket creation fails
        """
        try:
            self.client.head_bucket(Bucket=self.bucket_name)
            self.logger.debug(f"Bucket '{self.bucket_name}' already exists")
        except ClientError as e:
            if e.response["Error"]["Code"] == "404":
                self.logger.info(f"Creating bucket: {self.bucket_name}")
                self.client.create_bucket(Bucket=self.bucket_name)
            else:
                raise
