"""Canonical test-first CODE_QA pipeline: router -> retrieval -> evidence -> synthesis -> diagnostics."""

from __future__ import annotations

from dataclasses import dataclass, field
from difflib import get_close_matches
from time import perf_counter
from typing import Any, Protocol

from app.modules.rag.code_qa_pipeline.answer_synthesis import build_answer_synthesis_input
from app.modules.rag.code_qa_pipeline.contracts import (
    EvidenceBundle,
    RetrievalRequest,
    RetrievalResult,
    RouterResult,
)
from app.modules.rag.code_qa_pipeline.diagnostics import build_diagnostics_report
from app.modules.rag.code_qa_pipeline.evidence_bundle_builder import build_evidence_bundle
from app.modules.rag.code_qa_pipeline.evidence_gate import evaluate_evidence
from app.modules.rag.code_qa_pipeline.retrieval_request_builder import build_retrieval_request
from app.modules.rag.code_qa_pipeline.retrieval_result_builder import build_retrieval_result


class RetrievalAdapter(Protocol):
    """Protocol for retrieval in the CODE_QA pipeline; satisfied by RagDbAdapter."""

    def retrieve_with_plan(
        self,
        rag_session_id: str,
        query: str,
        retrieval_spec: Any,
        retrieval_constraints: Any = None,
        *,
        query_plan: Any = None,
    ) -> list[dict]: ...

    def retrieve_exact_files(
        self,
        rag_session_id: str,
        *,
        repo_id: str | None = None,
        paths: list[str],
        layers: list[str] | None = None,
        limit: int = 200,
        query: str = "",
        ranking_profile: str = "",
    ) -> list[dict]: ...

    def hydrate_resolved_symbol_sources(
        self,
        rag_session_id: str,
        base_query: str,
        rag_rows: list[dict],
        symbol_resolution: dict,
        retrieval_spec: Any,
        retrieval_constraints: Any = None,
    ) -> list[dict]: ...

    def force_symbol_context_c0(
        self,
        rag_session_id: str,
        *,
        rag_rows: list[dict],
        symbol_resolution: dict,
        limit: int = 20,
    ) -> list[dict]: ...

    def consume_retrieval_report(self) -> dict[str, Any] | None: ...


@dataclass(slots=True)
class CodeQAPipelineResult:
    """Result of one run of the canonical CODE_QA pipeline."""

    user_query: str
    rag_session_id: str
    router_result: RouterResult
    retrieval_request: RetrievalRequest
    retrieval_result: RetrievalResult
    evidence_bundle: EvidenceBundle
    evidence_gate_passed: bool
    answer_synthesis_input: Any
    diagnostics_report: Any
    answer_mode: str = "normal"
    degraded_message: str = ""
    llm_answer: str | None = None
    timings_ms: dict[str, int] = field(default_factory=dict)


