from __future__ import annotations

import re
from dataclasses import dataclass


@dataclass(slots=True)
class TargetTermsAnalysis:
    target_terms: list[str]
    endpoint_paths: list[str]
    api_like_terms: list[str]
    matched_aliases: list[str]
    alias_docs: list[str]


@dataclass(frozen=True, slots=True)
class _AliasRule:
    phrases: tuple[str, ...]
    canonical_term: str
    target_doc_hint: str


class _AliasMatcher:
    _RULES = (
        _AliasRule(("ручная отправка сообщения", "отправка сообщения вручную"), "/send", "docs/api/send-message-endpoint.md"),
        _AliasRule(("статус сервиса", "проверка здоровья"), "/health", "docs/api/health-endpoint.md"),
        _AliasRule(("control actions", "управление runtime"), "/actions/{action}", "docs/api/control-actions-endpoint.md"),
        _AliasRule(("runtime health", "здоровье runtime", "статусы здоровья"), "runtime_health", "docs/domains/runtime-health-entity.md"),
        _AliasRule(("цикл отправки уведомлений", "notification loop", "worker loop"), "telegram-notify-loop", "docs/logic/telegram-notification-loop.md"),
        _AliasRule(("архитектура приложения",), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"),
        _AliasRule(("архитектура",), "architecture_overview", "docs/architecture/telegram-notify-app-overview.md"),
        _AliasRule(("каталог ошибок", "errors catalog"), "errors_catalog", "docs/errors/catalog.yaml"),
        _AliasRule(("файл-индекс документации", "docs index", "индекс документации"), "docs_index", "docs/README.md"),
    )

    def match(self, lowered_query: str) -> tuple[list[str], list[str], list[str]]:
        terms: list[str] = []
        docs: list[str] = []
        aliases: list[str] = []
        for rule in self._RULES:
            if any(phrase in lowered_query for phrase in rule.phrases):
                self._append_unique(terms, rule.canonical_term.lower())
                self._append_unique(docs, rule.target_doc_hint)
                self._append_unique(aliases, rule.canonical_term.lower())
        return terms, docs, aliases

    def _append_unique(self, items: list[str], value: str) -> None:
        if value and value not in items:
            items.append(value)


class _EndpointPathExtractor:
    _PATH_RE = re.compile(r"`([^`]+)`|(/[A-Za-z0-9_./{}-]+)")
    _VALID_ENDPOINT_RE = re.compile(r"^/[a-z0-9._/-]+(?:/\{[a-z0-9_]+\})?$")
    _DOC_EXTENSIONS = (".md", ".yaml", ".yml", ".json")

    def extract(self, query: str) -> list[str]:
        values: list[str] = []
        for match in self._PATH_RE.finditer(query):
            candidate = next((item for item in match.groups() if item and item.startswith("/")), "")
            normalized = self._normalize(candidate)
            if self._is_endpoint(normalized):
                self._append_unique(values, normalized)
        return values

    def _normalize(self, token: str) -> str:
        trimmed = str(token or "").strip().strip("`'\"()[]!?.,:;")
        if "{" in trimmed and "}" not in trimmed:
            return ""
        return trimmed.lower()

    def _is_endpoint(self, token: str) -> bool:
        if not token or not self._VALID_ENDPOINT_RE.fullmatch(token):
            return False
        return not token.endswith(self._DOC_EXTENSIONS)

    def _append_unique(self, items: list[str], value: str) -> None:
        if value and value not in items:
            items.append(value)


@dataclass(slots=True)
class _ApiLikeAnchorAnalysis:
    endpoint_paths: list[str]
    candidate_terms: list[str]


class _ApiLikeAnchorExtractor:
    _TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_./{}-]+")
    _ASCII_ENDPOINT_RE = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$")
    _API_MARKERS = {
        "api",
        "endpoint",
        "route",
        "method",
        "метод",
        "метода",
        "методу",
        "ручка",
        "ручки",
        "эндпоинт",
        "эндпоинта",
        "маршрут",
        "роут",
    }
    _EXPLAIN_MARKERS = {
        "как",
        "что",
        "делает",
        "работает",
        "объясни",
        "объяснить",
        "расскажи",
        "опиши",
        "смысл",
    }
    _NOISE_WORDS = _API_MARKERS | _EXPLAIN_MARKERS | {
        "про",
        "какой",
        "какая",
        "какие",
        "какого",
        "какую",
        "кратко",
        "нужен",
        "нужно",
        "у",
    }
    _SHORT_QUERY_TOKEN_LIMIT = 7

    def extract(self, query: str, explicit_endpoint_paths: list[str]) -> _ApiLikeAnchorAnalysis:
        if explicit_endpoint_paths:
            return _ApiLikeAnchorAnalysis(endpoint_paths=list(explicit_endpoint_paths), candidate_terms=[])
        token_entries = self._token_entries(query)
        if not token_entries:
            return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[])
        candidate_terms = [token for token, _start in token_entries if self._is_api_candidate(token)]
        if not candidate_terms:
            return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[])
        if self._has_api_marker(token_entries):
            primary = self._primary_candidate(token_entries)
            endpoint_paths = [self._ensure_endpoint(primary)] if primary else []
            return _ApiLikeAnchorAnalysis(
                endpoint_paths=[path for path in endpoint_paths if path],
                candidate_terms=[primary] if primary else [],
            )
        if self._is_short_explain_query(token_entries) and len(candidate_terms) == 1:
            return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=list(candidate_terms))
        return _ApiLikeAnchorAnalysis(endpoint_paths=[], candidate_terms=[])

    def _token_entries(self, query: str) -> list[tuple[str, int]]:
        entries: list[tuple[str, int]] = []
        for match in self._TOKEN_RE.finditer(query):
            token = str(match.group(0) or "").strip().strip("`'\"()[]!?.,:;").lower()
            if token:
                entries.append((token, match.start()))
        return entries

    def _has_api_marker(self, token_entries: list[tuple[str, int]]) -> bool:
        return any(token in self._API_MARKERS for token, _start in token_entries)

    def _is_short_explain_query(self, token_entries: list[tuple[str, int]]) -> bool:
        if len(token_entries) > self._SHORT_QUERY_TOKEN_LIMIT:
            return False
        return any(token in self._EXPLAIN_MARKERS for token, _start in token_entries)

    def _primary_candidate(self, token_entries: list[tuple[str, int]]) -> str | None:
        marker_positions = [start for token, start in token_entries if token in self._API_MARKERS]
        candidates = [(token, start) for token, start in token_entries if self._is_api_candidate(token)]
        if not candidates:
            return None
        if not marker_positions:
            return candidates[-1][0]
        primary = min(
            candidates,
            key=lambda item: min(abs(item[1] - marker_pos) for marker_pos in marker_positions),
        )
        return primary[0]

    def _is_api_candidate(self, token: str) -> bool:
        if (
            not token
            or token in self._NOISE_WORDS
            or token.startswith("docs/")
            or token.endswith((".md", ".yaml", ".yml", ".json"))
        ):
            return False
        if token.startswith("/"):
            return True
        return self._ASCII_ENDPOINT_RE.fullmatch(token) is not None and len(token) >= 3

    def _ensure_endpoint(self, token: str) -> str:
        return token if token.startswith("/") else f"/{token}"


