from __future__ import annotations

import re

from app.modules.rag.intent_router_v2.models import AnchorSpan, QueryAnchor
from app.modules.rag.intent_router_v2.analysis.normalization_terms import KeyTermCanonicalizer
from app.modules.rag.intent_router_v2.analysis.symbol_rules import COMMON_PATH_SEGMENTS, PY_KEYWORDS
from app.modules.rag.intent_router_v2.analysis.term_mapping import RuEnTermMapper

_FILE_PATTERN = re.compile(r"(?P<value>\b(?:[\w.-]+/)*[\w.-]+\.(?:py|md|rst|txt|yaml|yml|json|toml|ini|cfg)\b)")
_PATH_HINT_PATTERN = re.compile(r"(?P<value>\b(?:src|app|docs|tests)/[\w./-]*[\w-]\b)")
_SYMBOL_PATTERN = re.compile(
    r"\b(?P<value>[A-Z][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+|[A-Z][A-Za-z0-9_]{2,}|[a-z_][A-Za-z0-9_]{2,})\b"
)
_DOC_SECTION_PATTERN = re.compile(r"(?:section|раздел)\s+[\"'`#]?(?P<value>[A-Za-zА-Яа-я0-9_ ./:-]{2,})", re.IGNORECASE)
_WORD_RE = re.compile(r"[A-Za-zА-Яа-яЁё-]+")


class AnchorExtractor:
    def __init__(
        self,
        mapper: RuEnTermMapper | None = None,
        canonicalizer: KeyTermCanonicalizer | None = None,
    ) -> None:
        self._mapper = mapper or RuEnTermMapper()
        self._canonicalizer = canonicalizer or KeyTermCanonicalizer()

    def extract(self, text: str) -> list[QueryAnchor]:
        anchors = self._file_anchors(text)
        anchors.extend(self._symbol_anchors(text, file_anchors=anchors))
        anchors.extend(self._doc_ref_anchors(text))
        anchors.extend(self._key_term_anchors(text))
        return self._dedupe(anchors)

    def _file_anchors(self, text: str) -> list[QueryAnchor]:
        anchors = self._anchors_from_matches(_FILE_PATTERN.finditer(text), anchor_type="FILE_PATH", confidence=0.95)
        anchors.extend(self._anchors_from_matches(_PATH_HINT_PATTERN.finditer(text), anchor_type="FILE_PATH", confidence=0.8))
        return anchors

    def _symbol_anchors(self, text: str, *, file_anchors: list[QueryAnchor]) -> list[QueryAnchor]:
        anchors: list[QueryAnchor] = []
        path_ranges = [(anchor.span.start, anchor.span.end) for anchor in file_anchors if anchor.span is not None]
        path_segments = self._path_segments(file_anchors)
        for match in _SYMBOL_PATTERN.finditer(text):
            value = match.group("value")
            if value.endswith((".py", ".md")) or "/" in value:
                continue
            if self._is_inside_path(match.start("value"), match.end("value"), path_ranges):
                continue
            if self._is_keyword(value):
                continue
            if file_anchors and value.lower() in path_segments:
                continue
            anchors.append(self._anchor("SYMBOL", value, match.start("value"), match.end("value"), 0.88, source="user_text"))
        return anchors

    def _doc_ref_anchors(self, text: str) -> list[QueryAnchor]:
        anchors = self._anchors_from_matches(_DOC_SECTION_PATTERN.finditer(text), anchor_type="DOC_REF", confidence=0.75, subtype="section")
        for match in _FILE_PATTERN.finditer(text):
            value = match.group("value")
            if not value.lower().endswith((".md", ".rst", ".txt")):
                continue
            anchors.append(self._anchor("DOC_REF", value, match.start("value"), match.end("value"), 0.92, subtype="file", source="user_text"))
        return anchors

    def _key_term_anchors(self, text: str) -> list[QueryAnchor]:
        literals = set(self._mapper.all_literal_terms())
        anchors: list[QueryAnchor] = []
        for token in _WORD_RE.finditer(text):
            value = token.group(0)
            normalized = value.lower()
            canonical = self._canonicalizer.canonicalize(value)
            if canonical is None and normalized not in literals:
                continue
            anchors.append(
                self._anchor(
                    "KEY_TERM",
                    canonical or value,
                    token.start(),
                    token.end(),
                    0.9,
                    source="user_text",
                )
            )
        return anchors

    def _anchors_from_matches(
        self,
        matches,
        *,
        anchor_type: str,
        confidence: float,
        subtype: str | None = None,
    ) -> list[QueryAnchor]:
        return [
            self._anchor(anchor_type, match.group("value"), match.start("value"), match.end("value"), confidence, subtype=subtype)
            for match in matches
        ]

    def _anchor(
        self,
        anchor_type: str,
        value: str,
        start: int,
        end: int,
        confidence: float,
        subtype: str | None = None,
        source: str = "user_text",
    ) -> QueryAnchor:
        return QueryAnchor(
            type=anchor_type,
            value=value,
            subtype=subtype,
            source=source,
            span=AnchorSpan(start=start, end=end),
            confidence=confidence,
        )

    def _dedupe(self, anchors: list[QueryAnchor]) -> list[QueryAnchor]:
        result: list[QueryAnchor] = []
        seen: set[tuple[str, str, str | None, str]] = set()
        for anchor in anchors:
            key = (anchor.type, anchor.value, anchor.subtype, anchor.source)
            if key in seen:
                continue
            seen.add(key)
            result.append(anchor)
        return result

    def _is_inside_path(self, start: int, end: int, ranges: list[tuple[int, int]]) -> bool:
        return any(start >= left and end <= right for left, right in ranges)

    def _is_keyword(self, token: str) -> bool:
        return token.lower() in PY_KEYWORDS

    def _path_segments(self, anchors: list[QueryAnchor]) -> set[str]:
        values: set[str] = set()
        for anchor in anchors:
            parts = re.split(r"[/.]+", anchor.value.lower())
            for part in parts:
                if not part:
                    continue
                values.add(part)
        return values | COMMON_PATH_SEGMENTS
