from __future__ import annotations

from typing import Any, Optional
from urllib.parse import ParseResult, urlparse

from xfintech.connect.common.connect import ConnectLike
from xfintech.connect.common.connectref import ConnectRef


class S3Connect(ConnectLike):
    """
    描述:
    - AWS S3 存储连接实现类。
    - 实现了 ConnectLike 协议，用于与 AWS S3 存储进行交互。
    - 支持从 S3 读取和写入对象数据。
    - 自动处理 S3 URI 解析和客户端初始化。
    - 可以使用自定义 boto3 客户端或自动创建默认客户端。

    属性:
    - kind: 连接类型标识，固定为 "s3"。
    - _client: boto3 S3 客户端实例。

    例子:
    ```python
        from xfintech.connect.instance.s3 import S3Connect

        # 使用默认客户端
        connect = S3Connect()

        # 写入数据
        ref = connect.put_object(b"data", "s3://bucket/key")

        # 读取数据
        data = connect.get_object("s3://bucket/key")

        # 使用自定义客户端
        import boto3
        custom_client = boto3.client('s3', region_name='us-west-2')
        connect = S3Connect(client=custom_client)
    ```
    """

    def __init__(
        self,
        client: Optional[object] = None,
    ) -> None:
        self.kind = "s3"
        self._client = self._resolve_client(client)

    def _resolve_client(
        self,
        client: Optional[object] = None,
    ) -> object:
        if client is not None:
            return client
        try:
            import boto3

            client = boto3.client("s3")
        except ImportError as e:
            msg = "boto3 is required for S3Connect but not installed. Install it with 'pip install boto3'."
            raise ImportError(msg) from e
        try:
            if not hasattr(client, "put_object"):
                msg = "invalid s3 client: missing put_object method"
                raise AttributeError(msg)
            if not callable(getattr(client, "put_object")):
                msg = "invalid s3 client: put_object is not callable"
                raise TypeError(msg)
        except AssertionError as e:
            msg = "invalid s3 client: missing put_object method"
            raise ValueError(msg) from e
        return client

    def _resolve_s3_uri(
        self,
        location: str,
    ) -> ParseResult:
        if not location.startswith("s3://"):
            location = "s3://" + location
        try:
            location = urlparse(location)
        except Exception as e:
            msg = f"invalid s3 location: {location}"
            raise ValueError(msg) from e
        if location.scheme != "s3":
            msg = f"s3 location must start with s3:// : {location.geturl()}"
            raise ValueError(msg)
        if not location.netloc:
            msg = f"s3 location missing bucket: {location.geturl()}"
            raise ValueError(msg)
        if not location.path:
            msg = f"s3 location missing key: {location.geturl()}"
            raise ValueError(msg)
        return location

    def _resolve_bucket(
        self,
        location: str,
    ) -> str:
        res = self._resolve_s3_uri(location)
        if not res.netloc:
            msg = f"s3 location missing bucket: {location}"
            raise ValueError(msg)
        return res.netloc

    def _resolve_key(
        self,
        location: str,
    ) -> str:
        res = self._resolve_s3_uri(location)
        if not res.path:
            msg = f"s3 location missing key: {location}"
            raise ValueError(msg)
        return res.path.lstrip("/")

    def __str__(self) -> str:
        return f"{self.__class__.__name__}"

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}()"

    def put_bytes(
        self,
        data: bytes,
        location: str,
        **kwargs: Any,
    ) -> None:
        bucket = self._resolve_bucket(location)
        key = self._resolve_key(location)
        self._client.put_object(
            Bucket=bucket,
            Key=key,
            Body=data,
            **kwargs,
        )

    def get_bytes(
        self,
        location: str,
        **kwargs: Any,
    ) -> bytes:
        bucket = self._resolve_bucket(location)
        key = self._resolve_key(location)
        response = self._client.get_object(
            Bucket=bucket,
            Key=key,
            **kwargs,
        )
        return response["Body"].read()

    def put_object(
        self,
        data: bytes,
        location: str,
        **kwargs: Any,
    ) -> ConnectRef:
        self.put_bytes(
            data,
            location,
            **kwargs,
        )
        return ConnectRef(
            location=str(location),
            kind=self.kind,
        )

    def get_object(
        self,
        location: str,
        **kwargs: Any,
    ) -> bytes:
        return self.get_bytes(
            location,
            **kwargs,
        )
