from __future__ import annotations

from enum import StrEnum
from typing import Protocol

from pydantic import BaseModel, ConfigDict

from app.case_epicrisis.domain.curation import CatalogReference, CodeSystem


class CatalogSearchRegistry(Protocol):
    def reference(self, system: CodeSystem) -> CatalogReference: ...

    def search(self, system: CodeSystem, query: str, limit: int = 20) -> list[dict[str, str]]: ...

    def semantic_search(self, system: CodeSystem, query: str, limit: int = 20) -> list[dict[str, str]]: ...

    def is_concept_query(self, system: CodeSystem, query: str) -> bool: ...


class SearchMode(StrEnum):
    LEXICAL = "lexical"
    SEMANTIC = "semantic"
    NONE = "none"


class SemanticFallback(StrEnum):
    NOT_NEEDED = "not_needed"
    USED = "used"
    UNAVAILABLE = "unavailable"


class MatchSource(StrEnum):
    EXACT_CODE = "exact_code"
    CODE_PREFIX = "code_prefix"
    CONCEPT = "concept"
    SEMANTIC = "semantic"


class CatalogSearchResult(BaseModel):
    model_config = ConfigDict(extra="forbid")

    code: str
    concept: str
    system: CodeSystem
    reference: CatalogReference
    match_source: MatchSource


class CatalogSearchResponse(BaseModel):
    model_config = ConfigDict(extra="forbid")

    mode: SearchMode
    semantic_fallback: SemanticFallback
    results: list[CatalogSearchResult]


class _CatalogSearcher:
    system: CodeSystem

    def __init__(self, registry: CatalogSearchRegistry) -> None:
        self.registry = registry

    def search(self, query: str, *, limit: int = 20) -> list[CatalogSearchResult]:
        return self.search_with_metadata(query, limit=limit).results

    def search_with_metadata(self, query: str, *, limit: int = 20) -> CatalogSearchResponse:
        clean_query = str(query or "").strip()
        if not clean_query:
            return CatalogSearchResponse(
                mode=SearchMode.NONE,
                semantic_fallback=SemanticFallback.NOT_NEEDED,
                results=[],
            )
        reference = self.registry.reference(self.system)
        if not reference.available:
            return CatalogSearchResponse(
                mode=SearchMode.NONE,
                semantic_fallback=SemanticFallback.UNAVAILABLE,
                results=[],
            )
        lexical = self.registry.search(self.system, clean_query, limit=limit)
        if lexical:
            return CatalogSearchResponse(
                mode=SearchMode.LEXICAL,
                semantic_fallback=SemanticFallback.NOT_NEEDED,
                results=self._to_results(lexical, reference),
            )
        if not self.registry.is_concept_query(self.system, clean_query):
            return CatalogSearchResponse(
                mode=SearchMode.NONE,
                semantic_fallback=SemanticFallback.NOT_NEEDED,
                results=[],
            )
        try:
            semantic = self.registry.semantic_search(self.system, clean_query, limit=limit)
        except (ImportError, OSError, RuntimeError, ValueError):
            return CatalogSearchResponse(
                mode=SearchMode.NONE,
                semantic_fallback=SemanticFallback.UNAVAILABLE,
                results=[],
            )
        return CatalogSearchResponse(
            mode=SearchMode.SEMANTIC if semantic else SearchMode.NONE,
            semantic_fallback=SemanticFallback.USED,
            results=self._to_results(semantic, reference),
        )

    def _to_results(
        self,
        items: list[dict[str, str]],
        reference: CatalogReference,
    ) -> list[CatalogSearchResult]:
        return [
            CatalogSearchResult(
                code=str(item.get("code") or ""),
                concept=str(item.get("concept") or ""),
                system=self.system,
                reference=reference,
                match_source=MatchSource(item.get("match_source") or MatchSource.CONCEPT),
            )
            for item in items
            if str(item.get("code") or "").strip() and str(item.get("concept") or "").strip()
        ]


class Cie10CatalogSearcher(_CatalogSearcher):
    system = CodeSystem.CIE10


class CupsCatalogSearcher(_CatalogSearcher):
    system = CodeSystem.CUPS


class CodingCatalogSearch:
    """Facade that keeps the CIE-10 and CUPS search contracts independent."""

    def __init__(self, registry: CatalogSearchRegistry) -> None:
        self.cie10 = Cie10CatalogSearcher(registry)
        self.cups = CupsCatalogSearcher(registry)
