"""Sub-namespace for async records API operations on a Pinecone index.

This module provides the async ``AsyncRecords`` class that exposes methods
for upserting and searching records using integrated inference.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import msgspec
import orjson


if TYPE_CHECKING:
    from pinecone.models.batch import BatchResult

from pinecone._internal.constants import DATA_PLANE_HEADERS, NDJSON_HEADERS
from pinecone._internal.validation import require_in_range, require_non_empty
from pinecone.models.records.search import SearchRecordsResponse


# Module-level decoder for search responses
_SEARCH_DECODER = msgspec.json.Decoder(SearchRecordsResponse)


class AsyncRecords:
    """Async sub-namespace for records API operations on a Pinecone index.

    Access via the ``records`` property on an AsyncIndex instance::

        async with AsyncIndex(host="...", api_key="...") as index:
            results = await index.records.search(
                namespace="ns",
                top_k=10,
                inputs={"text": "hello"},
            )

    Args:
        http: Async HTTP client instance for making requests.
        base_url: Base URL of the index (e.g., ``"https://my-index.svc.pinecone.io"``).
    """

    def __init__(self, *, http: Any, base_url: str) -> None:
        """Initialize AsyncRecords namespace.

        Args:
            http: Async HTTP client instance for making requests.
            base_url: Base URL of the index.
        """
        self._http = http
        self._base_url = base_url

    async def _make_data_plane_request(
        self,
        method: str,
        path: str,
        *,
        body: dict[str, Any] | None = None,
    ) -> bytes:
        """Make an async HTTP request to the data plane API.

        Args:
            method: HTTP method (GET or POST).
            path: API path relative to base_url.
            body: Optional request body to serialize as JSON.

        Returns:
            Raw response content as bytes.
        """
        url = f"{self._base_url}{path}"
        content = orjson.dumps(body) if body is not None else None

        if method == "GET":
            response = await self._http.get(url, headers=DATA_PLANE_HEADERS)
        else:
            response = await self._http.post(url, headers=DATA_PLANE_HEADERS, content=content)

        return response.content  # type: ignore[no-any-return]

    async def _make_ndjson_request(
        self,
        path: str,
        *,
        records: list[dict[str, Any]],
    ) -> None:
        """Send records as newline-delimited JSON.

        Args:
            path: API path relative to base_url.
            records: Records to send as NDJSON.
        """
        url = f"{self._base_url}{path}"
        content = b"\n".join(orjson.dumps(r) for r in records)
        await self._http.post(url, content=content, headers=NDJSON_HEADERS)

    async def upsert(
        self,
        *,
        namespace: str,
        records: list[dict[str, Any]],
    ) -> None:
        """Upsert records into a namespace.

        Sends records as newline-delimited JSON to the records upsert endpoint.
        Each record must contain an ``_id`` (or ``id``) field. Additional fields
        matching the index's ``field_map`` configuration are used for embedding;
        all other fields are stored as metadata.

        Args:
            namespace (str): Target namespace for the upsert.
            records (list[dict[str, Any]]): Records to upsert. Each must include
                an ``_id`` field.

        Returns:
            None

        Raises:
            ValueError: If namespace or records is empty.
            PineconeApiException: If the API request fails.

        Examples:
            >>> await index.records.upsert(
            ...     namespace="my-ns",
            ...     records=[
            ...         {"_id": "rec1", "text": "hello world", "category": "greeting"},
            ...         {"_id": "rec2", "text": "goodbye", "category": "farewell"},
            ...     ],
            ... )
        """
        require_non_empty("namespace", namespace)
        require_non_empty("records", records)

        await self._make_ndjson_request(
            f"/records/namespaces/{namespace}/upsert",
            records=records,
        )

    async def batch_upsert(
        self,
        *,
        namespace: str,
        records: list[dict[str, Any]],
        batch_size: int = 100,
        max_concurrency: int = 4,
        show_progress: bool = True,
    ) -> BatchResult:
        """Upsert records in concurrent batches.

        Splits *records* into chunks of *batch_size* and upserts them
        concurrently.  Returns a summary with success/failure counts and
        the failed items for easy retry.

        Individual batch failures are captured in the result rather than
        raised, so a single bad batch does not abort the entire operation.

        Args:
            namespace (str): Target namespace.
            records (list[dict[str, Any]]): Records to upsert. Each must
                include an ``_id`` field.
            batch_size (int): Maximum records per batch request (>= 1).
            max_concurrency (int): Maximum concurrent batch requests (1-64).
            show_progress (bool): Show a tqdm progress bar (if installed).

        Returns:
            BatchResult with counts and any errors for retry.

        Raises:
            ValueError: If *batch_size* or *max_concurrency* is out of range.

        Examples:
            >>> result = await index.records.batch_upsert(
            ...     namespace="my-ns",
            ...     records=[{"_id": f"rec{i}", "text": f"text {i}"} for i in range(10_000)],
            ... )
            >>> print(f"Upserted {result.successful_item_count}/{result.total_item_count}")

            Retry failed items:

            >>> if result.has_errors:
            ...     retry = await index.records.batch_upsert(
            ...         namespace="my-ns", records=result.failed_items
            ...     )
        """
        from pinecone._internal.batch import async_batch_execute

        async def _upsert_batch(batch: list[dict[str, Any]]) -> None:
            await self.upsert(namespace=namespace, records=batch)

        return await async_batch_execute(
            items=records,
            operation=_upsert_batch,
            batch_size=batch_size,
            max_concurrency=max_concurrency,
            show_progress=show_progress,
            desc="Upserting records",
        )

    async def search(
        self,
        *,
        namespace: str,
        top_k: int,
        inputs: dict[str, str] | None = None,
        vector: list[float] | None = None,
        id: str | None = None,  # noqa: A002
        filter: dict[str, Any] | None = None,  # noqa: A002
        fields: list[str] | None = None,
        rerank: dict[str, Any] | None = None,
    ) -> SearchRecordsResponse:
        """Search records in a namespace.

        Search with text (for indexes with integrated embedding), a query
        vector, or an existing record ID. Optionally rerank results.

        Args:
            namespace (str): Target namespace to search.
            top_k (int): Number of results to return (1-10000).
            inputs (dict[str, str] | None): Text inputs for integrated embedding
                search, e.g., ``{"text": "search query"}``.
            vector (list[float] | None): Query vector values for direct
                vector search.
            id (str | None): ID of an existing record to use as the query.
            filter (dict[str, Any] | None): Metadata filter to narrow results.
            fields (list[str] | None): Fields to return in results. If not
                specified, all fields are returned.
            rerank (dict[str, Any] | None): Reranking configuration with
                ``model`` (str, required), ``rank_fields`` (list[str], required),
                and optionally ``top_n`` (int), ``query`` (str), and
                ``parameters`` (dict).

        Returns:
            SearchRecordsResponse containing hits and usage information.

        Raises:
            ValueError: If namespace is empty or top_k is out of range.
            PineconeApiException: If the API request fails.

        Examples:
            Search with text (integrated embedding):

            >>> response = await index.records.search(
            ...     namespace="my-ns",
            ...     top_k=10,
            ...     inputs={"text": "search query"},
            ... )
            >>> for hit in response.hits:
            ...     print(f"{hit.id}: {hit.score}")

            Search with a vector:

            >>> response = await index.records.search(
            ...     namespace="my-ns",
            ...     top_k=5,
            ...     vector=[0.1, 0.2, 0.3],
            ... )

            Search with reranking:

            >>> response = await index.records.search(
            ...     namespace="my-ns",
            ...     top_k=100,
            ...     inputs={"text": "query"},
            ...     rerank={"model": "bge-reranker-v2-m3", "rank_fields": ["text"], "top_n": 10},
            ... )
        """
        require_non_empty("namespace", namespace)
        require_in_range("top_k", top_k, 1, 10000)

        query: dict[str, Any] = {"top_k": top_k}
        if inputs is not None:
            query["inputs"] = inputs
        if vector is not None:
            query["vector"] = vector
        if id is not None:
            query["id"] = id
        if filter is not None:
            query["filter"] = filter

        body: dict[str, Any] = {"query": query}
        if fields is not None:
            body["fields"] = fields
        if rerank is not None:
            body["rerank"] = rerank

        data = await self._make_data_plane_request(
            "POST",
            f"/records/namespaces/{namespace}/search",
            body=body,
        )
        return _SEARCH_DECODER.decode(data)

    def __repr__(self) -> str:
        """Return string representation."""
        return f"AsyncRecords(base_url={self._base_url!r})"
