"""Адаптер v2 к :class:`RagSessionRetriever` с plan-driven execution strategy."""

from __future__ import annotations

from app.core.agent.utils.process_v2.rag_retrieval.target_doc_seeding import (
    merge_row_lists,
    normalize_doc_path,
    path_variants_for_rag_query,
)
from app.core.rag.retrieval.session_retriever import RagSessionRetriever, RetrievalPlan


class _PlanDrivenRetrieval:
    def __init__(self, retriever: RagSessionRetriever) -> None:
        self._retriever = retriever

    async def fetch_rows(self, rag_session_id: str, query_text: str, plan: RetrievalPlan) -> list[dict]:
        seeded_rows = await self._seed_from_target_hints(rag_session_id, plan)
        semantic_rows = await self._retriever.retrieve(rag_session_id, query_text, plan)
        merged = merge_row_lists(seeded_rows, semantic_rows)
        if not _needs_docs_catalog_fallback(plan):
            return _apply_query_signal_filter(merged, plan)
        fallback_rows = await self._fetch_docs_catalog_rows(rag_session_id, plan)
        return _apply_query_signal_filter(merge_row_lists(merged, fallback_rows), plan)

    async def fetch_exact_paths(self, rag_session_id: str, *, paths: list[str], layers: list[str] | None = None) -> list[dict]:
        return await self._retriever.retrieve_exact_files(rag_session_id, paths=paths, layers=layers)

    async def fetch_chunks_by_path_substrings(
        self,
        rag_session_id: str,
        *,
        path_needles: list[str],
        layers: list[str] | None = None,
        limit: int = 200,
    ) -> list[dict]:
        return await self._retriever.retrieve_chunks_by_path_substrings(
            rag_session_id,
            path_needles=path_needles,
            layers=layers,
            limit=limit,
        )

    async def _seed_from_target_hints(self, rag_session_id: str, plan: RetrievalPlan) -> list[dict]:
        hints = self._target_doc_hints(plan)
        if not hints:
            return []
        exact_rows = await self._fetch_exact_rows(rag_session_id, hints)
        missing = self._missing_hints(hints, exact_rows)
        if not missing:
            return exact_rows
        fallback_rows = await self._fetch_substring_rows(rag_session_id, missing)
        return merge_row_lists(exact_rows, fallback_rows)

    async def _fetch_exact_rows(self, rag_session_id: str, hints: list[str]) -> list[dict]:
        variant_paths: list[str] = []
        for hint in hints:
            variant_paths.extend(path_variants_for_rag_query(hint))
        unique_paths = list(dict.fromkeys(path for path in variant_paths if path))
        if not unique_paths:
            return []
        return await self._retriever.retrieve_exact_files(rag_session_id, paths=unique_paths, layers=None)

    async def _fetch_substring_rows(self, rag_session_id: str, hints: list[str]) -> list[dict]:
        needles = [normalize_doc_path(hint).split("/")[-1] for hint in hints]
        unique_needles = list(dict.fromkeys(needle for needle in needles if needle))
        if not unique_needles:
            return []
        return await self._retriever.retrieve_chunks_by_path_substrings(
            rag_session_id,
            path_needles=unique_needles,
            layers=None,
            limit=200,
        )

    async def _fetch_docs_catalog_rows(self, rag_session_id: str, plan: RetrievalPlan) -> list[dict]:
        rows = await self._retriever.list_docs_scope_rows(rag_session_id, limit=max(1000, plan.limit * 10))
        return _filter_docs_rows(rows, plan)[: plan.limit]

    def _target_doc_hints(self, plan: RetrievalPlan) -> list[str]:
        raw = plan.filters.get("target_doc_hints")
        if not isinstance(raw, list):
            return []
        return [str(item).strip() for item in raw if str(item or "").strip()]

    def _missing_hints(self, hints: list[str], rows: list[dict]) -> list[str]:
        pool = {normalize_doc_path(str(row.get("path") or "")) for row in rows}
        return [hint for hint in hints if normalize_doc_path(hint) not in pool]


