from __future__ import annotations

import re
from typing import Any


RISK_LEVEL_LABELS: dict[str, str] = {
    "low": "低风险",
    "medium": "中风险",
    "high": "高风险",
    "critical": "极高风险",
}

RISK_SCORE_MODEL_VERSION = "risk_score_v3"

RISK_SCORE_WEIGHTS: dict[str, float] = {
    "impact": 0.35,
    "certainty": 0.25,
    "evidence": 0.15,
    "exception": 0.10,
    "action": 0.10,
    "sensitivity": 0.05,
}

LEVEL_SCORE_MAP: dict[str, int] = {
    "none": 0,
    "very_low": 12,
    "low": 25,
    "medium": 55,
    "high": 78,
    "critical": 94,
}

LEVEL_ALIASES: dict[str, str] = {
    "极低": "very_low",
    "很低": "very_low",
    "低": "low",
    "低风险": "low",
    "中": "medium",
    "中等": "medium",
    "中风险": "medium",
    "高": "high",
    "高风险": "high",
    "极高": "critical",
    "严重": "critical",
    "重大": "critical",
    "极高风险": "critical",
    "very_low": "very_low",
    "low": "low",
    "medium": "medium",
    "high": "high",
    "critical": "critical",
    "extreme": "critical",
}

ACTION_SCORE_MAP: dict[str, int] = {
    "observe": 20,
    "remind": 35,
    "supplement": 48,
    "manual_review": 65,
    "return": 78,
    "block": 94,
}

ACTION_ALIASES: dict[str, str] = {
    "提示": "remind",
    "提醒": "remind",
    "补充": "supplement",
    "补充说明": "supplement",
    "人工复核": "manual_review",
    "复核": "manual_review",
    "审核": "manual_review",
    "退回": "return",
    "驳回": "return",
    "阻断": "block",
    "禁止": "block",
    "禁止提交": "block",
    "observe": "observe",
    "remind": "remind",
    "supplement": "supplement",
    "manual_review": "manual_review",
    "review": "manual_review",
    "return": "return",
    "reject": "return",
    "block": "block",
}

SENSITIVE_CATEGORY_SCORES: dict[str, int] = {
    "travel": 70,
    "hotel": 76,
    "transport": 68,
    "meal": 72,
    "meeting": 58,
    "training": 48,
    "communication": 36,
    "office": 30,
    "welfare": 42,
}


def calculate_risk_rule_score(
    *,
    natural_language: str,
    draft: dict[str, Any],
    fields: list[Any],
    expense_category: str | None,
    expense_category_label: str,
    requires_attachment: bool,
) -> dict[str, Any]:
    evidence = _read_scoring_evidence(draft)
    text = _join_text(
        natural_language,
        draft.get("description"),
        draft.get("condition_summary"),
        draft.get("formula"),
        draft.get("message_template"),
    )
    hard_signal_text = _strip_negated_risk_context(text)
    template_key = str(draft.get("template_key") or "").strip()
    field_keys = _read_string_list(draft.get("field_keys"))
    condition_count = len(draft.get("conditions") if isinstance(draft.get("conditions"), list) else [])

    components = {
        "impact": _component_score(
            evidence.get("impact_level"),
            _infer_impact_score(hard_signal_text, template_key=template_key),
        ),
        "certainty": _component_score(
            evidence.get("violation_certainty"),
            _infer_certainty_score(template_key=template_key, condition_count=condition_count),
        ),
        "evidence": _component_score(
            evidence.get("evidence_strength"),
            _infer_evidence_score(field_keys, fields=fields, requires_attachment=requires_attachment),
        ),
        "exception": _component_score(
            evidence.get("exception_dependence"),
            _infer_exception_score(text, draft),
        ),
        "action": _action_score(
            evidence.get("control_action"),
            _infer_action_score(text, draft),
        ),
        "sensitivity": _component_score(
            evidence.get("business_sensitivity"),
            _infer_sensitivity_score(hard_signal_text, expense_category=expense_category),
        ),
    }
    raw_score = _clamp_score(
        round(sum(components[key] * RISK_SCORE_WEIGHTS[key] for key in RISK_SCORE_WEIGHTS))
    )
    score, calibration = _calibrate_score(
        raw_score,
        text=text,
        hard_signal_text=hard_signal_text,
        components=components,
    )
    level = risk_level_from_score(score)
    return {
        "score": score,
        "level": level,
        "level_label": RISK_LEVEL_LABELS[level],
        "model": RISK_SCORE_MODEL_VERSION,
        "weights": RISK_SCORE_WEIGHTS,
        "components": components,
        "calibration": calibration,
        "ai_evidence": evidence,
        "basis": {
            "template_key": template_key,
            "field_count": len(field_keys),
            "condition_count": condition_count,
            "expense_category": expense_category,
            "expense_category_label": expense_category_label,
            "requires_attachment": requires_attachment,
        },
    }


