"""Meilisearch backend using httpx."""

from __future__ import annotations

import logging
from typing import Any

import httpx

from icv_search.backends.base import BaseSearchBackend
from icv_search.backends.filters import translate_filter_to_meilisearch, translate_sort_to_meilisearch
from icv_search.exceptions import IndexNotFoundError, SearchBackendError, SearchTimeoutError

logger = logging.getLogger(__name__)


class MeilisearchBackend(BaseSearchBackend):
    """Search backend for Meilisearch using httpx.

    Uses httpx directly rather than the official meilisearch Python SDK
    for lighter dependencies and future async support.
    """

    def __init__(self, url: str, api_key: str, timeout: int = 30, **kwargs: Any) -> None:
        super().__init__(url=url.rstrip("/"), api_key=api_key, timeout=timeout, **kwargs)
        self._client = httpx.Client(
            base_url=self.url,
            headers=self._build_headers(),
            timeout=self.timeout,
        )

    def _build_headers(self) -> dict[str, str]:
        headers = {"Content-Type": "application/json"}
        if self.api_key:
            headers["Authorization"] = f"Bearer {self.api_key}"
        return headers

    def _request(self, method: str, path: str, **kwargs: Any) -> Any:
        """Execute an HTTP request with error handling."""
        try:
            response = self._client.request(method, path, **kwargs)
        except httpx.TimeoutException as exc:
            raise SearchTimeoutError(f"Meilisearch request timed out: {method} {path}", exc) from exc
        except httpx.HTTPError as exc:
            raise SearchBackendError(f"Meilisearch request failed: {method} {path} — {exc}", exc) from exc

        if response.status_code == 404:
            raise IndexNotFoundError(f"Index not found: {path}")
        if response.status_code >= 400:
            detail = response.text
            raise SearchBackendError(f"Meilisearch error {response.status_code}: {detail}")

        if response.status_code == 204:
            return {}
        return response.json()

    def create_index(self, uid: str, primary_key: str = "id") -> dict[str, Any]:
        return self._request("POST", "/indexes", json={"uid": uid, "primaryKey": primary_key})

    def delete_index(self, uid: str) -> None:
        self._request("DELETE", f"/indexes/{uid}")

    def update_settings(self, uid: str, settings: dict[str, Any]) -> dict[str, Any]:
        return self._request("PATCH", f"/indexes/{uid}/settings", json=settings)

    def get_settings(self, uid: str) -> dict[str, Any]:
        return self._request("GET", f"/indexes/{uid}/settings")

    def add_documents(self, uid: str, documents: list[dict[str, Any]], primary_key: str = "id") -> dict[str, Any]:
        return self._request(
            "POST",
            f"/indexes/{uid}/documents",
            json=documents,
            params={"primaryKey": primary_key},
        )

    def delete_documents(self, uid: str, document_ids: list[str]) -> dict[str, Any]:
        return self._request("POST", f"/indexes/{uid}/documents/delete-batch", json=document_ids)

    def clear_documents(self, uid: str) -> dict[str, Any]:
        """Remove all documents using Meilisearch's DELETE /indexes/{uid}/documents."""
        return self._request("DELETE", f"/indexes/{uid}/documents")

    def search(self, uid: str, query: str, **params: Any) -> dict[str, Any]:
        """Execute a search query against a Meilisearch index.

        Recognised params beyond the standard Meilisearch options:

        - ``highlight_fields`` (list[str]): Fields to highlight in results.
          Maps to ``attributesToHighlight``.
        - ``highlight_pre_tag`` (str): Opening tag for highlighted terms.
          Defaults to ``<mark>``.
        - ``highlight_post_tag`` (str): Closing tag for highlighted terms.
          Defaults to ``</mark>``.
        - ``show_ranking_score`` (bool): When ``True``, include each hit's
          relevance score as ``_rankingScore`` (Meilisearch v1.3+).  Maps to
          ``showRankingScore``.
        - ``matching_strategy`` (str): Controls how query terms are matched.
          One of ``"all"``, ``"last"``, or ``"frequency"``.  Maps to
          Meilisearch's ``matchingStrategy`` parameter.
        - ``geo_point`` (tuple[float, float]): ``(lat, lng)`` origin for
          geo-distance filtering and sorting.  Must be accompanied by
          ``geo_radius`` and/or ``geo_sort``.
        - ``geo_radius`` (int | None): Radius in metres.  When combined with
          ``geo_point`` a ``_geoRadius`` filter is appended to the existing
          filter expression.
        - ``geo_sort`` (str): ``"asc"`` or ``"desc"``.  When combined with
          ``geo_point`` a ``_geoPoint`` sort expression is prepended to the
          sort list so that results are ordered by distance from the point.
          Documents must have a ``_geo`` field (``{"lat": ..., "lng": ...}``)
          for Meilisearch to evaluate geo filters and sorts.
        """
        # Extract geo params before any other processing.
        geo_point: tuple[float, float] | None = params.pop("geo_point", None)
        geo_radius: int | None = params.pop("geo_radius", None)
        geo_sort: str | None = params.pop("geo_sort", None)

        # Translate Django-native filter dict to Meilisearch filter string
        if "filter" in params and isinstance(params["filter"], dict):
            params = {**params, "filter": translate_filter_to_meilisearch(params["filter"])}

        # Translate Django-native sort list (with - prefix) to Meilisearch sort format
        if "sort" in params and isinstance(params["sort"], list):
            params = {**params, "sort": translate_sort_to_meilisearch(params["sort"])}

        # Append geo radius filter when both geo_point and geo_radius are given.
        if geo_point is not None and geo_radius is not None:
            lat, lng = geo_point
            geo_filter = f"_geoRadius({lat}, {lng}, {geo_radius})"
            existing_filter: str = params.pop("filter", "") or ""
            if existing_filter:
                params["filter"] = f"{existing_filter} AND {geo_filter}"
            else:
                params["filter"] = geo_filter

        # Prepend geo distance sort when both geo_point and geo_sort are given.
        if geo_point is not None and geo_sort in ("asc", "desc"):
            lat, lng = geo_point
            geo_sort_expr = f"_geoPoint({lat}, {lng}):{geo_sort}"
            existing_sort: list[str] = params.pop("sort", []) or []
            params["sort"] = [geo_sort_expr, *existing_sort]

        # Extract highlighting params and map to Meilisearch naming convention.
        highlight_fields: list[str] | None = params.pop("highlight_fields", None)
        pre_tag: str = params.pop("highlight_pre_tag", "<mark>")
        post_tag: str = params.pop("highlight_post_tag", "</mark>")

        # Extract ranking score param and map to Meilisearch naming convention.
        show_ranking_score: bool = params.pop("show_ranking_score", False)

        # Extract matching strategy and map to Meilisearch naming convention.
        matching_strategy: str | None = params.pop("matching_strategy", None)

        body: dict[str, Any] = {"q": query, **params}

        if highlight_fields:
            body["attributesToHighlight"] = highlight_fields
            body["highlightPreTag"] = pre_tag
            body["highlightPostTag"] = post_tag

        if show_ranking_score:
            body["showRankingScore"] = True

        if matching_strategy is not None:
            body["matchingStrategy"] = matching_strategy

        return self._request("POST", f"/indexes/{uid}/search", json=body)

    def get_stats(self, uid: str) -> dict[str, Any]:
        return self._request("GET", f"/indexes/{uid}/stats")

    def health(self) -> bool:
        try:
            result = self._request("GET", "/health")
            return result.get("status") == "available"
        except (SearchBackendError, SearchTimeoutError):
            return False

    def get_task(self, task_uid: str) -> dict[str, Any]:
        return self._request("GET", f"/tasks/{task_uid}")

    def multi_search(self, queries: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """Execute multiple search queries via Meilisearch's POST /multi-search.

        Each query dict must contain ``uid`` (the Meilisearch index UID) and
        ``query`` (the search string). Optional keys: ``filter``, ``sort``,
        ``limit``, ``offset``, ``facets``, ``highlight_fields``.

        Returns:
            A list of raw Meilisearch search result dicts in query order.
        """
        searches: list[dict[str, Any]] = []
        for query in queries:
            uid = query["uid"]
            q = query.get("query", "")
            params = {k: v for k, v in query.items() if k not in ("uid", "query")}

            # Apply the same filter / sort translation as single search
            if "filter" in params and isinstance(params["filter"], dict):
                params = {**params, "filter": translate_filter_to_meilisearch(params["filter"])}
            if "sort" in params and isinstance(params["sort"], list):
                params = {**params, "sort": translate_sort_to_meilisearch(params["sort"])}

            # Map highlight_fields to attributesToHighlight
            highlight_fields: list[str] | None = params.pop("highlight_fields", None)
            pre_tag: str = params.pop("highlight_pre_tag", "<mark>")
            post_tag: str = params.pop("highlight_post_tag", "</mark>")

            entry: dict[str, Any] = {"indexUid": uid, "q": q, **params}
            if highlight_fields:
                entry["attributesToHighlight"] = highlight_fields
                entry["highlightPreTag"] = pre_tag
                entry["highlightPostTag"] = post_tag

            searches.append(entry)

        response = self._request("POST", "/multi-search", json={"queries": searches})
        return response.get("results", [])

    def swap_indexes(self, pairs: list[tuple[str, str]]) -> dict[str, Any]:
        """Atomically swap index names using Meilisearch's swap-indexes endpoint."""
        payload = [{"indexes": [a, b]} for a, b in pairs]
        return self._request("POST", "/swap-indexes", json=payload)