class V2RagRetrievalAdapter:
    """Обёртка над :class:`RagSessionRetriever` для plan-driven retrieval и подмены в тестах."""

    def __init__(self, retriever: RagSessionRetriever) -> None:
        self._retriever = _PlanDrivenRetrieval(retriever)

    async def fetch_rows(self, rag_session_id: str, query_text: str, plan: RetrievalPlan) -> list[dict]:
        return await self._retriever.fetch_rows(rag_session_id, query_text, plan)

    async def fetch_exact_paths(self, rag_session_id: str, *, paths: list[str], layers: list[str] | None = None) -> list[dict]:
        return await self._retriever.fetch_exact_paths(rag_session_id, paths=paths, layers=layers)

    async def fetch_chunks_by_path_substrings(
        self,
        rag_session_id: str,
        *,
        path_needles: list[str],
        layers: list[str] | None = None,
        limit: int = 200,
    ) -> list[dict]:
        return await self._retriever.fetch_chunks_by_path_substrings(
            rag_session_id,
            path_needles=path_needles,
            layers=layers,
            limit=limit,
        )


def _needs_docs_catalog_fallback(plan: RetrievalPlan) -> bool:
    layers = {str(item).strip() for item in plan.layers}
    if "D1_DOCUMENT_CATALOG" not in layers:
        return False
    return "metadata.type" in plan.filters or "metadata.doc_type" in plan.filters


def _filter_docs_rows(rows: list[dict], plan: RetrievalPlan) -> list[dict]:
    allowed_layers = {str(item).strip() for item in plan.layers if str(item).strip()}
    metadata_type = _norm(plan.filters.get("metadata.type")) or _norm(plan.filters.get("metadata.doc_type"))
    metadata_domain = _norm(plan.filters.get("metadata.domain"))
    metadata_subdomain = _norm(plan.filters.get("metadata.subdomain"))
    out: list[dict] = []
    for row in rows:
        layer = str(row.get("layer") or "").strip()
        if allowed_layers and layer not in allowed_layers:
            continue
        metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
        row_type = _norm(metadata.get("type")) or _norm(metadata.get("doc_type"))
        if metadata_type and row_type != metadata_type:
            continue
        if metadata_domain and _norm(metadata.get("domain")) != metadata_domain:
            continue
        if metadata_subdomain and _norm(metadata.get("subdomain")) != metadata_subdomain:
            continue
        out.append(row)
    return sorted(out, key=lambda item: str(item.get("path") or ""))


def _norm(value: object) -> str:
    return str(value or "").strip().lower()


def _apply_query_signal_filter(rows: list[dict], plan: RetrievalPlan) -> list[dict]:
    signals = _query_signals(plan)
    if not signals:
        return rows
    strict = [row for row in rows if _matches_any_signal(row, signals, strict=True)]
    if strict:
        return strict
    broad = [row for row in rows if _matches_any_signal(row, signals, strict=False)]
    return broad or rows


def _query_signals(plan: RetrievalPlan) -> list[str]:
    raw = plan.filters.get("query_signals")
    if not isinstance(raw, list):
        return []
    return [item for item in (_norm(value) for value in raw) if item]


def _matches_any_signal(row: dict, signals: list[str], *, strict: bool) -> bool:
    haystack = _strict_haystack(row) if strict else _broad_haystack(row)
    return any(signal in haystack for signal in signals)


def _strict_haystack(row: dict) -> str:
    metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
    parts = [
        row.get("path"),
        row.get("title"),
        metadata.get("endpoint"),
        metadata.get("name"),
    ]
    return " ".join(_norm(part) for part in parts if _norm(part))


def _broad_haystack(row: dict) -> str:
    metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
    parts = [
        row.get("path"),
        row.get("title"),
        row.get("content"),
        metadata.get("endpoint"),
        metadata.get("name"),
        metadata.get("summary_text"),
        metadata.get("entities"),
        metadata.get("tags"),
    ]
    return " ".join(_norm(part) for part in parts if _norm(part))