class _TermCollector:
    _TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_./{}-]+")
    _IDENTIFIER_RE = re.compile(
        r"^(?:[a-z0-9]+(?:[_-][a-z0-9]+)+|[a-z]+[A-Z][A-Za-z0-9]+|(?:[A-Z][a-z0-9]+){2,})$"
    )
    _QUESTION_WORDS = {"что", "как", "где", "какой", "какие", "каком", "когда", "чего"}
    _INTENT_WORDS = {"объясни", "покажи", "найди", "расскажи", "дай", "опиши", "нужен", "show"}
    _FILLER_WORDS = {"про", "там", "тут", "плз", "pls", "for"}
    _MARKER_WORDS = {
        "файл",
        "файле",
        "file",
        "method",
        "метод",
        "метода",
        "методу",
        "route",
        "ручка",
        "ручки",
        "эндпоинт",
        "эндпоинта",
        "overview",
        "architecture",
        "arch",
        "flow",
        "process",
        "workflow",
        "док",
        "дока",
        "доках",
        "документ",
        "doc",
        "описан",
        "док-саммари",
        "summary",
        "саммари",
    }
    _SERVICE_WORDS = {
        "кратко",
        "краткий",
        "для",
        "есть",
        "делает",
        "работает",
        "это",
        "этой",
        "этого",
        "этот",
        "документы",
        "документация",
        "документации",
        "файлы",
        "путь",
        "пути",
        "service",
        "summary",
        "endpoint",
        "docs",
    }
    _MAX_TERMS = 7

    def collect(self, query: str, alias_terms: list[str], endpoint_paths: list[str]) -> list[str]:
        explicit_terms: list[str] = []
        for value in endpoint_paths:
            self._append_unique(explicit_terms, value)
        for token in self._TOKEN_RE.findall(query):
            normalized = self._normalize(token)
            if not normalized:
                continue
            if self._is_endpoint(normalized) or self._is_identifier(normalized) or self._is_valid_term(normalized):
                self._append_unique(explicit_terms, normalized)
        alias_bucket = self._collect_alias_terms(alias_terms, explicit_terms)
        prioritized = self._prioritize(explicit_terms, alias_bucket)
        return prioritized[: self._MAX_TERMS]

    def _normalize(self, token: str) -> str:
        trimmed = str(token or "").strip().strip("`'\"()[]!?.,:;")
        if "{" in trimmed and "}" not in trimmed:
            return ""
        return trimmed.lower()

    def _is_endpoint(self, token: str) -> bool:
        return token.startswith("/") and len(token) > 1 and "{" not in token.replace("{", "", 1)

    def _is_identifier(self, token: str) -> bool:
        return bool(self._IDENTIFIER_RE.fullmatch(token))

    def _is_valid_term(self, token: str) -> bool:
        if len(token) < 3 or "/" in token or "." in token:
            return False
        if (
            token in self._QUESTION_WORDS
            or token in self._INTENT_WORDS
            or token in self._FILLER_WORDS
            or token in self._MARKER_WORDS
            or token in self._SERVICE_WORDS
        ):
            return False
        return True

    def _collect_alias_terms(self, alias_terms: list[str], explicit_terms: list[str]) -> list[str]:
        collected: list[str] = []
        explicit_set = set(explicit_terms)
        for term in alias_terms:
            normalized = self._normalize(term)
            if not normalized:
                continue
            if normalized in explicit_set:
                continue
            if self._is_identifier(normalized):
                parts = [part for part in re.split(r"[_-]", normalized) if part]
                if parts and all(part in explicit_set for part in parts):
                    continue
            self._append_unique(collected, normalized)
        return collected

    def _prioritize(self, explicit_terms: list[str], alias_terms: list[str]) -> list[str]:
        terms = explicit_terms + [term for term in alias_terms if term not in explicit_terms]
        endpoints = [term for term in terms if self._is_endpoint(term)]
        identifiers = [term for term in terms if term not in endpoints and self._is_identifier(term)]
        aliases = [term for term in alias_terms if term not in endpoints and term not in identifiers]
        other_terms = [term for term in terms if term not in endpoints and term not in identifiers and term not in aliases]
        return endpoints + identifiers + aliases + other_terms

    def _append_unique(self, items: list[str], value: str) -> None:
        if value and value not in items:
            items.append(value)


