"""Anchor-aware ranking для summary и find-files evidence."""

from __future__ import annotations

import re

from app.core.agent.utils.process_v2.anchor_signals import anchor_signal_types
from app.core.agent.utils.process_v2.models import RetrievedFile, RetrievedSummary, V2AnchorType, V2RouteResult
from app.core.agent.utils.process_v2.rag_retrieval.target_doc_seeding import normalize_doc_path
from app.core.rag.contracts.enums import RagLayer


class DocsEvidenceAssembler:
    _API_PATH_PREFIXES = ("docs/api/", "docs/endpoints/", "docs/methods/", "api/", "endpoints/", "methods/")
    _GENERIC_DOC_MARKERS = ("readme", "overview", "index", "navigation", "related docs", "catalog")

    def assemble_summaries(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedSummary]:
        items = self._rank_rows(rows, route, mode="summary")
        ranked = [
            RetrievedSummary(
                path=item["path"],
                title=item["title"],
                summary=item["summary"],
                document_id=item["document_id"],
                score=item["score"],
                confidence=min(1.0, item["score"] / 1000.0),
                match_reason=item["match_reason"],
                score_breakdown=item["score_breakdown"],
            )
            for item in items
            if item["summary"] and self._summary_row_allowed(item["row"])
        ]
        if ranked:
            ranked[0].is_primary = True
        return ranked[:3]

    def assemble_files(self, rows: list[dict], route: V2RouteResult) -> list[RetrievedFile]:
        items = self._rank_rows(rows, route, mode="find_files")
        ranked = [
            RetrievedFile(
                path=item["path"],
                title=item["title"],
                document_id=item["document_id"],
                score=item["score"],
                confidence=min(1.0, item["score"] / 1000.0),
                match_reason=item["match_reason"],
                score_breakdown=item["score_breakdown"],
            )
            for item in items
        ]
        if ranked:
            ranked[0].is_primary = True
        return ranked[:4]

    def _rank_rows(self, rows: list[dict], route: V2RouteResult, *, mode: str) -> list[dict]:
        seen: set[str] = set()
        ranked: list[dict] = []
        for row in rows:
            path = self._path(row)
            if not path or path in seen:
                continue
            seen.add(path)
            breakdown = self._score_breakdown(row, route, mode=mode)
            score = sum(breakdown.values())
            if score <= 0:
                continue
            ranked.append(
                {
                    "row": row,
                    "path": path,
                    "title": self._title(row, path),
                    "summary": self._summary(row),
                    "document_id": self._document_id(row, path),
                    "score": score,
                    "score_breakdown": breakdown,
                    "match_reason": self._match_reason(breakdown),
                    "is_generic_doc": self._is_generic_doc(path, self._title(row, path), self._summary(row), row),
                }
            )
        ranked.sort(key=lambda item: (-item["score"], item["path"]))
        ranked = self._ensure_target_docs_in_top_k(ranked, route, k=4 if mode == "find_files" else 3)
        return self._promote_specific_primary(ranked, route)

    def _score_breakdown(self, row: dict, route: V2RouteResult, *, mode: str) -> dict[str, int]:
        path_raw = self._path(row)
        path = path_raw.lower()
        filename = path.split("/")[-1]
        title = self._title(row, path).lower()
        summary = self._summary(row).lower()
        entity = self._entity_name(row).lower()
        query_tokens = self._query_tokens(route)
        path_tokens = self._path_tokens(path)
        compact_haystack = {self._compact(path), self._compact(filename), self._compact(title), self._compact(entity)}
        breakdown = {
            "semantic": 0,
            "path_match": 0,
            "filename_match": 0,
            "alias_match": 0,
            "anchor_boost": 0,
            "target_doc_boost": 0,
            "specificity_boost": 0,
            "generic_penalty": 0,
        }
        if route.intent == "GENERAL_QA":
            breakdown["semantic"] += 80
        hint_norm_lower = {normalize_doc_path(h).lower() for h in route.anchors.target_doc_hints if str(h or "").strip()}
        if normalize_doc_path(path_raw).lower() in hint_norm_lower:
            breakdown["target_doc_boost"] += 1000
        hint_texts = [str(hint or "").strip().lower() for hint in route.anchors.target_doc_hints if str(hint or "").strip()]
        if any(alias.lower() in " ".join([path, title, summary, entity]) for alias in route.anchors.matched_aliases):
            breakdown["alias_match"] += 500
        for token in query_tokens:
            if token in path_tokens:
                breakdown["path_match"] += 60
            if token and token in filename:
                breakdown["filename_match"] += 200
            if token and token in summary:
                breakdown["semantic"] += 20
            if self._compact(token) in compact_haystack:
                breakdown["alias_match"] += 250
        for hint in hint_texts:
            compact_hint = self._compact(hint)
            if compact_hint and compact_hint in compact_haystack:
                breakdown["target_doc_boost"] += 180
            elif hint and hint.strip("/") in " ".join([path, title, summary, entity]):
                breakdown["semantic"] += 70
        endpoint_text = self._summary(row).lower()
        for endpoint in route.anchors.endpoint_paths:
            normalized_endpoint = endpoint.strip().lower()
            endpoint_slug = normalized_endpoint.strip("/")
            if normalized_endpoint and normalized_endpoint in endpoint_text:
                breakdown["target_doc_boost"] += 260
            if endpoint_slug and endpoint_slug in filename:
                breakdown["filename_match"] += 200
        if any(endpoint.strip("/").lower() in filename for endpoint in route.anchors.endpoint_paths):
            breakdown["filename_match"] += 200
        signals = anchor_signal_types(route)
        breakdown["anchor_boost"] += self._anchor_boost(path, signals)
        breakdown["specificity_boost"] += self._specificity_boost(row, path, title, summary, route)
        breakdown["generic_penalty"] += self._generic_penalty(path, signals)
        if mode == "find_files":
            breakdown["path_match"] *= 3
            breakdown["filename_match"] *= 2
            breakdown["semantic"] = max(0, breakdown["semantic"] // 2)
        return breakdown

    def _anchor_boost(self, path: str, signals: set[str]) -> int:
        boost = 0
        if V2AnchorType.API_ENDPOINT in signals and path.startswith(self._API_PATH_PREFIXES):
            boost += 360
        if V2AnchorType.LOGIC_FLOW in signals and path.startswith("docs/logic/"):
            boost += 300
        if V2AnchorType.DOMAIN_ENTITY in signals and path.startswith("docs/domains/"):
            boost += 300
        if V2AnchorType.ARCHITECTURE in signals and path.startswith("docs/architecture/"):
            boost += 300
        if V2AnchorType.FIND_FILES in signals and path.startswith("docs/"):
            boost += 120
        return boost

    def _generic_penalty(self, path: str, signals: set[str]) -> int:
        penalty = 0
        lowered = path.lower()
        if path == "docs/README.md" and V2AnchorType.ARCHITECTURE not in signals:
            penalty -= 260
        if any(marker in lowered for marker in ("/readme", "readme.md", "/index", "/overview", "/catalog", "/navigation")):
            penalty -= 220
        if "/architecture/" in path and V2AnchorType.ARCHITECTURE not in signals and signals.intersection(
            {V2AnchorType.API_ENDPOINT, V2AnchorType.DOMAIN_ENTITY}
        ):
            penalty -= 150
        return penalty

    def _ensure_target_docs_in_top_k(self, ranked: list[dict], route: V2RouteResult, *, k: int) -> list[dict]:
        if not ranked or not route.anchors.target_doc_hints:
            return ranked
        top = ranked[:k]
        top_paths = {item["path"] for item in top}
        top_norm = {normalize_doc_path(p).lower() for p in top_paths if p}
        for hint in route.anchors.target_doc_hints:
            hn = normalize_doc_path(hint).lower()
            if hn in top_norm:
                continue
            candidate = next((item for item in ranked if normalize_doc_path(item["path"]).lower() == hn), None)
            if candidate is None:
                continue
            if len(top) < k:
                top.append(candidate)
            else:
                top[-1] = candidate
            top_paths = {item["path"] for item in top}
            top_norm = {normalize_doc_path(p).lower() for p in top_paths if p}
        remaining = [item for item in ranked if item["path"] not in top_paths]
        top.sort(key=lambda item: (-item["score"], item["path"]))
        return top + remaining

    def _promote_specific_primary(self, ranked: list[dict], route: V2RouteResult) -> list[dict]:
        if len(ranked) < 2:
            return ranked
        first = ranked[0]
        if not first.get("is_generic_doc"):
            return ranked
        promoted = next(
            (item for item in ranked[1:] if not item.get("is_generic_doc") and self._is_specific_candidate(item, route)),
            None,
        )
        if promoted is None:
            return ranked
        return [promoted] + [item for item in ranked if item["path"] != promoted["path"]]

    def _match_reason(self, breakdown: dict[str, int]) -> str:
        if breakdown["target_doc_boost"] > 0:
            return "exact_path"
        if breakdown["alias_match"] > 0:
            return "alias_match"
        if breakdown["filename_match"] > 0:
            return "exact_title"
        return "semantic_match"

    def _summary_row_allowed(self, row: dict) -> bool:
        metadata = dict(row.get("metadata") or {})
        if row.get("layer") != RagLayer.DOCS_DOC_CHUNKS:
            return True
        section = str(metadata.get("section_path") or "").lower()
        return "summary" in section or "свод" in section or "overview" in section

    def _specificity_boost(self, row: dict, path: str, title: str, summary: str, route: V2RouteResult) -> int:
        boost = 0
        filename = path.split("/")[-1]
        lowered_title = title.lower()
        lowered_summary = summary.lower()
        if not self._is_generic_doc(path, title, summary, row):
            boost += 90
        if path.startswith(self._API_PATH_PREFIXES):
            boost += 160
        if "endpoint" in filename or "endpoint" in lowered_title or "method" in lowered_title:
            boost += 120
        if row.get("layer") == RagLayer.DOCS_DOC_CHUNKS and not self._looks_like_navigation_chunk(row):
            boost += 80
        for token in self._query_tokens(route):
            if token and token in filename:
                boost += 90
            if token and token in lowered_title:
                boost += 70
            if token and token in lowered_summary:
                boost += 40
        return boost

    def _is_specific_candidate(self, item: dict, route: V2RouteResult) -> bool:
        breakdown = dict(item.get("score_breakdown") or {})
        if breakdown.get("target_doc_boost", 0) > 0:
            return True
        if breakdown.get("specificity_boost", 0) >= 160:
            return True
        return V2AnchorType.API_ENDPOINT in anchor_signal_types(route) and item["path"].startswith(self._API_PATH_PREFIXES)

    def _is_generic_doc(self, path: str, title: str, summary: str, row: dict) -> bool:
        haystack = " ".join([path.lower(), title.lower(), summary.lower()])
        if any(marker in haystack for marker in self._GENERIC_DOC_MARKERS):
            return True
        return self._looks_like_navigation_chunk(row)

    def _looks_like_navigation_chunk(self, row: dict) -> bool:
        text = self._summary(row).lower()
        if not text:
            return False
        lines = [line.strip() for line in text.splitlines() if line.strip()]
        bullet_lines = sum(1 for line in lines if line.startswith(("- ", "* ", "1.", "2.", "3.")))
        link_lines = sum(1 for line in lines if "](" in line or line.startswith("docs/"))
        if "related docs" in text or "navigation" in text:
            return True
        return bullet_lines >= 3 or link_lines >= 3

    def _query_tokens(self, route: V2RouteResult) -> list[str]:
        values = list(route.target_terms) + list(route.anchors.matched_aliases)
        tokens: list[str] = []
        for item in values:
            for token in re.split(r"[^a-zA-Zа-яА-Я0-9]+", str(item).lower()):
                if len(token) >= 3:
                    tokens.append(token)
        return list(dict.fromkeys(tokens))

    def _path_tokens(self, path: str) -> set[str]:
        return {token for token in re.split(r"[^a-zA-Zа-яА-Я0-9]+", path.lower()) if len(token) >= 3}

    def _compact(self, value: str) -> str:
        return "".join(self._path_tokens(value))

    def _path(self, row: dict) -> str:
        metadata = dict(row.get("metadata") or {})
        raw = str(row.get("path") or metadata.get("source_path") or "").strip()
        return normalize_doc_path(raw)

    def _title(self, row: dict, path: str) -> str:
        metadata = dict(row.get("metadata") or {})
        return str(row.get("title") or metadata.get("title") or path).strip()

    def _summary(self, row: dict) -> str:
        metadata = dict(row.get("metadata") or {})
        return str(metadata.get("summary_text") or row.get("content") or "").strip()

    def _document_id(self, row: dict, path: str) -> str:
        metadata = dict(row.get("metadata") or {})
        return str(metadata.get("document_id") or metadata.get("doc_id") or path).strip()

    def _entity_name(self, row: dict) -> str:
        metadata = dict(row.get("metadata") or {})
        return str(metadata.get("entity_name") or "").strip()
