"""Normalised response types for the icv-search abstraction layer."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass
class TaskResult:
    """Result of an asynchronous engine operation (index creation, document add, etc.)."""

    task_uid: str = ""
    status: str = ""
    detail: str = ""
    raw: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def from_engine(cls, data: dict[str, Any]) -> TaskResult:
        """Create from a raw engine response dict.

        Accepts the task UID under any of the following keys (in priority order):
        ``taskUid`` (Meilisearch async operations), ``task_uid``, or ``uid``
        (task-status responses from Postgres and DummyBackend ``get_task``).
        """
        task_uid = str(data.get("taskUid", data.get("task_uid", data.get("uid", ""))))
        return cls(
            task_uid=task_uid,
            status=str(data.get("status", "")),
            detail=str(data.get("type", "")),
            raw=data,
        )


@dataclass
class SearchResult:
    """Result of a search query."""

    hits: list[dict[str, Any]] = field(default_factory=list)
    query: str = ""
    processing_time_ms: int = 0
    estimated_total_hits: int = 0
    limit: int = 20
    offset: int = 0
    facet_distribution: dict[str, dict[str, int]] = field(default_factory=dict)
    formatted_hits: list[dict[str, Any]] = field(default_factory=list)
    ranking_scores: list[float | None] = field(default_factory=list)
    raw: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def from_engine(cls, data: dict[str, Any]) -> SearchResult:
        """Create from a raw engine response dict.

        Extracts ``_formatted`` from each Meilisearch hit (populated when
        ``attributesToHighlight`` is requested) and stores the results in
        ``formatted_hits``. For other backends, a top-level ``formatted_hits``
        key is accepted directly.

        Ranking scores are extracted from:

        - ``_rankingScore`` on each hit (Meilisearch v1.3+ when
          ``showRankingScore: true`` is passed to the search request).
        - A top-level ``ranking_scores`` list (Postgres and Dummy backends).

        When neither source is present the list is empty.
        """
        raw_hits: list[dict[str, Any]] = data.get("hits", [])

        # Meilisearch embeds highlighted versions as a ``_formatted`` key on
        # each hit.  Extract those into a parallel list.
        meili_formatted = [hit["_formatted"] for hit in raw_hits if "_formatted" in hit]

        # Backends that build highlighted hits server-side (Postgres, Dummy)
        # return them under a top-level ``formatted_hits`` key.
        formatted_hits: list[dict[str, Any]] = meili_formatted or data.get("formatted_hits", [])

        # Extract ranking scores.  Meilisearch embeds ``_rankingScore`` on each
        # hit; other backends may supply a top-level list.
        meili_scores: list[float | None] = [
            hit.get("_rankingScore") for hit in raw_hits if "_rankingScore" in hit
        ]
        ranking_scores: list[float | None] = meili_scores if meili_scores else data.get("ranking_scores", [])

        # Strip internal Meilisearch keys from the plain hits to keep them clean.
        # ``_geoDistance`` is intentionally preserved — Meilisearch adds this
        # automatically when geo search is used and it is useful to callers.
        clean_hits = [
            {k: v for k, v in hit.items() if k not in ("_formatted", "_rankingScore")}
            for hit in raw_hits
        ]

        return cls(
            hits=clean_hits,
            query=str(data.get("query", "")),
            processing_time_ms=int(data.get("processingTimeMs", data.get("processing_time_ms", 0))),
            estimated_total_hits=int(data.get("estimatedTotalHits", data.get("estimated_total_hits", 0))),
            limit=int(data.get("limit", 20)),
            offset=int(data.get("offset", 0)),
            facet_distribution=data.get("facetDistribution", data.get("facet_distribution", {})),
            formatted_hits=formatted_hits,
            ranking_scores=ranking_scores,
            raw=data,
        )

    def get_highlighted_hits(self) -> list[dict[str, Any]]:
        """Return highlighted hits when available, falling back to plain hits.

        Use this in templates and views instead of accessing ``hits`` directly
        when highlighting may have been requested — the method transparently
        returns whichever version is populated.
        """
        return self.formatted_hits if self.formatted_hits else self.hits

    def get_hit_with_score(self, index: int) -> tuple[dict[str, Any], float | None]:
        """Return the hit at ``index`` paired with its ranking score (or ``None``).

        Example::

            result = search("products", "chair")
            hit, score = result.get_hit_with_score(0)
            print(hit["name"], score)  # "Office Chair", 0.9872

        Args:
            index: Zero-based position in the ``hits`` list.

        Returns:
            A ``(hit, score)`` tuple.  ``score`` is ``None`` when ranking
            scores were not requested or are not available for that hit.
        """
        hit = self.hits[index]
        score: float | None = self.ranking_scores[index] if index < len(self.ranking_scores) else None
        return hit, score

    def get_facet_values(self, facet_name: str) -> list[dict[str, Any]]:
        """Return facet values sorted by count descending.

        Returns a list of dicts with 'name' and 'count' keys, e.g.::

            [{'name': 'Nike', 'count': 42}, {'name': 'Adidas', 'count': 31}]
        """
        distribution = self.facet_distribution.get(facet_name, {})
        return sorted(
            [{"name": name, "count": count} for name, count in distribution.items()],
            key=lambda x: x["count"],
            reverse=True,
        )


@dataclass
class IndexStats:
    """Statistics for a search index."""

    document_count: int = 0
    is_indexing: bool = False
    field_distribution: dict[str, int] = field(default_factory=dict)
    raw: dict[str, Any] = field(default_factory=dict)

    @classmethod
    def from_engine(cls, data: dict[str, Any]) -> IndexStats:
        """Create from a raw engine response dict."""
        return cls(
            document_count=int(data.get("numberOfDocuments", data.get("document_count", 0))),
            is_indexing=bool(data.get("isIndexing", data.get("is_indexing", False))),
            field_distribution=data.get("fieldDistribution", data.get("field_distribution", {})),
            raw=data,
        )