class V2TargetTermsExtractor:
    def __init__(
        self,
        alias_matcher: _AliasMatcher | None = None,
        endpoint_extractor: _EndpointPathExtractor | None = None,
        api_like_extractor: _ApiLikeAnchorExtractor | None = None,
        term_collector: _TermCollector | None = None,
    ) -> None:
        self._alias_matcher = alias_matcher or _AliasMatcher()
        self._endpoint_extractor = endpoint_extractor or _EndpointPathExtractor()
        self._api_like_extractor = api_like_extractor or _ApiLikeAnchorExtractor()
        self._term_collector = term_collector or _TermCollector()

    def extract(self, normalized_query: str) -> TargetTermsAnalysis:
        lowered = normalized_query.lower()
        endpoint_paths = self._endpoint_extractor.extract(normalized_query)
        api_like = self._api_like_extractor.extract(normalized_query, endpoint_paths)
        alias_terms, alias_docs, alias_hits = self._alias_matcher.match(lowered)
        return TargetTermsAnalysis(
            target_terms=self._term_collector.collect(normalized_query, alias_terms, api_like.endpoint_paths),
            endpoint_paths=api_like.endpoint_paths,
            api_like_terms=api_like.candidate_terms,
            matched_aliases=alias_hits,
            alias_docs=alias_docs,
        )
