from __future__ import annotations

import math
from copy import deepcopy
from typing import Any

from app.modules.rag.retrieval.test_filter import is_test_path


def build_router_plan(route_result) -> dict[str, Any]:
    query_plan = route_result.query_plan
    retrieval_spec = route_result.retrieval_spec
    constraints = route_result.retrieval_constraints
    layers = [str(item.layer_id) for item in list(retrieval_spec.layer_queries or [])]
    path_scope = list(getattr(retrieval_spec.filters, "path_scope", []) or [])
    return {
        "intent": route_result.intent,
        "sub_intent": query_plan.sub_intent,
        "graph_id": route_result.graph_id,
        "retrieval_profile": route_result.retrieval_profile,
        "conversation_mode": route_result.conversation_mode,
        "layers": layers,
        "symbol_kind_hint": query_plan.symbol_kind_hint,
        "symbol_candidates": list(query_plan.symbol_candidates or []),
        "keyword_hints": list(query_plan.keyword_hints or []),
        "path_hints": list(query_plan.path_hints or []),
        "path_scope": path_scope,
        "doc_scope_hints": list(query_plan.doc_scope_hints or []),
        "retrieval_constraints": {
            "include_globs": list(constraints.include_globs or []),
            "exclude_globs": list(constraints.exclude_globs or []),
            "prefer_globs": list(constraints.prefer_globs or []),
            "test_file_globs": list(constraints.test_file_globs or []),
            "test_symbol_patterns": list(constraints.test_symbol_patterns or []),
            "max_candidates": int(constraints.max_candidates),
            "fuzzy_symbol_search": {
                "enabled": bool(constraints.fuzzy_symbol_search.enabled),
                "max_distance": int(constraints.fuzzy_symbol_search.max_distance),
                "top_k": int(constraints.fuzzy_symbol_search.top_k),
            },
        },
    }


def init_diagnostics(route_result, *, router_ms: int, include_retrieval: bool) -> dict[str, Any]:
    diagnostics = {
        "router_plan": build_router_plan(route_result),
        "execution": {
            "executed_layers": [],
            "retrieval_mode_by_layer": {},
            "top_k_by_layer": {},
            "filters_by_layer": {},
            "repo_scope": {"repo_id": None, "workspace_id": None},
        },
        "retrieval": None,
        "constraint_violations": [],
        "timings_ms": {
            "router": int(router_ms),
            "symbol_resolution": 0,
            "retrieval_total": 0,
            "retrieval_by_layer": {},
            "merge_rank": 0,
            "prompt_build": 0,
            "llm_call": 0,
        },
        "prompt": None,
    }
    if include_retrieval:
        diagnostics["retrieval"] = {
            "requests": [],
            "applied": [],
            "fallback": {"used": False, "reason": None},
        }
    return diagnostics


def clone_diagnostics(diagnostics: dict[str, Any]) -> dict[str, Any]:
    return deepcopy(diagnostics)


def apply_retrieval_report(diagnostics: dict[str, Any], report: dict[str, Any] | None) -> None:
    if not report:
        return
    execution = diagnostics.get("execution", {})
    execution["executed_layers"] = list(report.get("executed_layers", []) or [])
    execution["retrieval_mode_by_layer"] = dict(report.get("retrieval_mode_by_layer", {}) or {})
    execution["top_k_by_layer"] = dict(report.get("top_k_by_layer", {}) or {})
    execution["filters_by_layer"] = dict(report.get("filters_by_layer", {}) or {})
    retrieval = diagnostics.get("retrieval")
    if isinstance(retrieval, dict):
        retrieval["requests"] = list(report.get("requests", []) or [])
        retrieval["applied"] = list(report.get("applied", []) or [])
        retrieval["fallback"] = dict(report.get("fallback", {"used": False, "reason": None}) or {})
    timings = diagnostics.get("timings_ms", {})
    timings["retrieval_by_layer"] = dict(report.get("retrieval_by_layer_ms", {}) or {})


def assign_repo_scope(
    diagnostics: dict[str, Any],
    rag_rows: list[dict[str, Any]],
    *,
    expected_repo_id: str | None = None,
    expected_workspace_id: str | None = None,
) -> None:
    repo_ids = sorted({str(_row_repo_id(row)).strip() for row in rag_rows if str(_row_repo_id(row)).strip()})
    workspace_ids = sorted({str(_row_workspace_id(row)).strip() for row in rag_rows if str(_row_workspace_id(row)).strip()})
    repo_id = expected_repo_id or (repo_ids[0] if len(repo_ids) == 1 else None)
    workspace_id = expected_workspace_id or (workspace_ids[0] if len(workspace_ids) == 1 else None)
    diagnostics["execution"]["repo_scope"] = {
        "repo_id": repo_id,
        "workspace_id": workspace_id,
        "returned_repo_ids": repo_ids,
        "returned_workspace_ids": workspace_ids,
    }