class CodeQAPipelineRunner:
    """Single entrypoint for the test-first CODE_QA pipeline; uses IntentRouterV2 only."""

    def __init__(
        self,
        router: Any,
        retrieval_adapter: RetrievalAdapter,
        repo_context: Any = None,
    ) -> None:
        self._router = router
        self._adapter = retrieval_adapter
        self._repo_context = repo_context

    def run(
        self,
        user_query: str,
        rag_session_id: str,
        *,
        conversation_state: Any = None,
        run_retrieval: bool = True,
        run_hydrate: bool = True,
    ) -> CodeQAPipelineResult:
        """Run the full pipeline: route -> retrieval -> evidence bundle -> gate -> synthesis -> diagnostics."""
        timings: dict[str, int] = {}
        t0 = perf_counter()
        router_result = self._router.route(
            user_query,
            conversation_state or _default_conversation_state(),
            self._repo_context or _default_repo_context(),
        )
        timings["router"] = _ms(t0)

        request = build_retrieval_request(router_result, rag_session_id)
        raw_rows: list[dict] = []
        symbol_resolution = router_result.symbol_resolution.model_dump() if router_result.symbol_resolution else {}

        if run_retrieval:
            t1 = perf_counter()
            raw_rows = self._retrieve(router_result, request)
            timings["retrieval_total"] = _ms(t1)
            symbol_resolution = self._resolve_symbol(symbol_resolution, raw_rows)
            timings["symbol_resolution"] = 0
            if run_hydrate and request.sub_intent == "EXPLAIN" and symbol_resolution.get("status") == "resolved":
                raw_rows = self._adapter.hydrate_resolved_symbol_sources(
                    rag_session_id,
                    request.query,
                    raw_rows,
                    symbol_resolution,
                    request.retrieval_spec,
                    request.retrieval_constraints,
                )
                raw_rows = self._adapter.force_symbol_context_c0(
                    rag_session_id,
                    rag_rows=raw_rows,
                    symbol_resolution=symbol_resolution,
                    limit=20,
                )

        report = self._adapter.consume_retrieval_report() or {}
        retrieval_result = build_retrieval_result(raw_rows, report, symbol_resolution)
        evidence_bundle = build_evidence_bundle(retrieval_result, router_result)
        gate_decision = evaluate_evidence(evidence_bundle)
        synthesis_input = build_answer_synthesis_input(user_query, evidence_bundle)
        answer_mode = "normal" if gate_decision.passed else "degraded"
        if not gate_decision.passed and evidence_bundle.evidence_count == 0:
            answer_mode = "insufficient"
        diagnostics_report = build_diagnostics_report(
            router_result=router_result,
            retrieval_request=request,
            retrieval_result=retrieval_result,
            evidence_bundle=evidence_bundle,
            answer_mode=answer_mode,
            timings_ms=timings,
        )

        return CodeQAPipelineResult(
            user_query=user_query,
            rag_session_id=rag_session_id,
            router_result=router_result,
            retrieval_request=request,
            retrieval_result=retrieval_result,
            evidence_bundle=evidence_bundle,
            evidence_gate_passed=gate_decision.passed,
            answer_synthesis_input=synthesis_input,
            diagnostics_report=diagnostics_report,
            answer_mode=answer_mode,
            degraded_message=gate_decision.degraded_message,
            timings_ms=timings,
        )

    def _retrieve(self, router_result: RouterResult, request: RetrievalRequest) -> list[dict]:
        path_scope = list(request.path_scope)
        spec = request.retrieval_spec
        if request.sub_intent == "OPEN_FILE" and path_scope:
            return self._adapter.retrieve_exact_files(
                request.rag_session_id,
                paths=path_scope,
                layers=["C0_SOURCE_CHUNKS"],
                limit=200,
                query=request.query,
                ranking_profile=getattr(spec, "rerank_profile", "") or "",
            )
        return self._adapter.retrieve_with_plan(
            request.rag_session_id,
            request.query,
            spec,
            request.retrieval_constraints,
            query_plan=request.query_plan,
        )

    def _resolve_symbol(self, initial: dict, rag_rows: list[dict]) -> dict:
        status = str(initial.get("status") or "not_requested")
        if status != "pending":
            return initial
        candidates = [str(x).strip() for x in initial.get("alternatives", []) if str(x).strip()]
        c1_titles = list(dict.fromkeys(
            str(row.get("title") or "").strip()
            for row in rag_rows
            if str(row.get("layer") or "") == "C1_SYMBOL_CATALOG" and str(row.get("title") or "").strip()
        ))
        if not c1_titles:
            return {**initial, "status": "not_found"}
        exact = next((t for t in c1_titles if t in candidates), None)
        if exact:
            return {"status": "resolved", "resolved_symbol": exact, "alternatives": c1_titles[:5], "confidence": 0.99}
        fuzzy = []
        for c in candidates:
            fuzzy.extend(get_close_matches(c, c1_titles, n=3, cutoff=0.7))
        fuzzy = list(dict.fromkeys(fuzzy))
        if len(fuzzy) == 1:
            return {"status": "resolved", "resolved_symbol": fuzzy[0], "alternatives": c1_titles[:5], "confidence": 0.82}
        if len(fuzzy) > 1:
            return {"status": "ambiguous", "resolved_symbol": None, "alternatives": fuzzy[:5], "confidence": 0.55}
        return {"status": "not_found", "resolved_symbol": None, "alternatives": c1_titles[:5], "confidence": 0.0}


def _ms(started: float) -> int:
    return int((perf_counter() - started) * 1000)


def _default_conversation_state() -> Any:
    from app.modules.rag.intent_router_v2 import ConversationState
    return ConversationState()


def _default_repo_context() -> Any:
    from app.modules.rag.intent_router_v2 import RepoContext
    return RepoContext()
