from __future__ import annotations

from time import perf_counter
from typing import Any

from app.modules.agent.llm import AgentLlmService
from app.modules.agent.intent_router_v2.analysis.docs_query_signals import DocsQuerySignals
from app.modules.agent.runtime.docs_qa_pipeline.answer_synthesizer import DocsAnswerSynthesizer
from app.modules.agent.runtime.docs_qa_pipeline.anchor_selector import DocsAnchorSelector
from app.modules.agent.runtime.docs_qa_pipeline.diagnostics_builder import DocsDiagnosticsBuilder
from app.modules.agent.runtime.docs_qa_pipeline.evidence_builder import DocsEvidenceBuilder
from app.modules.agent.runtime.docs_qa_pipeline.exact_anchor_matcher import DocsExactAnchorMatcher
from app.modules.agent.runtime.docs_qa_pipeline.models import DocsQAPipelineResult
from app.modules.agent.runtime.docs_qa_pipeline.openapi_postprocessor import OpenAPIPostprocessor
from app.modules.agent.runtime.docs_qa_pipeline.openapi_generator import OpenAPIGenerator
from app.modules.agent.runtime.docs_qa_pipeline.prompt_payload_builder import DocsPromptPayloadBuilder
from app.modules.agent.runtime.legacy_pipeline import RetrievalAdapter
from app.modules.agent.runtime.steps.context import build_retrieval_request
from app.modules.agent.runtime.steps.generation import RuntimePromptSelector
from app.modules.agent.observability.module_trace import RequestTraceContext


