"""Сборка нормализованного RetrievalResult из сырых строк retrieval и отчёта."""

from __future__ import annotations

import re

from app.modules.agent.runtime.steps.context.contracts import CodeChunkItem, LayerOutcome, RetrievalResult
from app.modules.rag.retrieval.test_filter import is_test_path

_ROUTE_RE = re.compile(r'@[\w\.]+\.(get|post|put|delete|patch|options|head)\(\s*["\']([^"\']+)["\']')
_DEF_RE = re.compile(r"async\s+def\s+(\w+)|def\s+(\w+)")


def build_retrieval_result(
    raw_rows: list[dict],
    retrieval_report: dict | None,
    symbol_resolution: dict | None,
) -> RetrievalResult:
    report = retrieval_report or {}
    sym = symbol_resolution or {}
    layers_seen: set[str] = set()
    code_chunks: list[CodeChunkItem] = []
    relations: list[dict] = []
    semantic_hints: list[dict] = []
    entrypoint_rows: list[CodeChunkItem] = []
    test_candidates: list[dict] = []
    file_candidates: list[str] = []
    target_symbols: list[str] = []

    for row in raw_rows:
        chunk = _to_chunk(row)
        layer = chunk.layer
        path = chunk.path
        if layer:
            layers_seen.add(layer)
        if path and path not in file_candidates:
            file_candidates.append(path)
        code_chunks.append(chunk)

        if layer == "C1_SYMBOL_CATALOG":
            title = chunk.title.strip()
            if title and title not in target_symbols:
                target_symbols.append(title)
            continue
        if layer == "C2_DEPENDENCY_GRAPH":
            relations.append(_relation_payload(chunk))
            continue
        if layer == "C4_SEMANTIC_ROLES":
            semantic_hints.append(_semantic_hint_payload(chunk))
            continue
        if layer == "C3_ENTRYPOINTS":
            entrypoint_rows.append(chunk)
            continue
        if is_test_path(path):
            test_candidates.append({"path": path, "title": chunk.title, "content": chunk.content[:300]})

    entrypoints = _normalize_entrypoints(entrypoint_rows, code_chunks)
    executed = list(report.get("executed_layers") or [])
    missing_layers = [lid for lid in executed if lid not in layers_seen]
    layer_outcomes = _layer_outcomes(raw_rows, executed, layers_seen, report)

    resolved = str(sym.get("resolved_symbol") or "").strip() or None
    status = str(sym.get("status") or "not_requested")

    return RetrievalResult(
        target_symbol_candidates=target_symbols,
        resolved_symbol=resolved,
        symbol_resolution_status=status,
        file_candidates=file_candidates,
        code_chunks=code_chunks,
        relations=relations,
        semantic_hints=semantic_hints,
        entrypoints=entrypoints,
        test_candidates=test_candidates,
        layer_outcomes=layer_outcomes,
        missing_layers=missing_layers,
        raw_rows=raw_rows,
        retrieval_report=report,
    )


def _to_chunk(row: dict) -> CodeChunkItem:
    return CodeChunkItem(
        layer=str(row.get("layer") or ""),
        path=str(row.get("path") or ""),
        title=str(row.get("title") or ""),
        content=str(row.get("content") or ""),
        start_line=row.get("span_start"),
        end_line=row.get("span_end"),
        metadata=dict(row.get("metadata") or {}),
    )


def _normalize_entrypoints(entrypoint_rows: list[CodeChunkItem], code_chunks: list[CodeChunkItem]) -> list[dict]:
    chunks_by_path: dict[str, list[CodeChunkItem]] = {}
    for chunk in code_chunks:
        chunks_by_path.setdefault(chunk.path, []).append(chunk)

    normalized: list[dict] = []
    seen: set[tuple[str, str, str, int | None, int | None]] = set()
    for chunk in entrypoint_rows:
        item = _entrypoint_payload(chunk, chunks_by_path.get(chunk.path, []))
        key = (
            str(item.get("http_method") or ""),
            str(item.get("route_path") or ""),
            str(item.get("path") or ""),
            chunk.start_line,
            chunk.end_line,
        )
        if key in seen:
            continue
        seen.add(key)
        normalized.append(item)
    return sorted(normalized, key=_entrypoint_sort_key)


