"""HTTP client for the Chlo API."""

from typing import Any, Optional

import httpx


class ChloApiError(Exception):
    """Raised when the Chlo API returns an error."""

    def __init__(self, status_code: int, detail: str):
        self.status_code = status_code
        self.detail = detail
        super().__init__(f"HTTP {status_code}: {detail}")


class ChloApiClient:
    """Async HTTP client wrapping the Chlo /api/v2 endpoints."""

    def __init__(self, api_url: str, api_key: str):
        self._base = api_url.rstrip("/") + "/api/v2"
        self._client = httpx.AsyncClient(
            base_url=self._base,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=30.0,
            follow_redirects=True,
        )

    async def close(self) -> None:
        await self._client.aclose()

    # -- helpers --

    async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        # Ensure trailing slash to avoid 307 redirects that strip auth headers
        if not path.endswith("/"):
            path = path + "/"
        resp = await self._client.request(method, path, **kwargs)
        if resp.status_code == 401:
            raise ChloApiError(
                401,
                "Invalid or expired API key. Generate a new one at https://chlo.dev",
            )
        if resp.status_code >= 400:
            detail = resp.text
            try:
                detail = resp.json().get("detail", detail)
            except Exception:
                pass
            raise ChloApiError(resp.status_code, str(detail))
        if resp.status_code == 204:
            return None
        return resp.json()

    async def _get(self, path: str, **params: Any) -> Any:
        # Strip None values from params
        params = {k: v for k, v in params.items() if v is not None}
        return await self._request("GET", path, params=params)

    async def _post(self, path: str, json: Any = None, **kwargs: Any) -> Any:
        return await self._request("POST", path, json=json, **kwargs)

    # -- projects --

    async def list_projects(self) -> list[dict]:
        return await self._get("/projects")

    # -- context buckets --

    async def get_buckets(self) -> dict:
        return await self._get("/context/buckets")

    # -- search --

    async def search(
        self,
        project_id: str,
        query: str,
        search_type: Optional[str] = None,
        limit: Optional[int] = None,
        file_filter: Optional[str] = None,
    ) -> list[dict]:
        return await self._get(
            f"/projects/{project_id}/search",
            q=query,
            type=search_type,
            limit=limit,
            file_filter=file_filter,
        )

    # -- chunks --

    async def get_chunk(self, project_id: str, chunk_id: str) -> dict:
        return await self._get(f"/projects/{project_id}/chunks/{chunk_id}")

    async def get_similar_chunks(
        self,
        project_id: str,
        chunk_id: str,
        limit: Optional[int] = None,
    ) -> list[dict]:
        return await self._get(
            f"/projects/{project_id}/chunks/{chunk_id}/similar",
            limit=limit,
        )

    # -- metrics --

    async def get_metrics(self, project_id: str) -> dict:
        return await self._get(f"/projects/{project_id}/metrics")

    # -- analysis --

    async def get_duplicates(
        self,
        project_id: str,
        threshold: Optional[float] = None,
        limit: Optional[int] = None,
    ) -> dict:
        return await self._get(
            f"/projects/{project_id}/analysis/duplicates",
            threshold=threshold,
            limit=limit,
        )

    # -- activity --

    async def post_activity(
        self,
        project_id: str,
        event_type: str,
        file_path: str = "",
        summary: str = "",
        session_id: str = "",
        chunk_ids: list[str] | None = None,
        subgraph_title: str | None = None,
        bucket_name: str | None = None,
        plan_title: str | None = None,
        plan_content: str | None = None,
        notes: str | None = None,
    ) -> dict:
        body: dict = {
            "event_type": event_type,
            "file_path": file_path,
            "summary": summary,
            "session_id": session_id,
        }
        if chunk_ids is not None:
            body["chunk_ids"] = chunk_ids
        if subgraph_title is not None:
            body["subgraph_title"] = subgraph_title
        if bucket_name is not None:
            body["bucket_name"] = bucket_name
        if plan_title is not None:
            body["plan_title"] = plan_title
        if plan_content is not None:
            body["plan_content"] = plan_content
        if notes is not None:
            body["notes"] = notes
        return await self._post(
            f"/projects/{project_id}/activity",
            json=body,
        )