class DocsQAPipelineRunner:
    def __init__(
        self,
        router: Any,
        retrieval_adapter: RetrievalAdapter,
        repo_context: Any = None,
        llm: AgentLlmService | None = None,
        evidence_builder: DocsEvidenceBuilder | None = None,
        answer_synthesizer: DocsAnswerSynthesizer | None = None,
        openapi_generator: OpenAPIGenerator | None = None,
        diagnostics_builder: DocsDiagnosticsBuilder | None = None,
        prompt_selector: RuntimePromptSelector | None = None,
        prompt_payload_builder: DocsPromptPayloadBuilder | None = None,
        openapi_postprocessor: OpenAPIPostprocessor | None = None,
        exact_anchor_matcher: DocsExactAnchorMatcher | None = None,
        anchor_selector: DocsAnchorSelector | None = None,
    ) -> None:
        self._router = router
        self._adapter = retrieval_adapter
        self._repo_context = repo_context
        self._llm = llm
        self._evidence_builder = evidence_builder or DocsEvidenceBuilder()
        self._answer_synthesizer = answer_synthesizer or DocsAnswerSynthesizer()
        self._openapi_generator = openapi_generator or OpenAPIGenerator()
        self._diagnostics_builder = diagnostics_builder or DocsDiagnosticsBuilder()
        self._prompt_selector = prompt_selector or RuntimePromptSelector()
        self._prompt_payload_builder = prompt_payload_builder or DocsPromptPayloadBuilder()
        self._openapi_postprocessor = openapi_postprocessor or OpenAPIPostprocessor()
        self._exact_anchor_matcher = exact_anchor_matcher or DocsExactAnchorMatcher()
        self._anchor_selector = anchor_selector or DocsAnchorSelector()
        self._docs_signals = DocsQuerySignals()

    def run(
        self,
        user_query: str,
        rag_session_id: str,
        *,
        conversation_state: Any = None,
        mode: str = "full",
        trace: RequestTraceContext | None = None,
    ) -> DocsQAPipelineResult:
        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)

        t1 = perf_counter()
        request = build_retrieval_request(router_result, rag_session_id)
        raw_rows = self._adapter.retrieve_with_plan(
            rag_session_id,
            request.query,
            request.retrieval_spec,
            request.retrieval_constraints,
            query_plan=request.query_plan,
        )
        retrieval_report = self._adapter.consume_retrieval_report() if hasattr(self._adapter, "consume_retrieval_report") else {}
        unfiltered_rows = list(raw_rows)
        raw_rows, exact_anchor_match = self._exact_anchor_matcher.filter_rows(
            raw_rows,
            anchor_type=router_result.matched_anchor_type,
            anchor_value=router_result.matched_anchor_value,
        )
        if request.sub_intent == "RELATED_DOCS_EXPLAIN" and not raw_rows and self._has_relation_hits(unfiltered_rows):
            raw_rows = unfiltered_rows
        if trace is not None:
            trace.module("rag_retrieval").log(
                "completed",
                {
                    "planned_layers": list(request.requested_layers),
                    "executed_layers": list(retrieval_report.get("executed_layers") or request.requested_layers),
                    "layer_diagnostics": dict(retrieval_report.get("layer_diagnostics") or {}),
                    "rows": len(raw_rows),
                },
            )
        timings["retrieval"] = _ms(t1)

        t2 = perf_counter()
        evidence_bundle = self._evidence_builder.build(
            intent=router_result.intent,
            sub_intent=request.sub_intent,
            raw_rows=raw_rows,
        )
        anchor_selection = self._anchor_selector.select(
            sub_intent=request.sub_intent,
            anchor_type=router_result.matched_anchor_type,
            anchor_value=router_result.matched_anchor_value,
            rows=raw_rows,
        )
        openapi_result = None
        prompt_name = self._prompt_selector.select(
            intent=router_result.intent,
            sub_intent=request.sub_intent,
            answer_mode="normal",
        )
        llm_mode = self._llm_mode(router_result.intent, request.sub_intent)
        output_valid = True
        answer_mode = "diagnostic_only" if mode == "pre_llm_only" else "answered"
        degraded_reason = ""
        answer = ""
        llm_request: dict[str, Any] = {}
        requested_fragment_type = self._requested_fragment_type(user_query, request.sub_intent)
        if router_result.intent in {"OPENAPI_GENERATION", "OPENAPI_FROM_DOCUMENTATION"}:
            openapi_result = self._openapi_generator.generate(
                evidence_bundle,
                request.sub_intent,
                requested_fragment_type=requested_fragment_type,
            )
            llm_request = self._build_llm_request(
                question=user_query,
                intent=router_result.intent,
                sub_intent=request.sub_intent,
                evidence_bundle=evidence_bundle,
                prompt_name=prompt_name,
                log_context="graph.project_qa.docs.openapi",
                api_contract=openapi_result,
            )
            if mode == "pre_llm_only":
                answer_mode, degraded_reason, output_valid = self._evaluate_openapi_gate(
                    user_query=user_query,
                    sub_intent=request.sub_intent,
                    router_result=router_result,
                    openapi_result=openapi_result,
                    exact_anchor_match=exact_anchor_match,
                )
                answer = openapi_result.raw_yaml if answer_mode != "degraded" else "Недостаточно contract evidence для OpenAPI."
            else:
                answer = self._generate_openapi_answer(
                    user_query,
                    router_result.intent,
                    request.sub_intent,
                    evidence_bundle,
                    openapi_result,
                    trace=trace,
                )
                output_valid, llm_details = self._openapi_postprocessor.validate(
                    answer,
                    require_paths=request.sub_intent != "OPENAPI_FRAGMENT_GENERATE",
                )
                if not output_valid:
                    answer = openapi_result.raw_yaml
                openapi_result = openapi_result.model_copy(
                    update={"diagnostics": {**openapi_result.diagnostics, "llm_validation": llm_details}}
                )
                answer_mode, degraded_reason, output_valid, answer = self._finalize_openapi_answer(
                    answer=answer,
                    router_result=router_result,
                    openapi_result=openapi_result,
                    output_valid=output_valid,
                    exact_anchor_match=exact_anchor_match,
                )
        else:
            llm_request = self._build_llm_request(
                question=user_query,
                intent=router_result.intent,
                sub_intent=request.sub_intent,
                evidence_bundle=evidence_bundle,
                prompt_name=prompt_name,
                log_context="graph.project_qa.docs.answer",
            )
            if mode == "pre_llm_only":
                answer_mode, degraded_reason = self._evaluate_docs_gate(
                    raw_rows=raw_rows,
                    sub_intent=request.sub_intent,
                    matched_anchor_type=router_result.matched_anchor_type,
                    exact_anchor_match=exact_anchor_match,
                    matched_anchor_value=router_result.matched_anchor_value,
                )
                output_valid = answer_mode != "degraded"
            else:
                answer = self._generate_docs_answer(
                    user_query,
                    router_result.intent,
                    request.sub_intent,
                    evidence_bundle,
                    trace=trace,
                )
                answer_mode, degraded_reason, answer = self._finalize_docs_answer(
                    answer=answer,
                    raw_rows=raw_rows,
                    sub_intent=request.sub_intent,
                    matched_anchor_type=router_result.matched_anchor_type,
                    exact_anchor_match=exact_anchor_match,
                    matched_anchor_value=router_result.matched_anchor_value,
                )
        gate_decision, gate_decision_reason, gate_missing_requirements, gate_satisfied_requirements = self._build_gate_details(
            intent=router_result.intent,
            sub_intent=request.sub_intent,
            answer_mode=answer_mode,
            degraded_reason=degraded_reason,
            raw_rows=raw_rows,
            exact_anchor_match=exact_anchor_match,
            matched_anchor_type=router_result.matched_anchor_type,
            matched_anchor_value=router_result.matched_anchor_value,
            openapi_result=openapi_result,
            router_result=router_result,
        )
        if trace is not None:
            trace.module("evidence_gate").log(
                "evaluated",
                {
                    "decision": gate_decision,
                    "reason": gate_decision_reason,
                    "missing": gate_missing_requirements,
                    "satisfied": gate_satisfied_requirements,
                },
            )
        diagnostics = self._diagnostics_builder.build(
            intent=router_result.intent,
            sub_intent=request.sub_intent,
            planned_layers=list(request.requested_layers),
            executed_layers=list(retrieval_report.get("executed_layers") or request.requested_layers),
            non_empty_layers=self._non_empty_layers(raw_rows, retrieval_report, request.sub_intent),
            layer_diagnostics=dict(retrieval_report.get("layer_diagnostics") or {}),
            evidence_bundle=evidence_bundle,
            openapi_result=openapi_result,
            prompt_used=prompt_name,
            llm_mode=llm_mode,
            answer_mode=answer_mode,
            output_valid=output_valid,
            matched_intent_source=router_result.matched_intent_source,
            matched_anchor_type=router_result.matched_anchor_type,
            matched_anchor_value=router_result.matched_anchor_value,
            exact_anchor_match=exact_anchor_match,
            query_entity_candidates=self._docs_signals.query_entity_candidates(user_query),
            resolved_entity_candidates=self._exact_anchor_matcher.resolved_entity_candidates(raw_rows),
            query_anchor_candidates=self._docs_signals.query_anchor_candidates(user_query),
            resolved_anchor_candidates=self._exact_anchor_matcher.resolved_anchor_candidates(raw_rows),
            anchor_candidates=list(anchor_selection.get("anchor_candidates") or []),
            selected_anchor=anchor_selection.get("selected_anchor"),
            anchor_selection_reason=str(anchor_selection.get("anchor_selection_reason") or ""),
            anchor_match_type=str(anchor_selection.get("anchor_match_type") or ""),
            docs_layers_with_hits=self._non_empty_layers(raw_rows, retrieval_report, request.sub_intent),
            gate_decision=gate_decision,
            gate_decision_reason=gate_decision_reason,
            gate_missing_requirements=gate_missing_requirements,
            gate_satisfied_requirements=gate_satisfied_requirements,
            requested_fragment_type=("method" if request.sub_intent == "OPENAPI_METHOD_GENERATE" else requested_fragment_type),
            fragment_evidence_found=self._fragment_evidence_found(requested_fragment_type, openapi_result),
            fragment_missing_requirements=self._fragment_missing_requirements(requested_fragment_type, openapi_result),
            prompt=llm_request,
            degraded_reason=degraded_reason or None,
            fallback_used=bool(dict(retrieval_report.get("fallback") or {}).get("used")),
            code_intents_stubbed=False,
        )
        timings["execution"] = _ms(t2)
        return DocsQAPipelineResult(
            user_query=user_query,
            rag_session_id=rag_session_id,
            router_result=router_result,
            retrieval_request=request,
            evidence_bundle=evidence_bundle,
            answer=answer,
            diagnostics=diagnostics,
            openapi_result=openapi_result,
            prompt_name=prompt_name,
            llm_request=llm_request,
            output_valid=output_valid,
            answer_mode=answer_mode,
            degraded_reason=degraded_reason,
            raw_rows=raw_rows,
            timings_ms=timings,
            mode=mode,
        )

    def _generate_docs_answer(self, question: str, intent: str, sub_intent: str, evidence_bundle, trace=None) -> str:
        if self._llm is None:
            return self._answer_synthesizer.synthesize(question, evidence_bundle)
        payload = self._prompt_payload_builder.build(
            question=question,
            intent=intent,
            sub_intent=sub_intent,
            evidence_bundle=evidence_bundle,
        )
        prompt_name = self._prompt_selector.select(intent=intent, sub_intent=sub_intent, answer_mode="normal")
        return self._llm.generate(
            prompt_name,
            payload,
            log_context="graph.project_qa.docs.answer",
            trace=trace.module("llm") if trace is not None else None,
        ).strip()

    def _generate_openapi_answer(self, question: str, intent: str, sub_intent: str, evidence_bundle, api_contract, trace=None) -> str:
        if self._llm is None:
            return api_contract.raw_yaml
        payload = self._prompt_payload_builder.build(
            question=question,
            intent=intent,
            sub_intent=sub_intent,
            evidence_bundle=evidence_bundle,
            api_contract=api_contract,
        )
        prompt_name = self._prompt_selector.select(intent=intent, sub_intent=sub_intent, answer_mode="normal")
        return self._llm.generate(
            prompt_name,
            payload,
            log_context="graph.project_qa.docs.openapi",
            trace=trace.module("llm") if trace is not None else None,
        ).strip()

    def _llm_mode(self, intent: str, sub_intent: str) -> str:
        if sub_intent == "RELATED_DOCS_EXPLAIN":
            return "graph_summary"
        if intent == "GENERAL_QA":
            return "prose"
        if intent in {"OPENAPI_GENERATION", "OPENAPI_FROM_DOCUMENTATION"}:
            return "yaml"
        return "prose"

    def _build_llm_request(
        self,
        *,
        question: str,
        intent: str,
        sub_intent: str,
        evidence_bundle,
        prompt_name: str,
        log_context: str,
        api_contract=None,
    ) -> dict[str, Any]:
        user_prompt = self._prompt_payload_builder.build(
            question=question,
            intent=intent,
            sub_intent=sub_intent,
            evidence_bundle=evidence_bundle,
            api_contract=api_contract,
        )
        if self._llm is None:
            return {
                "prompt_name": prompt_name,
                "system_prompt": "You are a helpful assistant.",
                "user_prompt": user_prompt,
                "log_context": log_context,
            }
        return self._llm.build_request(prompt_name, user_prompt, log_context=log_context)

    def _finalize_docs_answer(
        self,
        *,
        answer: str,
        raw_rows: list[dict],
        sub_intent: str,
        matched_anchor_type: str,
        exact_anchor_match: bool,
        matched_anchor_value: str | None,
    ) -> tuple[str, str, str]:
        if self._should_reject_on_exact_anchor(
            sub_intent=sub_intent,
            raw_rows=raw_rows,
            matched_anchor_type=matched_anchor_type,
            matched_anchor_value=matched_anchor_value,
            exact_anchor_match=exact_anchor_match,
        ):
            return "degraded", "not_found_exact_anchor", "Не найдено точное совпадение по запрошенному docs anchor."
        if not raw_rows:
            return "degraded", "retrieval_empty", "По документации не найдено релевантных данных."
        if not answer.strip():
            return "degraded", "insufficient_docs_evidence", "Недостаточно подтвержденных данных в документации."
        return "answered", "", answer

    def _finalize_openapi_answer(
        self,
        *,
        answer: str,
        router_result,
        openapi_result,
        output_valid: bool,
        exact_anchor_match: bool,
    ) -> tuple[str, str, bool, str]:
        requested_endpoint = str(router_result.matched_anchor_value or "").strip()
        generated_endpoint = str(openapi_result.path or "").strip()
        diagnostics = dict(openapi_result.diagnostics or {})
        if requested_endpoint and not exact_anchor_match:
            return "degraded", "not_found_exact_anchor", False, "Не найден точный endpoint в документации."
        if requested_endpoint and generated_endpoint and requested_endpoint != generated_endpoint:
            return "degraded", "generated_endpoint_mismatch", False, "OpenAPI сгенерирован не по запрошенному endpoint."
        if str(getattr(router_result.retrieval_spec.filters, "doc_type", "") or "") != "api_method":
            return "degraded", "wrong_doc_type_match", False, "Для OpenAPI не найден подходящий api_method evidence."
        if not generated_endpoint:
            return "degraded", "insufficient_docs_evidence", False, "Недостаточно contract evidence для OpenAPI."
        has_partial_contract = any(
            (
                openapi_result.request_schema is not None,
                openapi_result.response_schema is not None,
                diagnostics.get("status_codes_found"),
                diagnostics.get("payload_description_found"),
            )
        )
        if not has_partial_contract:
            return "degraded", "insufficient_docs_evidence", False, "Недостаточно contract evidence для OpenAPI."
        if not output_valid:
            return "structured_spec_partial", "invalid_llm_output_fallback", False, answer
        if openapi_result.method and openapi_result.response_schema is not None and openapi_result.request_schema is not None:
            return "structured_spec", "", True, answer
        if openapi_result.method or diagnostics.get("status_codes_found") or diagnostics.get("payload_description_found"):
            return "structured_spec_partial", "answered_with_gaps", True, answer
        return "degraded", "insufficient_docs_evidence", False, "Недостаточно contract evidence для OpenAPI."

    def _evaluate_docs_gate(
        self,
        *,
        raw_rows: list[dict],
        sub_intent: str,
        matched_anchor_type: str,
        exact_anchor_match: bool,
        matched_anchor_value: str | None,
    ) -> tuple[str, str]:
        if self._should_reject_on_exact_anchor(
            sub_intent=sub_intent,
            raw_rows=raw_rows,
            matched_anchor_type=matched_anchor_type,
            matched_anchor_value=matched_anchor_value,
            exact_anchor_match=exact_anchor_match,
        ):
            return "degraded", "not_found_exact_anchor"
        if not raw_rows:
            return "degraded", "retrieval_empty"
        return "ready", ""

    def _evaluate_openapi_gate(
        self,
        *,
        user_query: str,
        sub_intent: str,
        router_result,
        openapi_result,
        exact_anchor_match: bool,
    ) -> tuple[str, str, bool]:
        requested_endpoint = str(router_result.matched_anchor_value or "").strip()
        generated_endpoint = str(openapi_result.path or "").strip()
        diagnostics = dict(openapi_result.diagnostics or {})
        requested_fragment_type = self._requested_fragment_type(user_query, sub_intent)
        has_operation_signal = bool(openapi_result.method) or bool(diagnostics.get("status_codes_found")) or bool(diagnostics.get("payload_description_found"))
        has_contract_detail = any(
            (
                openapi_result.request_schema is not None,
                openapi_result.response_schema is not None,
                diagnostics.get("status_codes_found"),
                diagnostics.get("request_fields_found"),
                diagnostics.get("response_fields_found"),
                diagnostics.get("payload_description_found"),
            )
        )
        if requested_endpoint and not exact_anchor_match:
            return "degraded", "not_found_exact_anchor", False
        if requested_endpoint and generated_endpoint and requested_endpoint != generated_endpoint:
            return "degraded", "generated_endpoint_mismatch", False
        if str(getattr(router_result.retrieval_spec.filters, "doc_type", "") or "") != "api_method":
            return "degraded", "wrong_doc_type_match", False
        if not generated_endpoint or not has_operation_signal:
            return "degraded", "insufficient_docs_evidence", False
        if sub_intent == "OPENAPI_FRAGMENT_GENERATE":
            return self._evaluate_fragment_gate(requested_fragment_type, openapi_result)
        if not openapi_result.method and not diagnostics.get("status_codes_found"):
            return "degraded", "insufficient_docs_evidence", False
        if not has_contract_detail:
            return "degraded", "insufficient_docs_evidence", False
        if openapi_result.request_schema is None or openapi_result.response_schema is None:
            return "ready_partial", "answered_with_gaps", True
        return "ready", "", True

    def _build_gate_details(
        self,
        *,
        intent: str,
        sub_intent: str,
        answer_mode: str,
        degraded_reason: str,
        raw_rows: list[dict],
        exact_anchor_match: bool,
        matched_anchor_type: str,
        matched_anchor_value: str | None,
        openapi_result,
        router_result,
    ) -> tuple[str, str, list[str], list[str]]:
        satisfied: list[str] = []
        missing: list[str] = []
        relation_hits = self._relation_hits(raw_rows)
        if raw_rows:
            satisfied.append("retrieval_non_empty")
        else:
            missing.append("retrieval_non_empty")
        if matched_anchor_type in {"endpoint", "entity"} and matched_anchor_value:
            if exact_anchor_match or (sub_intent == "RELATED_DOCS_EXPLAIN" and relation_hits > 0):
                satisfied.append("exact_anchor_match")
            else:
                missing.append("exact_anchor_match")
        if intent in {"OPENAPI_GENERATION", "OPENAPI_FROM_DOCUMENTATION"} and openapi_result is not None:
            if openapi_result.path:
                satisfied.append("path_found")
            else:
                missing.append("path_found")
            diagnostics = openapi_result.diagnostics
            if openapi_result.method:
                satisfied.append("http_method_found")
            else:
                missing.append("http_method_found")
            if diagnostics.get("operation_semantics_found"):
                satisfied.append("operation_semantics_found")
            else:
                missing.append("operation_semantics_found")
            if diagnostics.get("request_payload_found"):
                satisfied.append("request_payload_found")
            if diagnostics.get("response_payload_found"):
                satisfied.append("response_payload_found")
            if openapi_result.request_schema is not None:
                satisfied.append("request_fields_found")
            if openapi_result.response_schema is not None:
                satisfied.append("response_fields_found")
            if openapi_result.request_schema is None and openapi_result.response_schema is None:
                missing.append("contract_fields_found")
            if diagnostics.get("status_codes_found"):
                satisfied.append("status_codes_found")
            else:
                missing.append("status_codes_found")
            if diagnostics.get("payload_description_found"):
                satisfied.append("payload_description_found")
            else:
                missing.append("payload_description_found")
            if diagnostics.get("content_type_found"):
                satisfied.append("content_type_found")
            if diagnostics.get("examples_found"):
                satisfied.append("examples_found")
            if str(getattr(router_result.retrieval_spec.filters, "doc_type", "") or "") == "api_method":
                satisfied.append("api_method_filter")
            else:
                missing.append("api_method_filter")
        gate = answer_mode
        if gate in {"diagnostic_only", "ready", "answered", "structured_spec"}:
            gate = "allow" if not degraded_reason else "reject"
        elif gate in {"ready_partial", "structured_spec_partial"}:
            gate = "partial"
        elif gate == "degraded":
            gate = "reject"
        return gate, self._gate_reason(gate, degraded_reason, sub_intent, relation_hits), missing, satisfied

    def _non_empty_layers(self, rows: list[dict], report: dict[str, Any], sub_intent: str) -> list[str]:
        diagnostics = dict(report.get("layer_diagnostics") or {})
        if sub_intent == "RELATED_DOCS_EXPLAIN" and int(dict(diagnostics.get("D5_RELATION_GRAPH") or {}).get("hits") or 0) > 0:
            return ["D5_RELATION_GRAPH"]
        if diagnostics:
            return [layer for layer, payload in diagnostics.items() if int(dict(payload).get("hits") or 0) > 0]
        return self._layers_with_hits(rows)

    def _layers_with_hits(self, rows: list[dict]) -> list[str]:
        result: list[str] = []
        for row in rows:
            layer = str(row.get("layer") or "")
            if layer.startswith("D") and layer not in result:
                result.append(layer)
        return result

    def _should_reject_on_exact_anchor(
        self,
        *,
        sub_intent: str,
        raw_rows: list[dict],
        matched_anchor_type: str,
        matched_anchor_value: str | None,
        exact_anchor_match: bool,
    ) -> bool:
        if matched_anchor_type not in {"endpoint", "entity"} or not matched_anchor_value or exact_anchor_match:
            return False
        if sub_intent == "RELATED_DOCS_EXPLAIN" and self._has_relation_hits(raw_rows):
            return False
        return True

    def _has_relation_hits(self, rows: list[dict]) -> bool:
        return self._relation_hits(rows) > 0

    def _relation_hits(self, rows: list[dict]) -> int:
        return sum(1 for row in rows if str(row.get("layer") or "") == "D5_RELATION_GRAPH")

    def _gate_reason(self, gate: str, degraded_reason: str, sub_intent: str, relation_hits: int) -> str:
        if degraded_reason:
            return degraded_reason
        if sub_intent == "RELATED_DOCS_EXPLAIN" and relation_hits > 0:
            return "relation_evidence_available"
        if gate == "partial":
            return "partial_evidence_available"
        if gate == "allow":
            return "evidence_sufficient"
        return ""

    def _evaluate_fragment_gate(self, requested_fragment_type: str | None, openapi_result) -> tuple[str, str, bool]:
        diagnostics = dict(openapi_result.diagnostics or {})
        if requested_fragment_type == "request_schema":
            if openapi_result.request_schema is not None:
                return "ready", "", True
            if diagnostics.get("request_fields_found") or diagnostics.get("request_payload_found") or diagnostics.get("payload_description_found"):
                return "ready_partial", "fragment_payload_only", True
            return "degraded", "insufficient_docs_evidence", False
        if requested_fragment_type == "response_schema":
            if openapi_result.response_schema is not None:
                return "ready", "", True
            if diagnostics.get("response_fields_found") or diagnostics.get("response_payload_found") or diagnostics.get("status_codes_found"):
                return "ready_partial", "fragment_response_partial", True
            return "degraded", "insufficient_docs_evidence", False
        if requested_fragment_type == "parameters":
            if diagnostics.get("has_method") or diagnostics.get("content_type_found"):
                return "ready_partial", "fragment_parameters_partial", True
            return "degraded", "insufficient_docs_evidence", False
        if openapi_result.request_schema is not None or openapi_result.response_schema is not None:
            return "ready_partial", "answered_with_gaps", True
        if diagnostics.get("payload_description_found"):
            return "ready_partial", "fragment_payload_only", True
        return "degraded", "insufficient_docs_evidence", False

    def _requested_fragment_type(self, user_query: str, sub_intent: str) -> str | None:
        if sub_intent != "OPENAPI_FRAGMENT_GENERATE":
            return None
        lowered = user_query.lower()
        if "request" in lowered:
            return "request_schema"
        if "response" in lowered:
            return "response_schema"
        if "parameter" in lowered or "парамет" in lowered:
            return "parameters"
        return "schema_fragment"

    def _fragment_evidence_found(self, requested_fragment_type: str | None, openapi_result) -> list[str]:
        if openapi_result is None or requested_fragment_type is None:
            return []
        diagnostics = dict(openapi_result.diagnostics or {})
        found: list[str] = []
        if openapi_result.path:
            found.append("path")
        if openapi_result.method:
            found.append("method")
        if diagnostics.get("operation_semantics_found"):
            found.append("operation_semantics")
        if diagnostics.get("payload_description_found"):
            found.append("payload_description")
        if diagnostics.get("status_codes_found"):
            found.append("status_codes")
        if requested_fragment_type == "request_schema" and openapi_result.request_schema is not None:
            found.append("request_schema")
        if requested_fragment_type == "request_schema" and diagnostics.get("request_fields_found"):
            found.append("request_fields")
        if requested_fragment_type == "response_schema" and openapi_result.response_schema is not None:
            found.append("response_schema")
        if requested_fragment_type == "response_schema" and diagnostics.get("response_fields_found"):
            found.append("response_fields")
        return found

    def _fragment_missing_requirements(self, requested_fragment_type: str | None, openapi_result) -> list[str]:
        if openapi_result is None or requested_fragment_type is None:
            return []
        diagnostics = dict(openapi_result.diagnostics or {})
        missing: list[str] = []
        if not openapi_result.path:
            missing.append("path")
        if requested_fragment_type == "request_schema":
            if openapi_result.request_schema is None and not diagnostics.get("payload_description_found"):
                missing.append("request_payload_evidence")
        elif requested_fragment_type == "response_schema":
            if openapi_result.response_schema is None and not diagnostics.get("status_codes_found"):
                missing.append("response_payload_evidence")
        elif openapi_result.request_schema is None and openapi_result.response_schema is None:
            missing.append("schema_fragment")
        return missing


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

    return ConversationState()


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

    return RepoContext()


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