"""Post-evidence gate: валидация черновика ответа и решение repair/return."""

from __future__ import annotations

import re

from pydantic import BaseModel, ConfigDict, Field

from app.modules.agent.runtime.steps.context.answer_fact_curator import build_curated_answer_facts
from app.modules.agent.runtime.steps.context.contracts import EvidenceBundle


class RuntimeValidationResult(BaseModel):
    model_config = ConfigDict(extra="forbid")

    passed: bool = False
    action: str = "return"
    reasons: list[str] = Field(default_factory=list)


_TOKEN_RE = re.compile(r"[a-zA-Zа-яА-Я0-9_/]+")
_VAGUE_PHRASES = (
    "ряд аргументов",
    "имеет responsibilities",
    "имеет responsibility",
    "ключевой компонент",
    "центральный компонент",
    "играет роль",
    "управляет системой",
    "этап пайплайна",
    "инициализация сервисов",
    "регистрация основных служб",
    "различные аргументы",
    "различные подпакеты",
    "основные службы",
    "представляет собой",
    "используется в службах",
)
_OPTIMISTIC_TRACE_CLAIMS = (
    "полностью восстанавливается",
    "полный поток выполнения",
    "полностью прослеживается",
    "полный поток виден",
    "полная цепочка",
    "весь поток",
)


