from __future__ import annotations

import tempfile
from pathlib import Path
from typing import Any, Optional

import httpx

from .errors import (
    AuthenticationError,
    NetworkError,
    RagentOssError,
    UploadError,
    ValidationError,
)
from .types import (
    DeleteRequest,
    DeleteResponse,
    HealthResponse,
    PresignRequest,
    PresignResponse,
    SignRequest,
    SignResponse,
)


class RagentOssClient:
    """Async client for ragent-oss storage gateway.

    Usage::

        async with RagentOssClient("https://oss.ragents.net", "your-api-key") as client:
            presign = await client.presign(PresignRequest("file.pdf", "application/pdf", "knowledge"))
    """

    def __init__(self, base_url: str, api_key: str) -> None:
        self._base_url = base_url.rstrip("/")
        self._api_key = api_key
        self._client: Optional[httpx.AsyncClient] = None

    async def __aenter__(self) -> RagentOssClient:
        self._client = httpx.AsyncClient()
        return self

    async def __aexit__(self, *args: Any) -> None:
        if self._client:
            await self._client.aclose()
            self._client = None

    def _get_client(self) -> httpx.AsyncClient:
        if self._client is None:
            self._client = httpx.AsyncClient()
        return self._client

    async def close(self) -> None:
        """Close the underlying HTTP client."""
        if self._client:
            await self._client.aclose()
            self._client = None

    async def health(self) -> HealthResponse:
        """Check service health (no authentication required)."""
        data = await self._get(f"{self._base_url}/api/health")
        return HealthResponse(status=data["status"], storage=data["storage"])

    async def presign(self, req: PresignRequest) -> PresignResponse:
        """Get a presigned upload URL."""
        data = await self._post(
            f"{self._base_url}/api/presign",
            {
                "filename": req.filename,
                "contentType": req.content_type,
                "category": req.category,
            },
        )
        return PresignResponse(
            object_key=data["objectKey"],
            upload_url=data["uploadUrl"],
            headers=data["headers"],
        )

    async def sign(self, req: SignRequest) -> SignResponse:
        """Get a signed download URL."""
        body: dict[str, Any] = {"objectKey": req.object_key}
        if req.expires_in is not None:
            body["expiresIn"] = req.expires_in
        data = await self._post(f"{self._base_url}/api/sign", body)
        return SignResponse(url=data["url"])

    async def delete(self, req: DeleteRequest) -> DeleteResponse:
        """Delete an object."""
        data = await self._post(
            f"{self._base_url}/api/delete",
            {"objectKey": req.object_key},
        )
        return DeleteResponse(success=data["success"])

    async def upload(
        self,
        filename: str,
        content: bytes,
        content_type: str,
        category: str,
    ) -> str:
        """Convenience: presign + PUT upload. Returns the object key."""
        presign = await self.presign(
            PresignRequest(
                filename=filename,
                content_type=content_type,
                category=category,
            )
        )
        client = self._get_client()
        try:
            res = await client.put(
                presign.upload_url,
                content=content,
                headers=presign.headers,
            )
        except httpx.HTTPError as exc:
            raise NetworkError("Upload failed", exc) from exc

        if res.status_code < 200 or res.status_code >= 300:
            raise UploadError(f"Upload failed: {res.status_code}", res.status_code)

        return presign.object_key

    async def download(self, object_key: str, expires_in: Optional[int] = None) -> bytes:
        """Convenience: sign + GET to download file content."""
        sign = await self.sign(SignRequest(object_key=object_key, expires_in=expires_in))
        client = self._get_client()
        try:
            res = await client.get(sign.url)
        except httpx.HTTPError as exc:
            raise NetworkError("Download failed", exc) from exc

        if res.status_code < 200 or res.status_code >= 300:
            raise RagentOssError(
                f"Download failed: {res.status_code}",
                res.status_code,
                res.text,
            )
        return res.content

    async def download_to_temp(
        self, object_key: str, expires_in: Optional[int] = None
    ) -> Path:
        """Convenience: sign + streaming download to a temporary file. Returns the file path."""
        sign = await self.sign(SignRequest(object_key=object_key, expires_in=expires_in))
        client = self._get_client()

        # Extract extension from object_key for the temp file
        ext = ""
        parts = object_key.rsplit(".", 1)
        if len(parts) == 2:
            ext = f".{parts[1]}"

        try:
            async with client.stream("GET", sign.url) as res:
                if res.status_code < 200 or res.status_code >= 300:
                    raise RagentOssError(
                        f"Download failed: {res.status_code}",
                        res.status_code,
                        None,
                    )
                tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
                try:
                    async for chunk in res.aiter_bytes():
                        tmp.write(chunk)
                finally:
                    tmp.close()
        except httpx.HTTPError as exc:
            raise NetworkError("Download failed", exc) from exc

        return Path(tmp.name)

    async def _get(self, url: str) -> dict[str, Any]:
        client = self._get_client()
        try:
            res = await client.get(url)
        except httpx.HTTPError as exc:
            raise NetworkError(f"Request to {url} failed", exc) from exc
        return self._handle_response(res)

    async def _post(self, url: str, body: dict[str, Any]) -> dict[str, Any]:
        client = self._get_client()
        try:
            res = await client.post(
                url,
                json=body,
                headers={"X-API-Key": self._api_key},
            )
        except httpx.HTTPError as exc:
            raise NetworkError(f"Request to {url} failed", exc) from exc
        return self._handle_response(res)

    @staticmethod
    def _handle_response(res: httpx.Response) -> dict[str, Any]:
        if 200 <= res.status_code < 300:
            return res.json()  # type: ignore[no-any-return]

        try:
            body = res.json()
        except Exception:
            body = None

        if res.status_code == 401:
            raise AuthenticationError(body)
        if res.status_code == 422:
            raise ValidationError(body)

        raise RagentOssError(f"Request failed: {res.status_code}", res.status_code, body)