def apply_risk_score_to_draft(draft: dict[str, Any], score_result: dict[str, Any]) -> dict[str, Any]:
    level_label = str(score_result.get("level_label") or RISK_LEVEL_LABELS["medium"]).strip()
    updated = dict(draft)
    flow = dict(updated.get("flow") if isinstance(updated.get("flow"), dict) else {})
    flow["fail"] = _replace_or_append_risk_label(
        str(flow.get("fail") or "命中风险，进入人工复核"),
        level_label,
    )
    updated["flow"] = flow
    description = str(updated.get("description") or "").strip()
    if description:
        updated["description"] = _replace_or_append_risk_label(description, level_label)
    updated["risk_scoring_evidence"] = score_result.get("ai_evidence") or {}
    return updated


def risk_level_from_score(score: int | float) -> str:
    normalized = _clamp_score(round(float(score or 0)))
    if normalized <= 30:
        return "low"
    if normalized <= 60:
        return "medium"
    if normalized <= 80:
        return "high"
    return "critical"


def _read_scoring_evidence(draft: dict[str, Any]) -> dict[str, Any]:
    value = draft.get("risk_scoring_evidence")
    return dict(value) if isinstance(value, dict) else {}


def _component_score(value: Any, fallback: int) -> int:
    normalized = LEVEL_ALIASES.get(str(value or "").strip().lower())
    if normalized:
        return LEVEL_SCORE_MAP[normalized]
    normalized = LEVEL_ALIASES.get(str(value or "").strip())
    if normalized:
        return LEVEL_SCORE_MAP[normalized]
    return _clamp_score(fallback)


def _action_score(value: Any, fallback: int) -> int:
    normalized = ACTION_ALIASES.get(str(value or "").strip().lower())
    if normalized:
        return ACTION_SCORE_MAP[normalized]
    normalized = ACTION_ALIASES.get(str(value or "").strip())
    if normalized:
        return ACTION_SCORE_MAP[normalized]
    return _clamp_score(fallback)


def _infer_impact_score(text: str, *, template_key: str) -> int:
    if _contains_any(text, "伪造", "虚假", "重复报销", "骗取", "套取", "假票", "发票重复"):
        return 94
    if _contains_any(text, "禁止", "阻断", "退回", "超预算", "超标准", "高风险", "不一致"):
        return 78
    if template_key in {"field_compare_v1", "composite_rule_v1"}:
        return 70
    if _contains_any(text, "缺少", "未上传", "不完整", "补充"):
        return 48
    return 42


def _infer_certainty_score(*, template_key: str, condition_count: int) -> int:
    if template_key == "composite_rule_v1":
        return 84 if condition_count >= 2 else 72
    if template_key == "field_compare_v1":
        return 80
    if template_key == "field_required_v1":
        return 86
    if template_key == "keyword_match_v1":
        return 58
    return 55


def _infer_evidence_score(
    field_keys: list[str],
    *,
    fields: list[Any],
    requires_attachment: bool,
) -> int:
    keys = set(field_keys)
    if any(key.startswith("attachment.") for key in keys) or requires_attachment:
        return 82
    field_by_key = {str(getattr(field, "key", "") or "").strip(): field for field in fields}
    if any(str(getattr(field_by_key.get(key), "source", "") or "") == "claim" for key in keys):
        return 62
    return 50


