"""Курация фактов из EvidenceBundle для сценариев explain, architecture, trace_flow."""

from __future__ import annotations

import re
from typing import Any

from app.modules.agent.runtime.steps.context.contracts import CodeChunkItem, EvidenceBundle

_CALL_RE = re.compile(r"([A-Za-z_][\w\.]*)\s*\(")
_FIELD_RE = re.compile(r"self\.(\w+)")
_SIGNATURE_RE = re.compile(r"(?P<name>[A-Za-z_][\w\.]*)\((?P<args>[^)]*)\)")
_RETRIEVAL_LABELS = ("dataflow_slice", "execution_trace", "trace_path")
_RELATION_VERBS = {
    "calls": "вызывает",
    "instantiates": "создает",
    "inherits": "наследует",
    "imports": "импортирует",
    "reads_attr": "читает",
    "writes_attr": "записывает",
}


def build_curated_answer_facts(bundle: EvidenceBundle) -> dict[str, Any]:
    target = str(bundle.resolved_target or "").strip()
    semantic_hints = _semantic_hints(bundle.code_chunks)
    primary_chunks = [chunk for chunk in bundle.code_chunks if chunk.layer != "C4_SEMANTIC_ROLES"]
    relations = _normalized_relations(bundle.relations)
    target_relations = [relation for relation in relations if _is_target_relation(relation, target)]

    return {
        "scenario": (bundle.resolved_sub_intent or "EXPLAIN").upper(),
        "semantic_hints": semantic_hints,
        "primary_chunk_count": len(primary_chunks),
        "relation_count": len(relations),
        "explain": _explain_facts(bundle, primary_chunks, target_relations),
        "architecture": _architecture_facts(bundle, primary_chunks, target_relations),
        "trace_flow": _trace_flow_facts(bundle, primary_chunks, target_relations),
    }


def _explain_facts(bundle: EvidenceBundle, chunks: list[CodeChunkItem], relations: list[dict[str, Any]]) -> dict[str, Any]:
    target = str(bundle.resolved_target or "").strip()
    signatures = [_signature_payload(chunk) for chunk in chunks if chunk.layer == "C1_SYMBOL_CATALOG"]
    target_signatures = [item for item in signatures if _is_target_symbol(item["name"], target)]
    # Конкретные имена методов: полный qname и короткое отображаемое имя (Class.method или method)
    methods_full = _unique(item["name"] for item in target_signatures if item["kind"] == "method")
    methods_short = _unique(_short_method_name(name) for name in methods_full)
    methods = methods_short or methods_full[:6]
    constructor_args = _unique(
        arg
        for item in target_signatures
        if item["name"].endswith(".__init__")
        for arg in item["args"]
        if arg not in {"self", "cls"}
    )
    # Конкретные вызовы: из relations и fallback из кода
    calls = _unique(
        _display_call_target(relation["target"])
        for relation in relations
        if relation["edge_type"] in {"calls", "instantiates"}
    )
    if not calls:
        calls = _fallback_calls(chunks, target)
    # Поля: из relations и из self.attr в коде
    fields = _unique(
        relation["target"].split(".", 1)[-1]
        for relation in relations
        if relation["edge_type"] in {"reads_attr", "writes_attr"}
    )
    if not fields:
        fields = _unique(field for chunk in chunks if _chunk_matches_target(chunk, target) for field in _FIELD_RE.findall(chunk.content or ""))
    # Зависимости: импорты и инстанциации
    dependencies = _unique(
        _display_dependency_target(relation["target"])
        for relation in relations
        if relation["edge_type"] in {"imports", "instantiates"}
    )
    required_files = _unique(chunk.path for chunk in chunks if _chunk_matches_target(chunk, target) and chunk.path)
    required_symbols = _unique(item["name"] for item in target_signatures if item["name"]) or _unique([target] if target else bundle.target_symbol_candidates)

    fact_gaps: list[str] = []
    if not methods:
        fact_gaps.append("Методы целевой сущности не подтверждены в извлеченных C0/C1 данных.")
    if not calls:
        fact_gaps.append("Конкретные вызовы целевой сущности не подтверждены в C2/C0.")
    if not dependencies:
        fact_gaps.append("Явные зависимости целевой сущности не подтверждены.")
    if methods and not (calls or dependencies):
        fact_gaps.append("Есть методы, но вызовы/зависимости не извлечены — опирайся только на перечисленные методы и поля.")

    return {
        "required_symbols": required_symbols[:8],
        "required_methods": methods[:8],
        "required_calls": calls[:8],
        "required_fields": fields[:8],
        "required_constructor_args": constructor_args[:8],
        "required_dependencies": dependencies[:8],
        "required_files": required_files[:4],
        "fact_gaps": fact_gaps,
    }


