from __future__ import annotations

import re
from typing import Any, Dict, Iterable, List

from app.services.code_kb.schema import CodeGraphContext, CodeSearchHit
from app.services.consistency.schema import RequirementSnapshot


def _clamp(value: float) -> float:
    return max(0.0, min(1.0, value))


def _tokens(*values: str) -> List[str]:
    text = " ".join(value or "" for value in values).lower()
    return [item for item in re.split(r"[^a-z0-9_\u4e00-\u9fff]+", text) if len(item) >= 2]


def semantic_score(hits: List[CodeSearchHit]) -> float:
    if not hits:
        return 0.0
    top = max(hit.similarity for hit in hits)
    avg = sum(hit.similarity for hit in hits[:3]) / min(3, len(hits))
    return _clamp(top * 0.7 + avg * 0.3)


def acceptance_coverage_score(requirement: RequirementSnapshot, judgment: Dict[str, Any]) -> float:
    criteria = requirement.acceptance_criteria or []
    covered = judgment.get("covered_points") or []
    missing = judgment.get("missing_points") or []
    verdict = judgment.get("verdict")
    if criteria:
        if missing:
            return _clamp((len(criteria) - min(len(missing), len(criteria))) / len(criteria))
        if covered:
            return _clamp(len(covered) / len(criteria))
        return 1.0 if verdict == "implemented" else 0.4 if verdict == "partial" else 0.0
    return {"implemented": 1.0, "partial": 0.55, "conflict": 0.25, "missing": 0.0}.get(verdict, 0.35)


def evidence_strength_score(hits: List[CodeSearchHit]) -> float:
    if not hits:
        return 0.0
    scores: List[float] = []
    for hit in hits[:5]:
        item = hit.evidence
        checks = [
            bool(item.file),
            item.start_line is not None,
            item.end_line is not None,
            bool(item.summary),
            bool(item.logic_flow),
            bool(item.code_snippet),
        ]
        scores.append(sum(1 for value in checks if value) / len(checks))
    return _clamp(sum(scores) / len(scores))


def call_graph_score(contexts: Iterable[CodeGraphContext]) -> float:
    contexts = list(contexts)
    if not contexts:
        return 0.0
    scored = []
    for context in contexts[:5]:
        score = 0.0
        if context.callers:
            score += 0.35
        if context.callees:
            score += 0.35
        if context.call_chains:
            score += 0.30
        scored.append(score)
    return _clamp(sum(scored) / len(scored))


def exact_match_score(requirement: RequirementSnapshot, hits: List[CodeSearchHit]) -> float:
    if not hits:
        return 0.0
    important = _tokens(
        requirement.interface_name or "",
        requirement.interface_type or "",
        requirement.data_source or "",
        requirement.data_destination or "",
        requirement.title or "",
    )
    if not important:
        important = _tokens(requirement.description)[:12]
    if not important:
        return 0.0

    evidence_text = " ".join(
        f"{hit.evidence.name} {hit.evidence.qualified_name} {hit.evidence.summary} {hit.evidence.logic_flow}"
        for hit in hits[:5]
    ).lower()
    matched = sum(1 for token in important if token.lower() in evidence_text)
    return _clamp(matched / len(important))


def coverage_score(
    requirement: RequirementSnapshot,
    hits: List[CodeSearchHit],
    contexts: List[CodeGraphContext],
    judgment: Dict[str, Any],
) -> float:
    score = (
        semantic_score(hits) * 0.25
        + acceptance_coverage_score(requirement, judgment) * 0.30
        + evidence_strength_score(hits) * 0.20
        + call_graph_score(contexts) * 0.15
        + exact_match_score(requirement, hits) * 0.10
    )
    verdict = judgment.get("verdict")
    if verdict == "missing":
        score = min(score, 0.25)
    elif verdict == "uncertain":
        score = min(score, 0.55)
    elif verdict == "conflict":
        score = min(score, 0.45)
    return round(_clamp(score), 4)

