"""Главный оркестратор runtime: роутинг → retrieval → evidence gate → генерация ответа (LLM)."""

from __future__ import annotations

import logging
from difflib import SequenceMatcher
from time import perf_counter

from app.modules.agent.runtime.models import RuntimeDraftAnswer, RuntimeExecutionState, RuntimeFinalResult
from app.modules.agent.intent_router_v2 import ConversationState, IntentRouterV2
from app.modules.agent.intent_router_v2.models import SymbolResolution
from app.modules.agent.runtime.steps.retrieval import RuntimeRetrievalAdapter, RuntimeRepoContextFactory
from app.modules.agent.runtime.steps.context import (
    build_answer_synthesis_input,
    build_evidence_bundle,
    build_retrieval_request,
    build_retrieval_result,
)
from app.modules.agent.runtime.steps.gates.pre.evidence_gate import evaluate_evidence
from app.modules.agent.runtime.steps.gates.post.post_gate import RuntimePostEvidenceGate
from app.modules.agent.runtime.steps.answer_policy import RuntimeAnswerPolicy
from app.modules.agent.runtime.steps.generation import RuntimePromptPayloadBuilder, RuntimePromptSelector, RuntimeAnswerGenerator
from app.modules.agent.runtime.steps.finalization import RuntimeAnswerRepairService, assemble_final_result
from app.modules.agent.llm import AgentLlmService

LOGGER = logging.getLogger(__name__)