def _relation_payload(chunk: CodeChunkItem) -> dict:
    metadata = dict(chunk.metadata or {})
    edge_type = str(metadata.get("edge_type") or "").strip()
    src_qname = str(metadata.get("src_qname") or "").strip()
    dst_ref = str(metadata.get("dst_ref") or "").strip()
    return {
        "path": chunk.path,
        "start_line": chunk.start_line,
        "end_line": chunk.end_line,
        "edge_type": edge_type,
        "source": src_qname,
        "target": dst_ref,
        "source_component": _component_name(src_qname),
        "target_component": _component_name(dst_ref),
        "has_retrieval_label": "dataflow_slice" in dst_ref or "dataflow_slice" in src_qname,
        "metadata": metadata,
        "content": chunk.content[:500],
    }


def _semantic_hint_payload(chunk: CodeChunkItem) -> dict:
    metadata = dict(chunk.metadata or {})
    return {
        "path": chunk.path,
        "title": chunk.title,
        "symbol": metadata.get("symbol_name") or chunk.title,
        "role": metadata.get("role"),
        "confidence": metadata.get("confidence"),
        "content": chunk.content[:300],
    }


def _component_name(value: str) -> str:
    cleaned = str(value or "").replace(".dataflow_slice", "").strip(". ")
    if "." not in cleaned:
        return cleaned
    head, tail = cleaned.rsplit(".", 1)
    return head if tail and not tail[:1].isupper() else cleaned


def _entrypoint_payload(chunk: CodeChunkItem, siblings: list[CodeChunkItem]) -> dict:
    methods = [str(item).strip().upper() for item in list(dict(chunk.metadata).get("lang_payload", {}).get("methods", [])) if str(item).strip()]
    method = methods[0] if methods else ""
    route_path = _infer_route_path(chunk, siblings)
    handler_symbol = _infer_handler_symbol(chunk, siblings)
    return {
        "path": chunk.path,
        "title": chunk.title,
        "start_line": chunk.start_line,
        "end_line": chunk.end_line,
        "http_method": method,
        "route_path": route_path,
        "handler_symbol": handler_symbol,
        "handler_symbol_id": dict(chunk.metadata).get("handler_symbol_id"),
        "route_or_command": dict(chunk.metadata).get("route_or_command"),
        "metadata": chunk.metadata,
    }


def _infer_route_path(chunk: CodeChunkItem, siblings: list[CodeChunkItem]) -> str:
    for candidate in siblings:
        if candidate.layer != "C0_SOURCE_CHUNKS":
            continue
        match = _ROUTE_RE.search(_window_text(candidate, chunk.start_line))
        if match:
            return match.group(2).strip()
    return ""


def _infer_handler_symbol(chunk: CodeChunkItem, siblings: list[CodeChunkItem]) -> str:
    for candidate in siblings:
        if candidate.layer != "C0_SOURCE_CHUNKS":
            continue
        match = _DEF_RE.search(_window_text(candidate, chunk.start_line, before=2, after=12))
        if match:
            return match.group(1) or match.group(2) or ""
    return ""


def _window_text(chunk: CodeChunkItem, target_line: int | None, *, before: int = 6, after: int = 8) -> str:
    text = chunk.content or ""
    if not text or not target_line or not chunk.start_line:
        return text
    lines = text.splitlines()
    offset = max(0, target_line - chunk.start_line - before)
    end = min(len(lines), target_line - chunk.start_line + after)
    if offset >= end:
        return text
    return "\n".join(lines[offset:end])


def _entrypoint_sort_key(item: dict) -> tuple[int, int, str, str, int]:
    route_path = str(item.get("route_path") or "")
    method = str(item.get("http_method") or "")
    score = 0
    if route_path:
        score += 3
    if route_path and route_path != "/":
        score += 3
    if method:
        score += 1
    return (-score, len(route_path or "zzzz"), method, str(item.get("path") or ""), int(item.get("start_line") or 0))


def _layer_outcomes(raw_rows: list[dict], executed: list[str], layers_seen: set[str], report: dict) -> list[LayerOutcome]:
    layer_outcomes = [
        LayerOutcome(
            layer_id=layer_id,
            hit_count=sum(1 for r in raw_rows if str(r.get("layer") or "") == layer_id),
            empty=layer_id not in layers_seen,
            fallback_used=bool((report.get("fallback") or {}).get("used") and report.get("retrieval_mode_by_layer", {}).get(layer_id)),
        )
        for layer_id in executed
    ]
    if not executed and raw_rows:
        for layer in layers_seen:
            layer_outcomes.append(
                LayerOutcome(
                    layer_id=layer,
                    hit_count=sum(1 for r in raw_rows if str(r.get("layer") or "") == layer),
                    empty=False,
                    fallback_used=False,
                )
            )
    return layer_outcomes
