"""S3-compatible storage backend using aiobotocore.

Works with RustFS, MinIO, AWS S3, Ceph, and any S3-compatible service.
"""

from __future__ import annotations

import mimetypes
from datetime import datetime, timezone
from typing import Any

from storage_core.clients.base import StorageClient
from storage_core.serializer import (
    JsonMetadataSerializer,
    MetadataSerializer,
)
from storage_core.storage_types import ObjectListing, StorageObject


class S3StorageClient(StorageClient):
    """Storage client backed by an S3-compatible service (RustFS, MinIO, AWS, …).

    Parameters
    ----------
    endpoint_url:
        S3-compatible endpoint (e.g. ``http://localhost:9000`` for RustFS).
    access_key:
        Access key / username.
    secret_key:
        Secret key / password.
    region:
        AWS-like region.  Defaults to ``"us-east-1"``.
    metadata_serializer:
        Serializer for object metadata.  Defaults to
        :class:`~storage_core.serializer.JsonMetadataSerializer`.
    secure:
        Use HTTPS.  Defaults to ``True`` when *endpoint_url* starts with
        ``https://``, ``False`` otherwise.
    """

    def __init__(
        self,
        endpoint_url: str,
        access_key: str,
        secret_key: str,
        *,
        region: str = "us-east-1",
        metadata_serializer: MetadataSerializer | None = None,
        secure: bool | None = None,
    ) -> None:
        self._endpoint_url = endpoint_url
        self._access_key = access_key
        self._secret_key = secret_key
        self._region = region
        self._metadata_serializer = (
            metadata_serializer
            if metadata_serializer is not None
            else JsonMetadataSerializer()
        )
        self._secure = (
            secure
            if secure is not None
            else endpoint_url.startswith("https://")
        )
        self._client: Any = None

    # ------------------------------------------------------------------
    # Connection lifecycle
    # ------------------------------------------------------------------

    async def connect(self) -> None:
        """Create the aiobotocore session and S3 client."""
        import aiobotocore.session  # lazy import

        session = aiobotocore.session.get_session()
        self._client = await session.create_client(
            "s3",
            endpoint_url=self._endpoint_url,
            aws_access_key_id=self._access_key,
            aws_secret_access_key=self._secret_key,
            region_name=self._region,
            use_ssl=self._secure,
        ).__aenter__()

    async def disconnect(self) -> None:
        """Close the aiobotocore client."""
        if self._client is not None:
            await self._client.__aexit__(None, None, None)
            self._client = None

    async def ping(self) -> bool:
        self._ensure_client()
        try:
            await self._client.list_buckets()
            return True
        except Exception:
            return False

    # ------------------------------------------------------------------
    # Bucket operations
    # ------------------------------------------------------------------

    async def create_bucket(self, name: str) -> None:
        self._ensure_client()
        try:
            await self._client.create_bucket(Bucket=name)
        except self._client.exceptions.BucketAlreadyOwnedByYou:
            pass  # bucket already exists — idempotent

    async def delete_bucket(self, name: str) -> None:
        self._ensure_client()
        await self._client.delete_bucket(Bucket=name)

    async def list_buckets(self) -> list[str]:
        self._ensure_client()
        resp = await self._client.list_buckets()
        return [b["Name"] for b in resp.get("Buckets", [])]

    async def bucket_exists(self, name: str) -> bool:
        self._ensure_client()
        try:
            await self._client.head_bucket(Bucket=name)
            return True
        except Exception:
            return False

    # ------------------------------------------------------------------
    # Object CRUD
    # ------------------------------------------------------------------

    async def put(
        self,
        bucket: str,
        key: str,
        data: bytes,
        *,
        metadata: dict[str, Any] | None = None,
        content_type: str | None = None,
    ) -> StorageObject:
        """Shorthand for :meth:`put_from_data`."""
        return await self.put_from_data(
            bucket, key, data, metadata=metadata, content_type=content_type
        )

    async def put_from_data(
        self,
        bucket: str,
        key: str,
        data: bytes,
        *,
        metadata: dict[str, Any] | None = None,
        content_type: str | None = None,
    ) -> StorageObject:
        """Upload raw bytes as an object."""
        self._ensure_client()
        kwargs: dict[str, Any] = {
            "Bucket": bucket,
            "Key": key,
            "Body": data,
        }
        if content_type is not None:
            kwargs["ContentType"] = content_type
        if metadata:
            kwargs["Metadata"] = self._metadata_serializer.serialize(metadata)

        resp = await self._client.put_object(**kwargs)
        return self._response_to_object(
            bucket, key, resp, data, metadata
        )

    async def put_from_path(
        self,
        bucket: str,
        key: str,
        path: str,
        *,
        metadata: dict[str, Any] | None = None,
        content_type: str | None = None,
    ) -> StorageObject:
        """Read a local file at *path* and upload its contents."""
        self._ensure_client()
        if content_type is None:
            content_type, _ = mimetypes.guess_type(path)

        fh = open(path, "rb")
        try:
            kwargs: dict[str, Any] = {
                "Bucket": bucket,
                "Key": key,
                "Body": fh,
            }
            if content_type is not None:
                kwargs["ContentType"] = content_type
            if metadata:
                kwargs["Metadata"] = self._metadata_serializer.serialize(metadata)

            resp = await self._client.put_object(**kwargs)
        finally:
            fh.close()

        return self._response_to_object(
            bucket, key, resp, metadata=metadata
        )

    async def put_from_source(
        self,
        bucket: str,
        key: str,
        source: Any,
        *,
        metadata: dict[str, Any] | None = None,
        content_type: str | None = None,
    ) -> StorageObject:
        """Upload from an async-iterable *source*.

        *source* is passed directly as ``Body`` to the S3 client.
        It can be an async iterable yielding ``bytes`` chunks, an
        ``aiofiles`` handle, etc.
        """
        self._ensure_client()
        kwargs: dict[str, Any] = {
            "Bucket": bucket,
            "Key": key,
            "Body": source,
        }
        if content_type is not None:
            kwargs["ContentType"] = content_type
        if metadata:
            kwargs["Metadata"] = self._metadata_serializer.serialize(metadata)

        resp = await self._client.put_object(**kwargs)
        return self._response_to_object(
            bucket, key, resp, metadata=metadata
        )

    async def get(self, bucket: str, key: str) -> StorageObject:
        self._ensure_client()
        resp = await self._client.get_object(Bucket=bucket, Key=key)
        body = await resp["Body"].read()
        metadata = self._deserialize_metadata(resp.get("Metadata", {}))
        return self._response_to_object(bucket, key, resp, body, metadata)

    async def delete(self, bucket: str, key: str) -> None:
        self._ensure_client()
        await self._client.delete_object(Bucket=bucket, Key=key)

    async def head(self, bucket: str, key: str) -> StorageObject:
        self._ensure_client()
        resp = await self._client.head_object(Bucket=bucket, Key=key)
        metadata = self._deserialize_metadata(resp.get("Metadata", {}))
        return self._response_to_object(bucket, key, resp, None, metadata)

    async def list_objects(
        self,
        bucket: str,
        *,
        prefix: str = "",
        max_keys: int = 1000,
        token: str | None = None,
    ) -> ObjectListing:
        self._ensure_client()
        kwargs: dict[str, Any] = {
            "Bucket": bucket,
            "MaxKeys": max_keys,
        }
        if prefix:
            kwargs["Prefix"] = prefix
        if token:
            kwargs["ContinuationToken"] = token

        resp = await self._client.list_objects_v2(**kwargs)
        objects: list[StorageObject] = []
        for obj in resp.get("Contents", []):
            objects.append(
                StorageObject(
                    key=obj["Key"],
                    data=None,
                    size=obj.get("Size"),
                    last_modified=obj.get("LastModified"),
                    etag=obj.get("ETag", "").strip('"'),
                )
            )

        return ObjectListing(
            prefix=prefix,
            objects=objects,
            is_truncated=resp.get("IsTruncated", False),
            next_token=resp.get("NextContinuationToken"),
        )

    async def copy(
        self,
        source_bucket: str,
        source_key: str,
        dest_bucket: str,
        dest_key: str,
    ) -> StorageObject:
        self._ensure_client()
        copy_source = {"Bucket": source_bucket, "Key": source_key}
        resp = await self._client.copy_object(
            Bucket=dest_bucket,
            Key=dest_key,
            CopySource=copy_source,
        )
        return self._response_to_object(
            dest_bucket,
            dest_key,
            resp.get("CopyObjectResult", resp),
        )

    # ------------------------------------------------------------------
    # Presigned URLs
    # ------------------------------------------------------------------

    async def presign_get(
        self, bucket: str, key: str, *, expires_in: int = 3600
    ) -> str:
        self._ensure_client()
        return await self._client.generate_presigned_url(
            ClientMethod="get_object",
            Params={"Bucket": bucket, "Key": key},
            ExpiresIn=expires_in,
        )

    async def presign_put(
        self, bucket: str, key: str, *, expires_in: int = 3600
    ) -> str:
        self._ensure_client()
        return await self._client.generate_presigned_url(
            ClientMethod="put_object",
            Params={"Bucket": bucket, "Key": key},
            ExpiresIn=expires_in,
        )

    # ------------------------------------------------------------------
    # Multipart upload
    # ------------------------------------------------------------------

    async def create_multipart_upload(
        self,
        bucket: str,
        key: str,
        *,
        metadata: dict[str, Any] | None = None,
        content_type: str | None = None,
    ) -> str:
        self._ensure_client()
        kwargs: dict[str, Any] = {"Bucket": bucket, "Key": key}
        if content_type is not None:
            kwargs["ContentType"] = content_type
        if metadata:
            kwargs["Metadata"] = self._metadata_serializer.serialize(metadata)
        resp = await self._client.create_multipart_upload(**kwargs)
        return resp["UploadId"]

    async def upload_part(
        self,
        bucket: str,
        key: str,
        upload_id: str,
        part_number: int,
        data: bytes,
    ) -> dict[str, Any]:
        self._ensure_client()
        resp = await self._client.upload_part(
            Bucket=bucket,
            Key=key,
            UploadId=upload_id,
            PartNumber=part_number,
            Body=data,
        )
        return {
            "PartNumber": part_number,
            "ETag": resp["ETag"],
        }

    async def complete_multipart_upload(
        self,
        bucket: str,
        key: str,
        upload_id: str,
        parts: list[dict[str, Any]],
    ) -> StorageObject:
        self._ensure_client()
        resp = await self._client.complete_multipart_upload(
            Bucket=bucket,
            Key=key,
            UploadId=upload_id,
            MultipartUpload={"Parts": parts},
        )
        return self._response_to_object(bucket, key, resp)

    async def abort_multipart_upload(
        self, bucket: str, key: str, upload_id: str
    ) -> None:
        self._ensure_client()
        await self._client.abort_multipart_upload(
            Bucket=bucket, Key=key, UploadId=upload_id
        )

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _ensure_client(self) -> None:
        if self._client is None:
            raise RuntimeError(
                "S3StorageClient is not connected. Call await connect() first."
            )

    def _deserialize_metadata(
        self, raw: dict[str, str]
    ) -> dict[str, Any] | None:
        """Deserialize S3 metadata headers, or return None if empty."""
        if not raw:
            return None
        return self._metadata_serializer.deserialize(raw)

    @staticmethod
    def _parse_last_modified(
        value: Any,
    ) -> datetime | None:
        """Try to parse a last-modified value to UTC datetime."""
        if value is None:
            return None
        if isinstance(value, datetime):
            return value
        try:
            from datetime import datetime as dt

            for fmt in (
                "%a, %d %b %Y %H:%M:%S %Z",
                "%Y-%m-%dT%H:%M:%S.%fZ",
                "%Y-%m-%dT%H:%M:%SZ",
            ):
                try:
                    return dt.strptime(str(value), fmt).replace(
                        tzinfo=timezone.utc
                    )
                except ValueError:
                    continue
        except Exception:
            pass
        return None

    def _response_to_object(
        self,
        bucket: str,
        key: str,
        resp: dict[str, Any],
        data: bytes | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> StorageObject:
        """Build a :class:`StorageObject` from an S3 response dict."""
        return StorageObject(
            key=key,
            data=data,
            metadata=metadata,
            content_type=resp.get("ContentType"),
            size=resp.get("ContentLength"),
            last_modified=self._parse_last_modified(
                resp.get("LastModified")
            ),
            etag=resp.get("ETag", "").strip('"'),
        )
