"""Сопоставление извлечённых позиций с эталонным справочником услуг.

Каскад по убыванию надёжности:
  1. код тарификатора — точное совпадение, если клиника его указала;
  2. точное совпадение нормализованного названия;
  3. эмбеддинги (многоязычная модель) — семантическая близость названий;
  4. нечёткое сравнение (RapidFuzz) как запасной сигнал;
  5. иначе — очередь ручной разметки (unmatched).

В справочнике нет синонимов, поэтому шаги 1 и 3 несут основную нагрузку. Коды у
клиник встречаются с лишним хвостом («A02.020.000.2»), поэтому сравниваем по
канонической части кода тарификатора.
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2]))

from rapidfuzz import fuzz, process  # noqa: E402

from contracts.models import MatchResult  # noqa: E402
from etl.dictionary import Service, normalize_name  # noqa: E402

# Каноническая часть кода тарификатора: буква + три группы цифр.
_CODE_CORE_RE = re.compile(r"[A-ZА-Я]\d{2}\.\d{3}\.\d{3}")


class Matcher:
    """Готовит индексы справочника и сопоставляет с ним сырые названия услуг."""

    def __init__(
        self,
        services: list[Service],
        embedder=None,
        emb_threshold: float = 0.70,
        fuzzy_threshold: int = 88,
        dict_emb=None,
    ):
        self.services = services
        self.by_code = {s.tarificator_code: s for s in services if s.tarificator_code}
        self.by_norm: dict[str, Service] = {}
        for s in services:
            self.by_norm.setdefault(s.name_norm, s)
        self.names_norm = [s.name_norm for s in services]
        self.emb_threshold = emb_threshold
        self.fuzzy_threshold = fuzzy_threshold
        self.embedder = embedder
        # Готовые эмбеддинги справочника (dict_emb) переиспользуются между прогонами:
        # при догрузке нового прайса не нужно заново векторизовать тысячи услуг.
        self.dict_emb = dict_emb
        if embedder is not None and self.dict_emb is None:
            self.dict_emb = embedder.encode(
                [s.name_ru for s in services],
                normalize_embeddings=True,
                task_type="RETRIEVAL_DOCUMENT",
            )

    def _match_by_code(self, code: str | None) -> Service | None:
        if not code:
            return None
        m = _CODE_CORE_RE.search(code)
        return self.by_code.get(m.group(0)) if m else None

    def _match_by_fuzzy(self, name_norm: str) -> MatchResult:
        best = process.extractOne(name_norm, self.names_norm, scorer=fuzz.token_set_ratio)
        if best and best[1] >= self.fuzzy_threshold:
            return MatchResult(
                service_id=self.services[best[2]].service_id,
                method="fuzzy",
                confidence=best[1] / 100,
            )
        return MatchResult()

    def match_batch(self, names: list[str], codes: list[str | None]) -> list[MatchResult]:
        """Сопоставить пачку позиций. Эмбеддинги считаются разом — так быстрее."""
        results: list[MatchResult | None] = [None] * len(names)
        pending_idx: list[int] = []
        pending_norm: list[str] = []

        for i, (name, code) in enumerate(zip(names, codes, strict=True)):
            service = self._match_by_code(code)
            if service:
                results[i] = MatchResult(
                    service_id=service.service_id, method="code", confidence=1.0
                )
                continue
            name_norm = normalize_name(name)
            exact = self.by_norm.get(name_norm)
            if exact:
                results[i] = MatchResult(
                    service_id=exact.service_id, method="exact", confidence=1.0
                )
                continue
            pending_idx.append(i)
            pending_norm.append(name_norm)

        if pending_norm and self.embedder is not None and self.dict_emb is not None:
            # Дедуп: одинаковые названия эмбеддим один раз (экономит запросы и квоту API).
            pending_names = [names[i] for i in pending_idx]
            unique: dict[str, int] = {}
            for nm in pending_names:
                unique.setdefault(nm, len(unique))
            query = self.embedder.encode(
                list(unique), normalize_embeddings=True, task_type="RETRIEVAL_QUERY"
            )
            sims = query @ self.dict_emb.T  # косинус по нормированным векторам
            best_idx = sims.argmax(axis=1)
            best_score = sims.max(axis=1)
            for k, i in enumerate(pending_idx):
                u = unique[pending_names[k]]
                suggested = self.services[int(best_idx[u])].service_id
                score = float(best_score[u])
                if score >= self.emb_threshold:
                    results[i] = MatchResult(
                        service_id=suggested,
                        method="embedding",
                        confidence=score,
                        suggested_service_id=suggested,
                        suggested_score=score,
                    )
                else:
                    # Не дотянул до порога — в очередь, но кандидата сохраняем оператору.
                    fallback = self._match_by_fuzzy(pending_norm[k])
                    fallback.suggested_service_id = suggested
                    fallback.suggested_score = score
                    results[i] = fallback
        else:
            for k, i in enumerate(pending_idx):
                results[i] = self._match_by_fuzzy(pending_norm[k])

        return [r if r is not None else MatchResult() for r in results]