def _infer_exception_score(text: str, draft: dict[str, Any]) -> int:
    exception_keywords = _read_string_list(draft.get("exception_keywords"))
    if exception_keywords or _contains_any(text, "无说明", "没有说明", "未说明", "补充说明", "合理说明"):
        return 66
    if _contains_any(text, "人工判断", "疑似", "可能"):
        return 74
    return 35


def _infer_action_score(text: str, draft: dict[str, Any]) -> int:
    flow = draft.get("flow") if isinstance(draft.get("flow"), dict) else {}
    corpus = _join_text(text, flow.get("fail"), draft.get("message_template"))
    if _contains_any(corpus, "禁止提交", "阻断", "不允许提交", "拦截"):
        return 94
    if _contains_any(corpus, "退回", "驳回", "重新提交"):
        return 78
    if _contains_any(corpus, "人工复核", "复核", "审核"):
        return 65
    if _contains_any(corpus, "提醒", "提示", "补齐"):
        return 35
    if _contains_any(corpus, "补充", "说明"):
        return 48
    return 35


def _infer_sensitivity_score(text: str, *, expense_category: str | None) -> int:
    if _contains_any(text, "发票", "票据", "虚假", "重复报销", "伪造"):
        return 88
    if expense_category in SENSITIVE_CATEGORY_SCORES:
        return SENSITIVE_CATEGORY_SCORES[expense_category or ""]
    if _contains_any(text, "差旅", "住宿", "招待", "交通"):
        return 72
    return 45


def _calibrate_score(
    score: int,
    *,
    text: str,
    hard_signal_text: str,
    components: dict[str, int],
) -> tuple[int, dict[str, Any]]:
    calibration: dict[str, Any] = {"raw_score": score, "rules": []}
    if _is_low_control_rule(text, hard_signal_text, components):
        calibrated = min(score, 30)
        calibration["rules"].append(
            {
                "name": "explicit_low_control_cap",
                "score_before": score,
                "score_after": calibrated,
                "reason": "规则语义明确为低风险，且控制动作仅为提醒、提示、补齐或补充说明。",
            }
        )
        score = calibrated
    return score, calibration


def _is_low_control_rule(text: str, hard_signal_text: str, components: dict[str, int]) -> bool:
    if not _contains_any(text, "低风险", "轻微风险", "轻微", "提醒", "提示", "补齐"):
        return False
    if _contains_any(
        hard_signal_text,
        "高风险",
        "极高风险",
        "严重",
        "重大",
        "造假",
        "虚假",
        "伪造",
        "重复报销",
        "骗取",
        "套取",
        "不一致",
        "超预算",
        "超标准",
        "阻断",
        "禁止",
        "退回",
        "驳回",
    ):
        return False
    return components.get("action", 100) <= ACTION_SCORE_MAP["supplement"]


def _strip_negated_risk_context(text: str) -> str:
    normalized = str(text or "")
    if not normalized:
        return ""
    negated_risk_pattern = (
        r"(?:暂未|未|没有|无|不存在)"
        r"(?:发现|存在)?"
        r"[^，。；;,.]*"
        r"(?:冲突|异常|重复报销|造假|虚假|伪造|超标|超预算|高风险|不一致|迹象)"
        r"[^，。；;,.]*"
    )
    return re.sub(negated_risk_pattern, "", normalized)


def _replace_or_append_risk_label(value: str, level_label: str) -> str:
    normalized = str(value or "").strip()
    if not normalized:
        return f"命中{level_label}，进入人工复核"
    replaced = re.sub(r"(低风险|中风险|高风险|极高风险)", level_label, normalized)
    if replaced != normalized:
        return replaced
    if "风险" in normalized:
        return normalized
    return f"{normalized}（{level_label}）"


def _read_string_list(value: Any) -> list[str]:
    if not isinstance(value, list):
        return []
    return [str(item or "").strip() for item in value if str(item or "").strip()]


def _contains_any(text: str, *keywords: str) -> bool:
    return any(keyword and keyword in text for keyword in keywords)


def _join_text(*parts: Any) -> str:
    return "\n".join(str(part or "") for part in parts if str(part or "").strip())


def _clamp_score(value: int | float) -> int:
    return max(0, min(100, int(round(float(value)))))