class RuntimePostEvidenceGate:
    def validate(
        self,
        *,
        answer: str,
        answer_mode: str,
        degraded_message: str,
        sub_intent: str,
        user_query: str,
        evidence_pack: EvidenceBundle | None,
    ) -> RuntimeValidationResult:
        normalized = (answer or "").strip()
        if not normalized:
            return RuntimeValidationResult(passed=False, action="repair", reasons=["empty_answer"])
        if answer_mode in {"degraded", "insufficient"} and "недостат" not in normalized.lower():
            return RuntimeValidationResult(passed=False, action="repair", reasons=["degraded_answer_missing_guardrail"])
        if answer_mode == "not_found" and "не найден" not in normalized.lower():
            return RuntimeValidationResult(passed=False, action="repair", reasons=["not_found_answer_missing_phrase"])
        if answer_mode == "ambiguous" and "не удалось однозначно разрешить" not in normalized.lower():
            return RuntimeValidationResult(passed=False, action="repair", reasons=["ambiguous_answer_missing_phrase"])
        if degraded_message and answer_mode != "normal" and len(normalized) < 24:
            return RuntimeValidationResult(passed=False, action="repair", reasons=["answer_too_short"])
        if answer_mode != "normal" or evidence_pack is None:
            return RuntimeValidationResult(passed=True, action="return")

        reasons = self._normal_answer_reasons(normalized.lower(), sub_intent.upper(), user_query, evidence_pack)
        if reasons:
            return RuntimeValidationResult(passed=False, action="repair", reasons=_dedupe(reasons))
        return RuntimeValidationResult(passed=True, action="return")

    def _normal_answer_reasons(self, answer: str, sub_intent: str, user_query: str, evidence_pack: EvidenceBundle) -> list[str]:
        reasons: list[str] = []
        if sub_intent == "FIND_ENTRYPOINTS":
            reasons.extend(self._validate_entrypoints(answer, user_query, evidence_pack))
        elif sub_intent == "EXPLAIN":
            reasons.extend(self._validate_explain(answer, evidence_pack))
        elif sub_intent == "ARCHITECTURE":
            reasons.extend(self._validate_architecture(answer, evidence_pack))
        elif sub_intent == "TRACE_FLOW":
            reasons.extend(self._validate_trace_flow(answer, evidence_pack))
        return reasons

    def _validate_entrypoints(self, answer: str, user_query: str, evidence_pack: EvidenceBundle) -> list[str]:
        confirmed = [item for item in list(evidence_pack.entrypoints or []) if item.get("http_method") and item.get("route_path")]
        if not confirmed:
            return []
        query_tokens = self._tokens(user_query)
        matched = [item for item in confirmed if self._tokens(str(item.get("route_path") or "")) & query_tokens]
        required = matched or confirmed[:1]
        reasons = []
        if any(f"{item['http_method']} {item['route_path']}".lower() not in answer for item in required):
            reasons.append("missing_confirmed_route")
        if matched and any(token in query_tokens for token in {"health", "/health"}) and "не обнаруж" in answer:
            reasons.append("contradicts_confirmed_route")
        return reasons

    def _validate_explain(self, answer: str, evidence_pack: EvidenceBundle) -> list[str]:
        facts = build_curated_answer_facts(evidence_pack)
        explain = dict(facts.get("explain") or {})
        reasons = self._validate_target_focus(answer, evidence_pack)
        reasons.extend(self._vagueness_reasons(answer, "explain"))

        methods = list(explain.get("required_methods") or [])
        calls = list(explain.get("required_calls") or [])
        dependencies = list(explain.get("required_dependencies") or [])
        fields = list(explain.get("required_fields") or [])
        has_any_required = bool(methods or calls or dependencies or fields)
        matches = 0
        if methods and not self._mentions_fact_group(answer, methods):
            reasons.append("missing_concrete_methods")
        elif methods:
            matches += 1
        if calls and not self._mentions_fact_group(answer, calls):
            reasons.append("missing_concrete_calls")
        elif calls:
            matches += 1
        if dependencies and not self._mentions_fact_group(answer, dependencies):
            reasons.append("missing_concrete_dependencies")
        elif dependencies:
            matches += 1
        if fields and not self._mentions_fact_group(answer, fields):
            reasons.append("missing_concrete_fields")
        elif fields:
            matches += 1
        if has_any_required and matches == 0:
            reasons.append("too_vague_for_explain")
        if has_any_required and self._answer_is_generic_with_facts(answer, methods + calls + dependencies + fields):
            reasons.append("ignores_concrete_explain_facts")
        if self._semantic_leakage(answer, facts, has_concrete_support=matches > 0):
            reasons.append("semantic_labels_without_code_edges")
        return reasons

    def _validate_architecture(self, answer: str, evidence_pack: EvidenceBundle) -> list[str]:
        facts = build_curated_answer_facts(evidence_pack)
        architecture = dict(facts.get("architecture") or {})
        reasons = self._validate_target_focus(answer, evidence_pack)
        reasons.extend(self._vagueness_reasons(answer, "architecture"))

        components = list(architecture.get("required_components") or [])
        relations = list(architecture.get("required_relations") or [])
        verbs = list(architecture.get("required_relation_verbs") or [])
        if components and not self._mentions_fact_group(answer, components):
            reasons.append("missing_concrete_components")
        if relations and not self._mentions_relations(answer, relations):
            reasons.append("missing_concrete_relations")
        if verbs and not self._mentions_fact_group(answer, verbs):
            reasons.append("missing_relation_verbs")
        forbidden = architecture.get("forbidden_labels") or []
        if any(label.lower() in answer.lower() for label in forbidden):
            reasons.append("contains_retrieval_artifacts")
        if self._methods_dominate_components(answer, components):
            reasons.append("methods_as_primary_components")
        has_relations = self._mentions_relations(answer, relations)
        has_verbs = self._mentions_fact_group(answer, verbs)
        if relations and (not has_relations or not has_verbs):
            reasons.append("too_vague_for_architecture")
        target = str(evidence_pack.resolved_target or "").strip().lower()
        if target and target in answer and relations and not has_relations:
            reasons.append("target_mentioned_but_no_relations")
        if self._semantic_leakage(answer, facts, has_concrete_support=has_relations):
            reasons.append("semantic_labels_without_code_edges")
        return reasons

    def _validate_trace_flow(self, answer: str, evidence_pack: EvidenceBundle) -> list[str]:
        facts = build_curated_answer_facts(evidence_pack)
        trace = dict(facts.get("trace_flow") or {})
        reasons = self._validate_target_focus(answer, evidence_pack)
        reasons.extend(self._vagueness_reasons(answer, "trace_flow"))

        steps = list(trace.get("required_flow_steps") or [])
        calls = list(trace.get("required_calls") or [])
        ordered_descriptions = list(trace.get("ordered_step_descriptions") or [])
        if steps and not self._mentions_steps(answer, steps):
            reasons.append("missing_flow_steps")
        if steps and not self._mentions_relations(answer, steps):
            reasons.append("missing_sequence_edges")
        if calls and not self._mentions_fact_group(answer, calls):
            reasons.append("missing_concrete_calls")
        if any(claim.lower() in answer.lower() for claim in _OPTIMISTIC_TRACE_CLAIMS):
            reasons.append("overclaims_trace_completeness")
        if steps and not (self._mentions_steps(answer, steps) and self._mentions_relations(answer, steps)):
            reasons.append("too_vague_for_trace_flow")
        if ordered_descriptions and not self._mentions_ordered_steps(answer, ordered_descriptions):
            reasons.append("missing_ordered_flow_steps")
        return reasons

    def _validate_target_focus(self, answer: str, evidence_pack: EvidenceBundle) -> list[str]:
        target = str(evidence_pack.resolved_target or "").strip().lower()
        return [] if not target or target in answer else ["missing_resolved_target"]

    def _mentions_fact_group(self, answer: str, values: list[str]) -> bool:
        return any(alias in answer for value in values for alias in _aliases(value))

    def _mentions_relations(self, answer: str, relations: list[dict]) -> bool:
        for relation in relations:
            source = str(relation.get("source") or "").lower()
            target = str(relation.get("target") or "").lower()
            verb = str(relation.get("verb") or "").lower()
            if source and target and source in answer and target in answer:
                return True
            if source and verb and target and source in answer and verb in answer and target in answer:
                return True
        return False

    def _mentions_steps(self, answer: str, steps: list[dict]) -> bool:
        if "сначала" in answer and "затем" in answer:
            return True
        numeric_steps = sum(1 for marker in ("1.", "2.", "3.") if marker in answer)
        if numeric_steps >= 2:
            return True
        mentioned = sum(1 for step in steps[:3] if self._mentions_relations(answer, [step]))
        return mentioned >= min(2, len(steps[:3]))

    def _mentions_ordered_steps(self, answer: str, ordered_descriptions: list[str]) -> bool:
        """Проверяет, что в ответе отражена хотя бы часть упорядоченных шагов (источник/цель/глагол)."""
        if not ordered_descriptions:
            return True
        answer_lower = answer.lower()
        hits = 0
        for desc in ordered_descriptions[:5]:
            parts = desc.replace(".", " ").split()
            if len(parts) >= 3 and any(p in answer_lower for p in parts if len(p) > 2):
                hits += 1
        return hits >= min(2, len(ordered_descriptions))

    def _answer_is_generic_with_facts(
        self, answer: str, concrete_values: list[str]
    ) -> bool:
        """True, если в ответе есть общие фразы, но почти нет конкретных имён из списка."""
        if not concrete_values:
            return False
        mentioned = sum(1 for v in concrete_values if any(a in answer.lower() for a in _aliases(v)))
        return mentioned == 0 and len(answer) > 100

    def _methods_dominate_components(self, answer: str, components: list[str]) -> bool:
        method_like = re.findall(r"\b[a-z_]+\(\)", answer)
        component_hits = sum(1 for component in components if component.lower() in answer)
        return bool(method_like) and component_hits == 0

    def _semantic_leakage(self, answer: str, facts: dict, *, has_concrete_support: bool) -> bool:
        if has_concrete_support:
            return False
        semantic_roles = [str(item.get("role") or "").strip().lower() for item in facts.get("semantic_hints") or [] if str(item.get("role") or "").strip()]
        return bool(semantic_roles) and any(role in answer for role in semantic_roles)

    def _vagueness_reasons(self, answer: str, scenario: str) -> list[str]:
        if any(phrase in answer for phrase in _VAGUE_PHRASES):
            return [f"too_vague_for_{scenario}"]
        return []

    def _tokens(self, value: str) -> set[str]:
        return {token.lower().strip("/") for token in _TOKEN_RE.findall(value or "") if token.strip("/")}


def _aliases(value: str) -> list[str]:
    text = str(value or "").strip().lower()
    if not text:
        return []
    aliases = [text]
    bare = text.removesuffix("()")
    if bare != text:
        aliases.append(bare)
    tail = bare.rsplit(".", 1)[-1]
    if tail and tail not in aliases:
        aliases.append(tail)
    if tail and f"{tail}()" not in aliases:
        aliases.append(f"{tail}()")
    return aliases


def _dedupe(values: list[str]) -> list[str]:
    seen: set[str] = set()
    result: list[str] = []
    for value in values:
        if value in seen:
            continue
        seen.add(value)
        result.append(value)
    return result