def _architecture_facts(bundle: EvidenceBundle, chunks: list[CodeChunkItem], relations: list[dict[str, Any]]) -> dict[str, Any]:
    target = str(bundle.resolved_target or "").strip()
    # Компоненты: target, из связей, из заголовков чанков (классы/модули)
    from_relations = _unique(
        [_component_name(relation["source"]) for relation in relations]
        + [_component_name(relation["target"]) for relation in relations]
    )
    from_chunks = _unique(
        _component_name(chunk.title) or _component_name(str(dict(chunk.metadata or {}).get("qname") or ""))
        for chunk in chunks
        if chunk.layer in ("C1_SYMBOL_CATALOG", "C0_SOURCE_CHUNKS") and (target and _chunk_matches_target(chunk, target))
    )
    components = _unique(([target] if target else []) + from_relations + from_chunks)
    relation_rows = [
        {
            "source": _component_name(relation["source"]),
            "verb": relation["verb"],
            "target": _component_name(relation["target"]),
            "source_method": relation["source"],
            "target_method": relation["target"],
            "path": relation["path"],
            "line_span": relation["line_span"],
            "edge_type": relation["edge_type"],
        }
        for relation in relations
        if _component_name(relation["source"]) and _component_name(relation["target"])
    ]
    relation_rows = [row for row in relation_rows if row["source"] != row["target"]]
    relation_verbs = _unique(row["verb"] for row in relation_rows if row["verb"])
    # Краткие формулировки связей для обязательного упоминания в ответе
    relation_summaries = [f"{r['source']} {r['verb']} {r['target']}" for r in relation_rows[:8]]
    fact_gaps: list[str] = []
    if not relation_rows:
        fact_gaps.append("Concrete code edges между компонентами не подтверждены.")
    if components and not relation_rows:
        fact_gaps.append("Компоненты есть, но связи между ними не извлечены — не придумывай связи.")

    return {
        "required_components": components[:10],
        "required_relations": relation_rows[:10],
        "required_relation_summaries": relation_summaries[:8],
        "required_relation_verbs": relation_verbs[:8],
        "required_creation_edges": [row for row in relation_rows if row["edge_type"] == "instantiates"][:4],
        "required_call_edges": [row for row in relation_rows if row["edge_type"] == "calls"][:4],
        "required_registration_edges": [row for row in relation_rows if row["edge_type"] == "imports"][:4],
        "required_data_edges": [row for row in relation_rows if row["edge_type"] in {"reads_attr", "writes_attr"}][:4],
        "fact_gaps": fact_gaps,
        "forbidden_labels": list(_RETRIEVAL_LABELS),
    }


def _trace_flow_facts(bundle: EvidenceBundle, chunks: list[CodeChunkItem], relations: list[dict[str, Any]]) -> dict[str, Any]:
    target = str(bundle.resolved_target or "").strip()
    sorted_relations = sorted(
        relations,
        key=lambda item: (item["path"], item["sort_line"], item["source"], item["target"]),
    )
    flow_steps = [
        {
            "step": index,
            "source": relation["source"],
            "verb": relation["verb"],
            "target": relation["target"],
            "path": relation["path"],
            "line_span": relation["line_span"],
        }
        for index, relation in enumerate(sorted_relations, start=1)
    ]
    # Упорядоченные короткие формулировки шагов для обязательного отражения в ответе
    ordered_step_descriptions = [
        f"{i}. {_component_name(s['source'])} {s['verb']} {_display_call_target(s['target'])}"
        for i, s in enumerate(flow_steps[:8], start=1)
    ]
    required_calls = _unique(_display_call_target(item["target"]) for item in flow_steps)
    fact_gaps: list[str] = []
    if len(flow_steps) < 2:
        fact_gaps.append("Полная последовательность шагов не подтверждена; виден только частичный flow.")
    if not flow_steps:
        fact_gaps.append("Конкретные sequence edges для flow не подтверждены.")
    if flow_steps and len(flow_steps) < 3:
        fact_gaps.append("Цепочка короткая — не заявляй полноту потока.")

    return {
        "required_flow_steps": flow_steps[:10],
        "required_calls": required_calls[:10],
        "required_sequence_edges": flow_steps[:10],
        "ordered_step_descriptions": ordered_step_descriptions[:8],
        "required_files": _unique(chunk.path for chunk in chunks if _chunk_matches_target(chunk, target) and chunk.path)[:4],
        "fact_gaps": fact_gaps,
    }


def _semantic_hints(chunks: list[CodeChunkItem]) -> list[dict[str, Any]]:
    hints: list[dict[str, Any]] = []
    for chunk in chunks:
        if chunk.layer != "C4_SEMANTIC_ROLES":
            continue
        meta = dict(chunk.metadata or {})
        hints.append(
            {
                "symbol": meta.get("symbol_name") or chunk.title,
                "role": meta.get("role"),
                "path": chunk.path,
                "confidence": meta.get("confidence"),
            }
        )
    return hints[:8]


