"""Search provider abstraction.

Modules call :func:`run_search` / :func:`index_document` / :func:`remove_document`
and the active :class:`SearchProvider` (set via ``settings.SEARCH_PROVIDER``)
decides how to satisfy them. The default :class:`InMemorySearchProvider`
adapts the existing in-process registry so behaviour is unchanged until a
real backend (Meilisearch, OpenSearch, …) is plugged in.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable

from django.conf import settings
from django.utils.module_loading import import_string


@dataclass(frozen=True)
class SearchHit:
    entity_type: str
    entity_id: str
    title: str
    snippet: str = ""
    score: float = 0.0
    extra: dict[str, Any] = field(default_factory=dict)


@runtime_checkable
class SearchProvider(Protocol):
    """A pluggable indexing + search backend."""

    def index(self, entity_type: str, entity_id: str, document: dict[str, Any]) -> None: ...

    def remove(self, entity_type: str, entity_id: str) -> None: ...

    def search(
        self,
        query: str,
        *,
        entity_types: tuple[str, ...] = (),
        tenant_id: int | None = None,
        limit: int = 20,
    ) -> list[SearchHit]: ...


class InMemorySearchProvider:
    """Trivial substring-match provider used as the default.

    Storage layout: ``{(tenant_id, entity_type, entity_id): document}``.
    Documents must include at minimum ``{"title": str, "body": str}``.
    """

    def __init__(self) -> None:
        self._docs: dict[tuple[int | None, str, str], dict[str, Any]] = {}

    def index(self, entity_type: str, entity_id: str, document: dict[str, Any]) -> None:
        tenant_id = document.get("tenant_id")
        self._docs[(tenant_id, entity_type, str(entity_id))] = dict(document)

    def remove(self, entity_type: str, entity_id: str) -> None:
        for key in list(self._docs.keys()):
            _tid, et, eid = key
            if et == entity_type and eid == str(entity_id):
                self._docs.pop(key, None)

    def search(
        self,
        query: str,
        *,
        entity_types: tuple[str, ...] = (),
        tenant_id: int | None = None,
        limit: int = 20,
    ) -> list[SearchHit]:
        if not query:
            return []
        needle = query.lower()
        hits: list[SearchHit] = []
        for (tid, et, eid), doc in self._docs.items():
            if tenant_id is not None and tid is not None and tid != tenant_id:
                continue
            if entity_types and et not in entity_types:
                continue
            title = str(doc.get("title", ""))
            body = str(doc.get("body", ""))
            haystack = f"{title}\n{body}".lower()
            if needle not in haystack:
                continue
            score = haystack.count(needle)
            snippet = body[:120]
            hits.append(
                SearchHit(
                    entity_type=et,
                    entity_id=eid,
                    title=title,
                    snippet=snippet,
                    score=score,
                    extra={k: v for k, v in doc.items() if k not in {"title", "body"}},
                )
            )
        hits.sort(key=lambda h: h.score, reverse=True)
        return hits[:limit]

    def reset(self) -> None:
        self._docs.clear()


_provider: SearchProvider | None = None


def get_provider() -> SearchProvider:
    global _provider
    if _provider is None:
        dotted = getattr(
            settings,
            "SEARCH_PROVIDER",
            "simorgh.core.search_provider.InMemorySearchProvider",
        )
        cls = import_string(dotted)
        _provider = cls()
    return _provider


def set_provider(provider: SearchProvider | None) -> None:
    global _provider
    _provider = provider


# Convenience top-level functions ------------------------------------------------


def index_document(entity_type: str, entity_id: str, document: dict[str, Any]) -> None:
    get_provider().index(entity_type, entity_id, document)


def remove_document(entity_type: str, entity_id: str) -> None:
    get_provider().remove(entity_type, entity_id)


def run_search(
    query: str,
    *,
    entity_types: tuple[str, ...] = (),
    tenant_id: int | None = None,
    limit: int = 20,
) -> list[SearchHit]:
    return get_provider().search(
        query,
        entity_types=entity_types,
        tenant_id=tenant_id,
        limit=limit,
    )


__all__ = [
    "InMemorySearchProvider",
    "SearchHit",
    "SearchProvider",
    "get_provider",
    "index_document",
    "remove_document",
    "run_search",
    "set_provider",
]
