"""In-memory search backend for testing and development."""

from __future__ import annotations

import time
from collections import defaultdict
from typing import Any

from search.backends.base import SearchBackend
from search.models import (
    IndexConfig,
    SearchDocument,
    SearchQuery,
    SearchResponse,
    SearchResult,
)


class MemoryBackend(SearchBackend):
    """Simple in-memory backend using substring matching with basic scoring.

    Useful for unit tests and local development without external dependencies.
    Documents are stored in plain dicts keyed by ``(index_name, doc_id)``.
    """

    def __init__(self) -> None:
        self._indexes: dict[str, IndexConfig] = {}
        self._documents: dict[str, dict[str, SearchDocument]] = defaultdict(dict)

    async def create_index(self, config: IndexConfig) -> None:
        self._indexes[config.name] = config
        if config.name not in self._documents:
            self._documents[config.name] = {}

    async def delete_index(self, index_name: str) -> None:
        self._indexes.pop(index_name, None)
        self._documents.pop(index_name, None)

    async def index(self, document: SearchDocument) -> None:
        if document.index_name not in self._documents:
            self._documents[document.index_name] = {}
        self._documents[document.index_name][document.id] = document

    async def bulk_index(self, documents: list[SearchDocument]) -> int:
        for doc in documents:
            await self.index(doc)
        return len(documents)

    async def delete(self, index_name: str, document_id: str) -> bool:
        store = self._documents.get(index_name, {})
        if document_id in store:
            del store[document_id]
            return True
        return False

    async def search(self, index_name: str, query: SearchQuery) -> SearchResponse:
        start = time.monotonic()
        store = self._documents.get(index_name, {})
        config = self._indexes.get(index_name)
        searchable = config.searchable_fields if config else None

        scored: list[tuple[float, SearchDocument]] = []
        for doc in store.values():
            if not self._matches_filters(doc.content, query.filters):
                continue
            score = self._score(doc.content, query.query, searchable)
            if score > 0 or not query.query:
                scored.append((score, doc))

        scored.sort(key=lambda t: t[0], reverse=True)

        if query.sort_by:
            desc = query.sort_by.startswith("-")
            field = query.sort_by.lstrip("-")
            scored.sort(
                key=lambda t: t[1].content.get(field, ""),
                reverse=desc,
            )

        total = len(scored)
        start_idx = (query.page - 1) * query.per_page
        page_items = scored[start_idx : start_idx + query.per_page]

        results: list[SearchResult] = []
        for score, doc in page_items:
            highlights = self._highlights(doc.content, query.query) if query.highlight else {}
            results.append(
                SearchResult(
                    id=doc.id,
                    score=score,
                    highlights=highlights,
                    document=doc.content,
                )
            )

        matched_docs = [doc for _, doc in scored]
        facets = self._compute_facets(matched_docs, query.facets)
        elapsed = (time.monotonic() - start) * 1000

        return SearchResponse(
            results=results,
            total=total,
            facets=facets,
            query_time_ms=round(elapsed, 2),
            page=query.page,
            per_page=query.per_page,
        )

    # -- private helpers -------------------------------------------------------

    @staticmethod
    def _matches_filters(content: dict[str, Any], filters: dict[str, Any]) -> bool:
        for key, value in filters.items():
            doc_value = content.get(key)
            if isinstance(value, list):
                if doc_value not in value:
                    return False
            elif doc_value != value:
                return False
        return True

    @staticmethod
    def _score(
        content: dict[str, Any],
        query: str,
        searchable_fields: list[str] | None,
    ) -> float:
        if not query:
            return 1.0
        query_lower = query.lower()
        terms = query_lower.split()
        score = 0.0
        for field, value in content.items():
            if searchable_fields and field not in searchable_fields:
                continue
            text = str(value).lower()
            for term in terms:
                if term in text:
                    score += 1.0
                    if text == term:
                        score += 2.0
                    elif text.startswith(term):
                        score += 1.0
        return score

    @staticmethod
    def _highlights(content: dict[str, Any], query: str) -> dict[str, str]:
        if not query:
            return {}
        highlights: dict[str, str] = {}
        query_lower = query.lower()
        terms = query_lower.split()
        for field, value in content.items():
            text = str(value)
            text_lower = text.lower()
            for term in terms:
                idx = text_lower.find(term)
                if idx != -1:
                    matched = text[idx : idx + len(term)]
                    highlights[field] = text.replace(matched, f"<em>{matched}</em>")
                    break
        return highlights

    @staticmethod
    def _compute_facets(
        documents: list[SearchDocument],
        facet_fields: list[str],
    ) -> dict[str, dict[str, int]]:
        if not facet_fields:
            return {}
        facets: dict[str, dict[str, int]] = {f: defaultdict(int) for f in facet_fields}
        for doc in documents:
            for field in facet_fields:
                val = doc.content.get(field)
                if val is not None:
                    facets[field][str(val)] += 1
        return {k: dict(v) for k, v in facets.items()}