class AgentRuntimeExecutor:
    def __init__(
        self,
        llm: AgentLlmService | None,
        *,
        router: IntentRouterV2 | None = None,
        retrieval: RuntimeRetrievalAdapter | None = None,
        repo_context_factory: RuntimeRepoContextFactory | None = None,
        prompt_selector: RuntimePromptSelector | None = None,
        payload_builder: RuntimePromptPayloadBuilder | None = None,
        answer_policy: RuntimeAnswerPolicy | None = None,
        post_gate: RuntimePostEvidenceGate | None = None,
    ) -> None:
        self._llm = llm
        self._router = router or IntentRouterV2()
        self._retrieval = retrieval or RuntimeRetrievalAdapter()
        self._repo_context_factory = repo_context_factory or RuntimeRepoContextFactory()
        self._prompt_selector = prompt_selector or RuntimePromptSelector()
        self._payload_builder = payload_builder or RuntimePromptPayloadBuilder()
        self._answer_policy = answer_policy or RuntimeAnswerPolicy()
        self._post_gate = post_gate or RuntimePostEvidenceGate()
        self._repair = RuntimeAnswerRepairService(llm) if llm is not None else None
        self._generator = RuntimeAnswerGenerator(llm) if llm is not None else None

    def execute(self, *, user_query: str, rag_session_id: str, files_map: dict[str, dict] | None = None) -> RuntimeFinalResult:
        timings_ms: dict[str, int] = {}
        runtime_trace: list[dict] = []
        answer_policy_branch = ""
        decision_reason = ""
        post_gate_snapshot: dict = {}
        state = RuntimeExecutionState(
            user_query=user_query,
            rag_session_id=rag_session_id,
            conversation_state=ConversationState(),
            repo_context=self._repo_context_factory.build(files_map),
        )
        started = perf_counter()
        state.router_result = self._router.route(user_query, state.conversation_state, state.repo_context)
        timings_ms["router"] = self._elapsed_ms(started)
        runtime_trace.append(
            {
                "step": "router",
                "status": "completed",
                "timings_ms": {"router": timings_ms["router"]},
                "output": {
                    "intent": state.router_result.intent,
                    "sub_intent": state.router_result.query_plan.sub_intent,
                    "graph_id": state.router_result.graph_id,
                    "conversation_mode": state.router_result.conversation_mode,
                },
            }
        )
        state.retrieval_request = build_retrieval_request(state.router_result, rag_session_id)
        started = perf_counter()
        raw_rows = self._retrieve(state)
        timings_ms["retrieval"] = self._elapsed_ms(started)
        retrieval_report = self._retrieval.consume_retrieval_report() or {}
        raw_rows, retrieval_report = self._hydrate_entrypoint_sources(state, raw_rows, retrieval_report)
        symbol_resolution = self._resolve_symbol(state.router_result.symbol_resolution.model_dump(), raw_rows)
        state.router_result = state.router_result.model_copy(update={"symbol_resolution": SymbolResolution(**symbol_resolution)})
        state.retrieval_result = build_retrieval_result(raw_rows, retrieval_report, symbol_resolution)
        if state.retrieval_request.sub_intent.upper() == "EXPLAIN" and symbol_resolution.get("status") in {"not_found", "ambiguous"}:
            state.retrieval_result = build_retrieval_result([], retrieval_report, symbol_resolution)
        runtime_trace.append(
            {
                "step": "retrieval",
                "status": "completed",
                "timings_ms": {"retrieval": timings_ms["retrieval"]},
                "output": {
                    "rag_count": len(raw_rows),
                    "answer_path_rag_count": len(state.retrieval_result.raw_rows),
                    "resolved_symbol_status": symbol_resolution.get("status"),
                    "resolved_symbol": symbol_resolution.get("resolved_symbol"),
                    "requested_layers": list(state.retrieval_request.requested_layers or []),
                },
                "diagnostics": retrieval_report or {},
            }
        )
        state.evidence_pack = build_evidence_bundle(state.retrieval_result, state.router_result)
        if state.retrieval_request.sub_intent.upper() == "EXPLAIN" and symbol_resolution.get("status") in {"not_found", "ambiguous"}:
            state.evidence_pack.sufficient = False
            state.evidence_pack.failure_reasons = ["target_not_resolved"]
        pre_gate_input = self._build_pre_gate_input(state)
        started = perf_counter()
        gate_decision = evaluate_evidence(state.evidence_pack)
        timings_ms["pre_evidence_gate"] = self._elapsed_ms(started)
        state.answer_mode = "normal" if gate_decision.passed else "degraded"
        state.degraded_message = gate_decision.degraded_message
        runtime_trace.append(
            {
                "step": "pre_evidence_gate",
                "status": "passed" if gate_decision.passed else "blocked",
                "timings_ms": {"pre_evidence_gate": timings_ms["pre_evidence_gate"]},
                "input": pre_gate_input,
                "output": {
                    "passed": gate_decision.passed,
                    "failure_reasons": list(gate_decision.failure_reasons),
                    "degraded_message": gate_decision.degraded_message,
                    "evidence_count": state.evidence_pack.evidence_count,
                },
            }
        )
        decision = self._answer_policy.decide(router_result=state.router_result, gate_decision=gate_decision)
        answer_policy_branch = decision.branch
        decision_reason = decision.reason
        if not decision.should_call_llm:
            state.answer_mode = decision.answer_mode
            started = perf_counter()
            runtime_trace.append(
                {
                    "step": "llm",
                    "status": "skipped",
                    "timings_ms": {"llm": self._elapsed_ms(started)},
                    "output": {
                        "reason": "policy_short_circuit",
                        "answer_mode": decision.answer_mode,
                        "decision_reason": decision.reason,
                        "answer_policy_branch": decision.branch,
                    },
                }
            )
            started = perf_counter()
            timings_ms["post_evidence_gate"] = self._elapsed_ms(started)
            post_gate_snapshot = {
                "input": {
                    "answer_mode": decision.answer_mode,
                    "draft_present": False,
                    "resolved_target": self._resolved_target(state),
                },
                "output": {"reason": "no_draft_answer"},
            }
            runtime_trace.append(
                {
                    "step": "post_evidence_gate",
                    "status": "skipped",
                    "timings_ms": {"post_evidence_gate": timings_ms["post_evidence_gate"]},
                    "input": post_gate_snapshot["input"],
                    "output": post_gate_snapshot["output"],
                }
            )
            return assemble_final_result(
                state,
                draft=None,
                final_answer=decision.answer,
                repair_used=False,
                llm_used=False,
                timings_ms=timings_ms,
                runtime_trace=runtime_trace,
                answer_policy_branch=answer_policy_branch,
                decision_reason=decision_reason,
                pre_gate_input=pre_gate_input,
                post_gate_snapshot=post_gate_snapshot,
                resolved_target=self._resolved_target(state),
                post_gate=self._post_gate,
            )
        if self._llm is None:
            answer_policy_branch = "llm_unavailable"
            decision_reason = "llm_service_missing"
            started = perf_counter()
            runtime_trace.append(
                {
                    "step": "llm",
                    "status": "skipped",
                    "timings_ms": {"llm": self._elapsed_ms(started)},
                    "output": {"reason": "llm_unavailable", "answer_policy_branch": answer_policy_branch, "decision_reason": decision_reason},
                }
            )
            started = perf_counter()
            timings_ms["post_evidence_gate"] = self._elapsed_ms(started)
            post_gate_snapshot = {
                "input": {
                    "answer_mode": state.answer_mode,
                    "draft_present": False,
                    "resolved_target": self._resolved_target(state),
                },
                "output": {"reason": "no_draft_answer"},
            }
            runtime_trace.append(
                {
                    "step": "post_evidence_gate",
                    "status": "skipped",
                    "timings_ms": {"post_evidence_gate": timings_ms["post_evidence_gate"]},
                    "input": post_gate_snapshot["input"],
                    "output": post_gate_snapshot["output"],
                }
            )
            return assemble_final_result(
                state,
                draft=None,
                final_answer="",
                repair_used=False,
                llm_used=False,
                timings_ms=timings_ms,
                runtime_trace=runtime_trace,
                answer_policy_branch=answer_policy_branch,
                decision_reason=decision_reason,
                pre_gate_input=pre_gate_input,
                post_gate_snapshot=post_gate_snapshot,
                resolved_target=self._resolved_target(state),
                post_gate=self._post_gate,
            )
        state.synthesis_input = build_answer_synthesis_input(user_query, state.evidence_pack)
        prompt_name = self._prompt_selector.select(
            intent=state.router_result.intent,
            sub_intent=state.retrieval_request.sub_intent,
            answer_mode=state.answer_mode,
        )
        prompt_payload = self._payload_builder.build(
            user_query=user_query,
            synthesis_input=state.synthesis_input,
            evidence_pack=state.evidence_pack,
            answer_mode=state.answer_mode,
        )
        started = perf_counter()
        draft = RuntimeDraftAnswer(
            prompt_name=prompt_name,
            prompt_payload=prompt_payload,
            answer=self._generator.generate(prompt_name, prompt_payload),
        )
        timings_ms["llm"] = self._elapsed_ms(started)
        runtime_trace.append(
            {
                "step": "llm",
                "status": "completed",
                "timings_ms": {"llm": timings_ms["llm"]},
                "output": {
                    "prompt_name": prompt_name,
                    "answer_preview": draft.answer[:300],
                    "resolved_target": self._resolved_target(state),
                    "answer_policy_branch": answer_policy_branch,
                    "decision_reason": decision_reason,
                },
            }
        )
        post_gate_input = {
            "answer_mode": state.answer_mode,
            "degraded_message": state.degraded_message,
            "resolved_target": self._resolved_target(state),
            "draft_answer_preview": draft.answer[:300],
            "repair_candidate": bool(self._repair is not None),
        }
        started = perf_counter()
        validation = self._post_gate.validate(
            answer=draft.answer,
            answer_mode=state.answer_mode,
            degraded_message=state.degraded_message,
            sub_intent=state.retrieval_request.sub_intent,
            user_query=user_query,
            evidence_pack=state.evidence_pack,
        )
        timings_ms["post_evidence_gate"] = self._elapsed_ms(started)
        final_answer = draft.answer
        repair_used = False
        if not validation.passed and self._repair is not None:
            started = perf_counter()
            final_answer = self._repair.repair(draft_answer=draft.answer, validation=validation, prompt_payload=prompt_payload)
            repair_used = True
            timings_ms["repair"] = self._elapsed_ms(started)
            started = perf_counter()
            validation = self._post_gate.validate(
                answer=final_answer,
                answer_mode=state.answer_mode,
                degraded_message=state.degraded_message,
                sub_intent=state.retrieval_request.sub_intent,
                user_query=user_query,
                evidence_pack=state.evidence_pack,
            )
            timings_ms["post_evidence_gate_recheck"] = self._elapsed_ms(started)
            if not validation.passed:
                final_answer = self._fallback_answer(state)
                state.answer_mode = self._fallback_mode(state)
        post_gate_snapshot = {
            "input": post_gate_input,
            "output": {
                "passed": validation.passed,
                "action": validation.action,
                "reasons": list(validation.reasons),
                "repair_used": repair_used,
                "final_answer_preview": final_answer[:300],
            },
        }
        runtime_trace.append(
            {
                "step": "post_evidence_gate",
                "status": "passed" if validation.passed else "failed",
                "timings_ms": {
                    "post_evidence_gate": timings_ms["post_evidence_gate"],
                    "post_evidence_gate_recheck": timings_ms.get("post_evidence_gate_recheck", 0),
                    "repair": timings_ms.get("repair", 0),
                },
                "input": post_gate_snapshot["input"],
                "output": post_gate_snapshot["output"],
            }
        )
        return assemble_final_result(
            state,
            draft=draft,
            final_answer=final_answer,
            repair_used=repair_used,
            llm_used=True,
            validation=validation,
            timings_ms=timings_ms,
            runtime_trace=runtime_trace,
            answer_policy_branch=answer_policy_branch,
            decision_reason=decision_reason,
            pre_gate_input=pre_gate_input,
            post_gate_snapshot=post_gate_snapshot,
            resolved_target=self._resolved_target(state),
            post_gate=self._post_gate,
        )

    def _retrieve(self, state: RuntimeExecutionState) -> list[dict]:
        assert state.retrieval_request is not None
        if state.retrieval_request.sub_intent == "OPEN_FILE" and state.retrieval_request.path_scope:
            return self._retrieval.retrieve_exact_files(
                state.rag_session_id,
                paths=state.retrieval_request.path_scope,
                layers=["C0_SOURCE_CHUNKS"],
                limit=200,
                query=state.retrieval_request.query,
                ranking_profile=str(getattr(state.retrieval_request.retrieval_spec, "rerank_profile", "") or ""),
            )
        return self._retrieval.retrieve_with_plan(
            state.rag_session_id,
            state.retrieval_request.query,
            state.retrieval_request.retrieval_spec,
            state.retrieval_request.retrieval_constraints,
            query_plan=state.retrieval_request.query_plan,
        )

    def _resolve_symbol(self, initial: dict, rag_rows: list[dict]) -> dict:
        if str(initial.get("status") or "") != "pending":
            return initial
        candidates = [str(item).strip() for item in initial.get("alternatives", []) if str(item).strip()]
        found = [
            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()
        ]
        exact = next((item for item in found if item in candidates), None)
        if exact:
            return {"status": "resolved", "resolved_symbol": exact, "alternatives": found[:5], "confidence": 0.99}
        close = self._close_matches(candidates, found)
        if close:
            return {"status": "ambiguous", "resolved_symbol": None, "alternatives": close[:5], "confidence": 0.55}
        return {"status": "not_found", "resolved_symbol": None, "alternatives": close[:5], "confidence": 0.0}

    def _elapsed_ms(self, started: float) -> int:
        return max(1, round((perf_counter() - started) * 1000))

    def _build_pre_gate_input(self, state: RuntimeExecutionState) -> dict:
        evidence = state.evidence_pack
        retrieval = state.retrieval_result
        return {
            "resolved_target": self._resolved_target(state),
            "sub_intent": state.retrieval_request.sub_intent if state.retrieval_request else None,
            "target_type": evidence.target_type if evidence else None,
            "evidence_count": evidence.evidence_count if evidence else 0,
            "code_chunk_count": len(evidence.code_chunks) if evidence else 0,
            "entrypoint_count": len(evidence.entrypoints) if evidence else 0,
            "relation_count": len(evidence.relations) if evidence else 0,
            "test_evidence_count": len(evidence.test_evidence) if evidence else 0,
            "symbol_resolution_status": retrieval.symbol_resolution_status if retrieval else None,
            "path_scope": list(state.retrieval_request.path_scope) if state.retrieval_request else [],
        }

    def _resolved_target(self, state: RuntimeExecutionState) -> str | None:
        if state.evidence_pack and state.evidence_pack.resolved_target:
            return state.evidence_pack.resolved_target
        if state.retrieval_result and state.retrieval_result.resolved_symbol:
            return state.retrieval_result.resolved_symbol
        if state.retrieval_request and state.retrieval_request.path_scope:
            return state.retrieval_request.path_scope[0]
        return None

    def _close_matches(self, candidates: list[str], found: list[str]) -> list[str]:
        ranked: list[tuple[float, str]] = []
        for candidate in candidates:
            for item in found:
                score = SequenceMatcher(None, candidate.lower(), item.lower()).ratio()
                if score >= 0.52:
                    ranked.append((score, item))
        ranked.sort(key=lambda pair: (-pair[0], pair[1]))
        result: list[str] = []
        for _, item in ranked:
            if item not in result:
                result.append(item)
        return result

    def _hydrate_entrypoint_sources(
        self,
        state: RuntimeExecutionState,
        raw_rows: list[dict],
        retrieval_report: dict,
    ) -> tuple[list[dict], dict]:
        if not state.retrieval_request or state.retrieval_request.sub_intent.upper() != "FIND_ENTRYPOINTS":
            return raw_rows, retrieval_report
        entrypoint_paths = []
        for row in raw_rows:
            if str(row.get("layer") or "") != "C3_ENTRYPOINTS":
                continue
            path = str(row.get("path") or "").strip()
            if path and path not in entrypoint_paths:
                entrypoint_paths.append(path)
        if not entrypoint_paths:
            return raw_rows, retrieval_report
        extra_rows = self._retrieval.retrieve_exact_files(
            state.rag_session_id,
            paths=entrypoint_paths[:3],
            layers=["C0_SOURCE_CHUNKS"],
            limit=24,
            query=state.user_query,
            ranking_profile="entrypoint_source_hydration",
        )
        extra_report = self._retrieval.consume_retrieval_report() or {}
        return self._merge_rows(raw_rows, extra_rows), self._merge_reports(retrieval_report, extra_report)

    def _merge_rows(self, base_rows: list[dict], extra_rows: list[dict]) -> list[dict]:
        merged: list[dict] = []
        seen: set[tuple[str, str, str, int | None, int | None]] = set()
        for row in [*base_rows, *extra_rows]:
            key = (
                str(row.get("layer") or ""),
                str(row.get("path") or ""),
                str(row.get("title") or ""),
                row.get("span_start"),
                row.get("span_end"),
            )
            if key in seen:
                continue
            seen.add(key)
            merged.append(row)
        return merged

    def _merge_reports(self, base: dict, extra: dict) -> dict:
        merged = dict(base or {})
        merged["executed_layers"] = list(dict.fromkeys([*(base.get("executed_layers") or []), *(extra.get("executed_layers") or [])]))
        merged["retrieval_mode_by_layer"] = {**dict(base.get("retrieval_mode_by_layer") or {}), **dict(extra.get("retrieval_mode_by_layer") or {})}
        merged["top_k_by_layer"] = {**dict(base.get("top_k_by_layer") or {}), **dict(extra.get("top_k_by_layer") or {})}
        merged["filters_by_layer"] = {**dict(base.get("filters_by_layer") or {}), **dict(extra.get("filters_by_layer") or {})}
        merged["retrieval_by_layer_ms"] = {**dict(base.get("retrieval_by_layer_ms") or {}), **dict(extra.get("retrieval_by_layer_ms") or {})}
        merged["fallback"] = dict(extra.get("fallback") or base.get("fallback") or {"used": False, "reason": None})
        merged["supplemental_requests"] = [*(base.get("supplemental_requests") or []), *(extra.get("requests") or [])]
        return merged

    def _fallback_mode(self, state: RuntimeExecutionState) -> str:
        status = str(state.router_result.symbol_resolution.status if state.router_result and state.router_result.symbol_resolution else "")
        if status == "ambiguous":
            return "ambiguous"
        if status == "not_found":
            return "not_found"
        return "degraded"

    def _fallback_answer(self, state: RuntimeExecutionState) -> str:
        symbol_resolution = state.router_result.symbol_resolution if state.router_result else None
        query_plan = state.router_result.query_plan if state.router_result else None
        target = next((item for item in list(query_plan.symbol_candidates or []) if item), "запрошенная сущность") if query_plan else "запрошенная сущность"
        if symbol_resolution and symbol_resolution.status == "ambiguous":
            return self._answer_policy.decide(router_result=state.router_result, gate_decision=evaluate_evidence(state.evidence_pack)).answer
        if symbol_resolution and symbol_resolution.status == "not_found":
            return self._answer_policy.decide(router_result=state.router_result, gate_decision=evaluate_evidence(state.evidence_pack)).answer
        if state.degraded_message:
            return state.degraded_message
        return f"Недостаточно подтверждённых данных для уверенного ответа по {target}."
