"""Synchronous HTTP client for the aqoon API."""

from __future__ import annotations

import httpx

from aqoon.exceptions import (
    AqoonError,
    AuthenticationError,
    ForbiddenError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
)
from aqoon.models import (
    APIKeyCreated,
    APIKeyInfo,
    Collection,
    Document,
    DocumentDetail,
    SearchResponse,
    UsageStats,
)

DEFAULT_BASE_URL = "https://aqoon.ai"
DEFAULT_TIMEOUT = 30.0


class Aqoon:
    """Synchronous client for the aqoon knowledge-as-a-service API.

    Usage::

        from aqoon import Aqoon

        client = Aqoon(api_key="aqn_your_key")
        results = client.search("machine learning basics")
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        if not api_key or not api_key.startswith("aqn_"):
            raise ValueError("api_key must start with 'aqn_'")

        self._http = httpx.Client(
            base_url=base_url.rstrip("/"),
            headers={
                "Authorization": f"Bearer {api_key}",
                "User-Agent": "aqoon-python/0.1.0",
            },
            timeout=timeout,
        )

    def close(self) -> None:
        """Close the underlying HTTP connection."""
        self._http.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    # --- Internal helpers ---

    def _handle_response(self, response: httpx.Response) -> dict:
        """Parse response JSON and raise typed exceptions on errors."""
        if response.is_success:
            return response.json()

        try:
            body = response.json()
        except Exception:
            body = {}

        message = body.get("error", body.get("detail", response.text))
        kwargs = {"status_code": response.status_code, "response": response}

        if response.status_code == 400:
            raise ValidationError(message, errors=body.get("fields"), **kwargs)
        if response.status_code == 401:
            raise AuthenticationError(message, **kwargs)
        if response.status_code == 403:
            raise ForbiddenError(message, **kwargs)
        if response.status_code == 404:
            raise NotFoundError(message, **kwargs)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            raise RateLimitError(
                message,
                retry_after=float(retry_after) if retry_after else None,
                **kwargs,
            )
        if response.status_code >= 500:
            raise ServerError(message, **kwargs)

        raise AqoonError(message, **kwargs)

    def _get(self, path: str, **params) -> dict:
        """Make a GET request and return parsed JSON."""
        resp = self._http.get(path, params=params or None)
        return self._handle_response(resp)

    def _post(self, path: str, json: dict | None = None) -> dict:
        """Make a POST request and return parsed JSON."""
        resp = self._http.post(path, json=json)
        return self._handle_response(resp)

    def _delete(self, path: str) -> dict:
        """Make a DELETE request and return parsed JSON."""
        resp = self._http.delete(path)
        return self._handle_response(resp)

    def _post_multipart(
        self,
        path: str,
        files: dict | None = None,
        data: dict | None = None,
    ) -> dict:
        """Make a multipart POST request and return parsed JSON."""
        resp = self._http.post(path, files=files, data=data or {})
        return self._handle_response(resp)

    def _paginate(self, path: str, **params) -> list[dict]:
        """Auto-paginate a list endpoint, returning all results."""
        results = []
        url: str | None = path
        while url:
            resp = self._http.get(url, params=params or None)
            data = self._handle_response(resp)
            results.extend(data.get("results", []))
            url = data.get("next")
            params = {}  # next URL includes params
        return results

    # --- Collections ---

    def list_collections(self) -> list[Collection]:
        """List all visible collections (owned + subscribed)."""
        return self._paginate("/api/v1/collections/")

    def get_collection(self, uuid: str) -> Collection:
        """Get a collection by UUID."""
        return self._get(f"/api/v1/collections/{uuid}/")

    def create_collection(self, name: str, *, description: str = "") -> Collection:
        """Create a new collection."""
        return self._post("/api/v1/collections/", json={"name": name, "description": description})

    def update_collection(self, uuid: str, *, name: str | None = None, description: str | None = None) -> Collection:
        """Update a collection's name and/or description."""
        payload: dict = {}
        if name is not None:
            payload["name"] = name
        if description is not None:
            payload["description"] = description
        resp = self._http.patch(f"/api/v1/collections/{uuid}/", json=payload)
        return self._handle_response(resp)

    def delete_collection(self, uuid: str) -> None:
        """Delete (soft-delete) a collection and all its documents."""
        self._delete(f"/api/v1/collections/{uuid}/")

    def subscribe(self, slug: str) -> dict:
        """Subscribe to a public collection by slug."""
        return self._post(f"/api/v1/collections/{slug}/subscribe/")

    def unsubscribe(self, slug: str) -> dict:
        """Unsubscribe from a collection by slug."""
        return self._delete(f"/api/v1/collections/{slug}/subscribe/")

    # --- Documents ---

    def list_documents(
        self,
        *,
        collection: str | None = None,
        tag: str | None = None,
    ) -> list[Document]:
        """List visible documents with optional filters."""
        params = {}
        if collection:
            params["collection"] = collection
        if tag:
            params["tag"] = tag
        return self._paginate("/api/v1/documents/", **params)

    def get_document(self, uuid: str) -> DocumentDetail:
        """Get document detail including content and chunks."""
        return self._get(f"/api/v1/documents/{uuid}/")

    def get_download_url(self, uuid: str) -> str:
        """Get a temporary download URL for the original document file.

        Returns a time-limited (1 hour) signed URL that can be used to
        download the original file. The URL is a redirect, so follow it
        or use the returned location.
        """
        resp = self._http.get(
            f"/api/v1/documents/{uuid}/download/",
            follow_redirects=False,
        )
        if resp.status_code == 302:
            return resp.headers["location"]
        self._handle_response(resp)
        return ""  # unreachable, _handle_response raises

    def replace_document(
        self,
        uuid: str,
        file_path: str,
        *,
        title: str | None = None,
        tags: str | None = None,
        source_type: str | None = None,
    ) -> Document:
        """Replace a document's file and re-process it.

        The document UUID is preserved. Old search entries are removed
        and the new file is extracted, chunked, and indexed.

        Args:
            uuid: UUID of the document to replace.
            file_path: Path to the new file.
            title: Optional title override (auto-detected from filename if omitted).
            tags: Optional comma-separated tags (keeps existing tags if omitted).
            source_type: Optional source type override.

        Returns:
            The updated document with status ``"pending"``.
        """
        import os

        data = {}
        if title:
            data["title"] = title
        if tags is not None:
            data["tags"] = tags
        if source_type:
            data["source_type"] = source_type

        filename = os.path.basename(file_path)
        with open(file_path, "rb") as f:
            return self._post_multipart(
                f"/api/v1/documents/{uuid}/replace/",
                files={"file": (filename, f)},
                data=data,
            )

    def upload_document(
        self,
        collection_uuid: str,
        file_path: str,
        *,
        title: str | None = None,
        tags: str | None = None,
        source_type: str | None = None,
    ) -> Document:
        """Upload a single document to a collection.

        Requires write permission on the collection.

        Args:
            collection_uuid: UUID of the target collection.
            file_path: Path to the file to upload.
            title: Optional title override (auto-detected from filename if omitted).
            tags: Optional comma-separated tags.
            source_type: Optional source type override (auto-detected from extension if omitted).

        Returns:
            The created document with status ``"pending"``.
        """
        import os

        data = {}
        if title:
            data["title"] = title
        if tags:
            data["tags"] = tags
        if source_type:
            data["source_type"] = source_type

        filename = os.path.basename(file_path)
        with open(file_path, "rb") as f:
            return self._post_multipart(
                f"/api/v1/collections/{collection_uuid}/documents/",
                files={"file": (filename, f)},
                data=data,
            )

    def upload_documents(
        self,
        collection_uuid: str,
        file_paths: list[str],
        *,
        tags: str | None = None,
    ) -> dict:
        """Upload multiple documents to a collection in a single request.

        Requires write permission on the collection.

        Args:
            collection_uuid: UUID of the target collection.
            file_paths: List of file paths to upload.
            tags: Optional comma-separated tags applied to all files.

        Returns:
            Dict with ``uploaded`` (list), ``skipped`` (list),
            ``total_uploaded`` (int), ``total_skipped`` (int).
        """
        import os

        data = {}
        if tags:
            data["tags"] = tags

        opened = [open(fp, "rb") for fp in file_paths]  # noqa: SIM115
        files = [("files", (os.path.basename(fp), fh)) for fp, fh in zip(file_paths, opened)]
        try:
            return self._post_multipart(
                f"/api/v1/collections/{collection_uuid}/documents/",
                files=files,
                data=data,
            )
        finally:
            for fh in opened:
                fh.close()

    # --- Search ---

    def search(
        self,
        query: str,
        *,
        collection: str | None = None,
        limit: int = 5,
    ) -> SearchResponse:
        """Hybrid text + vector search across collections."""
        payload: dict = {"query": query, "limit": limit}
        if collection:
            payload["collection"] = collection
        return self._post("/api/v1/search/", json=payload)

    # --- API Keys ---

    def list_keys(self) -> list[APIKeyInfo]:
        """List all API keys for the authenticated user."""
        return self._paginate("/api/v1/keys/")

    def create_key(
        self,
        name: str,
        *,
        is_full_access: bool = False,
        collections: list[str] | None = None,
    ) -> APIKeyCreated:
        """Create a new API key."""
        payload: dict = {"name": name, "is_full_access": is_full_access}
        if collections:
            payload["collections"] = collections
        return self._post("/api/v1/keys/", json=payload)

    def get_key(self, uuid: str) -> APIKeyInfo:
        """Get API key details."""
        return self._get(f"/api/v1/keys/{uuid}/")

    def revoke_key(self, uuid: str) -> dict:
        """Revoke (deactivate) an API key."""
        return self._delete(f"/api/v1/keys/{uuid}/")

    def rotate_key(self, uuid: str) -> APIKeyCreated:
        """Rotate an API key (revokes old, creates new with same config)."""
        return self._post(f"/api/v1/keys/{uuid}/rotate/")

    def list_key_collections(self, uuid: str) -> list[dict]:
        """List collection grants for an API key."""
        return self._get(f"/api/v1/keys/{uuid}/collections/")

    def grant_collection(
        self,
        key_uuid: str,
        slug: str,
        *,
        permission: str = "read",
    ) -> dict:
        """Grant an API key access to a collection.

        Args:
            key_uuid: UUID of the API key.
            slug: Collection slug.
            permission: ``"read"`` (search only) or ``"write"`` (search and upload).
        """
        return self._post(
            f"/api/v1/keys/{key_uuid}/collections/",
            json={"collection": slug, "permission": permission},
        )

    def revoke_collection(self, key_uuid: str, slug: str) -> dict:
        """Revoke an API key's access to a collection."""
        return self._delete(f"/api/v1/keys/{key_uuid}/collections/{slug}/")

    # --- Usage ---

    def key_usage(self, uuid: str, *, days: int = 30) -> UsageStats:
        """Get usage stats for a specific API key."""
        return self._get(f"/api/v1/keys/{uuid}/usage/", days=days)

    def usage(self, *, days: int = 30) -> UsageStats:
        """Get aggregate usage stats across all keys."""
        return self._get("/api/v1/keys/usage/", days=days)

    # --- Webhooks ---

    def get_webhook(self) -> dict:
        """Get the current webhook configuration."""
        return self._get("/api/v1/webhooks/")

    def set_webhook(self, url: str) -> dict:
        """Create or update the webhook endpoint.

        On create, the response includes a ``secret`` field (shown once).
        On update, only the URL is changed.
        """
        return self._post("/api/v1/webhooks/", json={"url": url})

    def delete_webhook(self) -> dict:
        """Remove the webhook endpoint."""
        return self._delete("/api/v1/webhooks/")

    def test_webhook(self) -> dict:
        """Send a test event to the configured webhook URL."""
        return self._post("/api/v1/webhooks/test/")