def _normalized_relations(relations: list[dict[str, Any]]) -> list[dict[str, Any]]:
    normalized: list[dict[str, Any]] = []
    for relation in relations:
        metadata = dict(relation.get("metadata") or {})
        edge_type = str(metadata.get("edge_type") or relation.get("edge_type") or "").strip()
        source = _clean_endpoint(str(metadata.get("src_qname") or relation.get("source") or "").strip())
        target = _clean_endpoint(str(metadata.get("dst_ref") or relation.get("target") or "").strip())
        if not edge_type or not source or not target:
            continue
        normalized.append(
            {
                "edge_type": edge_type,
                "source": source,
                "target": target,
                "verb": _RELATION_VERBS.get(edge_type, edge_type),
                "path": str(relation.get("path") or ""),
                "line_span": _line_span(relation),
                "sort_line": int(relation.get("start_line") or 0),
            }
        )
    return normalized


def _signature_payload(chunk: CodeChunkItem) -> dict[str, Any]:
    meta = dict(chunk.metadata or {})
    signature = str(meta.get("signature") or chunk.content or "").strip()
    match = _SIGNATURE_RE.search(signature)
    args = [part.strip() for part in match.group("args").split(",") if part.strip()] if match else []
    return {
        "name": _clean_endpoint(str(meta.get("qname") or chunk.title or "").strip()),
        "kind": str(meta.get("kind") or "").strip(),
        "args": args,
    }


def _fallback_calls(chunks: list[CodeChunkItem], target: str) -> list[str]:
    values: list[str] = []
    for chunk in chunks:
        if not _chunk_matches_target(chunk, target):
            continue
        values.extend(_display_call_target(item) for item in _CALL_RE.findall(chunk.content or ""))
    return _unique(values)


def _chunk_matches_target(chunk: CodeChunkItem, target: str) -> bool:
    if not target:
        return True
    title = _clean_endpoint(chunk.title)
    symbol = _clean_endpoint(str(dict(chunk.metadata or {}).get("qname") or ""))
    return _is_target_symbol(title, target) or _is_target_symbol(symbol, target) or target.lower() in (chunk.content or "").lower()


def _is_target_relation(relation: dict[str, Any], target: str) -> bool:
    if not target:
        return True
    return _is_target_symbol(relation["source"], target) or _component_name(relation["source"]) == target or _component_name(relation["target"]) == target


def _is_target_symbol(name: str, target: str) -> bool:
    clean_name = _clean_endpoint(name)
    clean_target = _clean_endpoint(target)
    if not clean_target:
        return True
    return clean_name == clean_target or clean_name.startswith(f"{clean_target}.")


def _component_name(value: str) -> str:
    clean = _clean_endpoint(value)
    if not clean:
        return ""
    parts = clean.split(".")
    if len(parts) <= 1:
        return clean
    if len(parts) == 2:
        return clean if parts[1][:1].isupper() else parts[0]
    return ".".join(parts[:-1])


def _short_method_name(qname: str) -> str:
    """Короткое отображаемое имя метода для ответа (Class.method или method())."""
    clean = _clean_endpoint(qname)
    if not clean:
        return ""
    parts = clean.split(".")
    tail = parts[-1]
    if len(parts) >= 2 and parts[-2][:1].isupper():
        return f"{parts[-2]}.{tail}()" if tail != "__init__" else f"{parts[-2]}.__init__()"
    return f"{tail}()" if tail else clean


def _display_call_target(value: str) -> str:
    clean = _clean_endpoint(value)
    if not clean:
        return ""
    tail = clean.rsplit(".", 1)[-1]
    return f"{tail}()" if tail and tail != clean else clean


def _display_dependency_target(value: str) -> str:
    clean = _clean_endpoint(value)
    if not clean:
        return ""
    return _component_name(clean) or clean


def _clean_endpoint(value: str) -> str:
    clean = str(value or "").strip()
    if not clean:
        return ""
    for label in _RETRIEVAL_LABELS:
        clean = clean.replace(f".{label}", "")
        clean = clean.replace(label, "")
    return clean.strip(".: ")


def _line_span(item: dict[str, Any]) -> str:
    start = item.get("start_line")
    end = item.get("end_line")
    if start is None and end is None:
        return "?:?"
    return f"{start or '?'}-{end or '?'}"


def _unique(values: Any) -> list[str]:
    seen: set[str] = set()
    result: list[str] = []
    for value in values:
        text = str(value or "").strip()
        if not text or text in seen:
            continue
        seen.add(text)
        result.append(text)
    return result