def validate_constraints(
    route_result,
    rag_rows: list[dict[str, Any]],
    symbol_resolution: dict[str, Any],
    *,
    expected_repo_id: str | None = None,
) -> list[dict[str, Any]]:
    violations: list[dict[str, Any]] = []
    query_plan = route_result.query_plan
    retrieval_spec = route_result.retrieval_spec
    path_scope = list(getattr(retrieval_spec.filters, "path_scope", []) or [])
    row_paths = {str(row.get("path") or "") for row in rag_rows}
    if query_plan.sub_intent == "OPEN_FILE" and path_scope:
        if not any(path in row_paths for path in path_scope):
            violations.append(
                _violation(
                    "PATH_SCOPE_NOT_SATISFIED",
                    "error",
                    {"path_scope": path_scope, "returned_paths": sorted(row_paths)},
                    "Use exact path_scope retrieval and disable broad fallback for OPEN_FILE.",
                )
            )
    requested_layers = [str(item.layer_id) for item in list(retrieval_spec.layer_queries or [])]
    row_layers = {str(row.get("layer") or "") for row in rag_rows}
    for layer in requested_layers:
        if layer not in row_layers:
            violations.append(
                _violation(
                    "LAYER_MISSING",
                    "warn",
                    {"layer": layer, "requested_layers": requested_layers},
                    "Increase top_k for this layer or relax constraints for retrieval.",
                )
            )
    exclude_globs = list(route_result.retrieval_constraints.exclude_globs or [])
    if _tests_excluded(exclude_globs):
        test_paths = sorted({path for path in row_paths if is_test_path(path)})
        if test_paths:
            violations.append(
                _violation(
                    "TESTS_EXCLUSION_VIOLATED",
                    "warn",
                    {"exclude_globs": exclude_globs, "test_paths": test_paths},
                    "Tighten test exclude globs and apply them at pre-rank filtering stage.",
                )
            )
    if expected_repo_id:
        mismatched = sorted(
            {
                repo_id
                for repo_id in {str(_row_repo_id(row)).strip() for row in rag_rows if str(_row_repo_id(row)).strip()}
                if repo_id != expected_repo_id
            }
        )
        if mismatched:
            violations.append(
                _violation(
                    "REPO_SCOPE_MISMATCH",
                    "error",
                    {"expected_repo_id": expected_repo_id, "returned_repo_ids": mismatched},
                    "Use rag_session_id/repo scope that matches the expected repository.",
                )
            )
    symbol_candidates = list(query_plan.symbol_candidates or [])
    status = str(symbol_resolution.get("status") or "not_requested")
    if query_plan.sub_intent == "EXPLAIN" and symbol_candidates and status != "resolved":
        violations.append(
            _violation(
                "SYMBOL_RESOLUTION_FAILED",
                "warn",
                {"status": status, "symbol_candidates": symbol_candidates},
                "Increase symbol layer budget or improve fuzzy symbol resolution settings.",
            )
        )
    return violations


def build_prompt_diagnostics(
    *,
    system_prompt: str,
    user_prompt: str,
    rag_rows: list[dict[str, Any]],
    prompt_template_id: str | None = None,
    system_prompt_version: str | None = None,
) -> dict[str, Any]:
    evidence_chars = sum(len(str(row.get("content") or "")) for row in rag_rows)
    tokens_in_estimate = max(1, int(math.ceil((len(system_prompt) + len(user_prompt)) / 4)))
    per_layer: dict[str, dict[str, Any]] = {}
    for row in rag_rows:
        layer = str(row.get("layer") or "")
        bucket = per_layer.setdefault(layer, {"layer": layer, "count": 0, "paths": set()})
        bucket["count"] += 1
        bucket["paths"].add(str(row.get("path") or ""))
    evidence_summary = [
        {"layer": item["layer"], "count": item["count"], "unique_paths": len(item["paths"])}
        for item in per_layer.values()
    ]
    return {
        "prompt_stats": {
            "tokens_in_estimate": tokens_in_estimate,
            "evidence_rows": len(rag_rows),
            "evidence_chars": evidence_chars,
        },
        "evidence_summary": evidence_summary,
        "prompt_template_id": prompt_template_id,
        "system_prompt_version": system_prompt_version,
    }


def _tests_excluded(exclude_globs: list[str]) -> bool:
    tokens = ("tests", "test", "__tests__", "mocks", "fixtures", "stubs", "conftest")
    return any(any(token in str(item).lower() for token in tokens) for item in exclude_globs)


def _violation(violation_type: str, severity: str, details: dict[str, Any], suggested_fix: str) -> dict[str, Any]:
    return {
        "type": violation_type,
        "severity": severity,
        "details": details,
        "suggested_fix": suggested_fix,
    }


def _row_repo_id(row: dict[str, Any]) -> str | None:
    metadata = dict(row.get("metadata") or {})
    return metadata.get("repo_id")


def _row_workspace_id(row: dict[str, Any]) -> str | None:
    metadata = dict(row.get("metadata") or {})
    return metadata.get("workspace_id")
