from __future__ import annotations

from app.modules.rag.intent_router_v2.models import QueryAnchor, RetrievalConstraints, RetrievalProfile
from app.modules.rag.intent_router_v2.analysis.test_signals import has_test_focus, is_negative_test_request


class RetrievalConstraintsFactory:
    _DEFAULT_DOCS_INCLUDE = ["docs/**", "README*", "**/*.md"]
    _DEFAULT_DOCS_EXCLUDE = [".venv/**", "node_modules/**", "**/*.bin", "**/*.png", "**/*.jpg"]
    _DEFAULT_EXCLUDE_TESTS = [
        "tests/**",
        "**/test_*.py",
        "**/*_test.py",
    ]
    _TEST_FILE_GLOBS = [
        "tests/**",
        "**/test_*.py",
        "**/*_test.py",
        "**/conftest.py",
    ]

    def build(
        self,
        *,
        retrieval_profile: RetrievalProfile,
        sub_intent: str,
        raw_query: str,
        anchors: list[QueryAnchor],
        path_scope: list[str],
    ) -> RetrievalConstraints:
        if retrieval_profile == "docs":
            return self._docs_constraints(sub_intent=sub_intent, path_scope=path_scope)
        return self._code_constraints(sub_intent=sub_intent, raw_query=raw_query, anchors=anchors)

    def _code_constraints(self, *, sub_intent: str, raw_query: str, anchors: list[QueryAnchor]) -> RetrievalConstraints:
        include_tests = sub_intent == "FIND_TESTS" or has_test_focus(raw_query)
        exclude_tests = is_negative_test_request(raw_query)
        if include_tests:
            include_globs = ["src/**", *self._TEST_FILE_GLOBS]
            prefer_globs = ["tests/**", "**/test_*.py", "**/*_test.py"]
            exclude_globs: list[str] = []
        else:
            include_globs = ["src/**"]
            prefer_globs = []
            exclude_globs = list(self._DEFAULT_EXCLUDE_TESTS)
        if exclude_tests:
            include_globs = ["src/**"]
            prefer_globs = []
            exclude_globs = list(self._DEFAULT_EXCLUDE_TESTS)
        if sub_intent == "OPEN_FILE":
            include_globs = self._open_file_globs(anchors) or include_globs
            prefer_globs = []
        test_file_globs = list(self._TEST_FILE_GLOBS) if sub_intent == "FIND_TESTS" else []
        test_symbol_patterns = self._test_symbol_patterns(anchors) if sub_intent == "FIND_TESTS" else []
        return RetrievalConstraints(
            include_globs=include_globs,
            exclude_globs=exclude_globs,
            prefer_globs=prefer_globs,
            test_file_globs=test_file_globs,
            test_symbol_patterns=test_symbol_patterns,
            max_candidates=20,
            fuzzy_symbol_search={
                "enabled": sub_intent != "OPEN_FILE",
                "max_distance": 2,
                "top_k": 5,
            },
        )

    def _docs_constraints(self, *, sub_intent: str, path_scope: list[str]) -> RetrievalConstraints:
        include_globs = self._dedupe([*path_scope, *self._DEFAULT_DOCS_INCLUDE]) if path_scope else list(self._DEFAULT_DOCS_INCLUDE)
        return RetrievalConstraints(
            include_globs=include_globs,
            exclude_globs=list(self._DEFAULT_DOCS_EXCLUDE),
            prefer_globs=[],
            test_file_globs=[],
            test_symbol_patterns=[],
            max_candidates=20,
            fuzzy_symbol_search={
                "enabled": sub_intent != "OPEN_FILE",
                "max_distance": 2,
                "top_k": 5,
            },
        )

    def _open_file_globs(self, anchors: list[QueryAnchor]) -> list[str]:
        for anchor in anchors:
            if anchor.type != "FILE_PATH":
                continue
            value = anchor.value.strip()
            if not value:
                continue
            return [value]
        return []

    def _test_symbol_patterns(self, anchors: list[QueryAnchor]) -> list[str]:
        result: list[str] = []
        symbols = [anchor.value for anchor in anchors if anchor.type == "SYMBOL"]
        for symbol in symbols[:3]:
            snake = self._camel_to_snake(symbol)
            for pattern in (f"test_{snake}", f"Test{symbol}", symbol):
                if pattern and pattern not in result:
                    result.append(pattern)
        return result

    def _camel_to_snake(self, value: str) -> str:
        chars: list[str] = []
        for index, char in enumerate(value):
            if index > 0 and char.isupper() and value[index - 1].isalnum():
                chars.append("_")
            chars.append(char.lower())
        return "".join(chars).strip("_")

    def _dedupe(self, values: list[str]) -> list[str]:
        result: list[str] = []
        for value in values:
            if value and value not in result:
                result.append(value)
        return result
