from __future__ import annotations

from copy import deepcopy
from typing import Any

from app.core.agent_enums import AgentAssetDomain
from app.services.risk_rule_generation_interpreter import COMPOSITE_RULE_TEMPLATE_KEY
from app.services.risk_rule_generation_ontology import (
    EXPENSE_BUSINESS_STAGE_LABELS,
    EXPENSE_RISK_CATEGORY_LABELS,
    FIELD_ONTOLOGY,
)

TEMPLATE_GROUPS: tuple[dict[str, str | int], ...] = (
    {"group": "budget", "group_label": "预算", "order": 10},
    {"group": "invoice", "group_label": "票据", "order": 20},
    {"group": "travel", "group_label": "差旅", "order": 30},
    {"group": "entertainment", "group_label": "招待", "order": 40},
    {"group": "procurement_ap", "group_label": "采购/AP", "order": 50},
    {"group": "corporate_card", "group_label": "企业卡", "order": 60},
    {"group": "general", "group_label": "通用", "order": 70},
)

_GROUP_LABELS = {str(item["group"]): str(item["group_label"]) for item in TEMPLATE_GROUPS}
_FIELD_BY_KEY = {field.key: field for field in FIELD_ONTOLOGY}


def list_risk_rule_template_groups() -> list[dict[str, Any]]:
    templates = [_build_template(item) for item in _TEMPLATE_DEFINITIONS]
    groups: list[dict[str, Any]] = []
    for group in TEMPLATE_GROUPS:
        group_code = str(group["group"])
        group_templates = [item for item in templates if item["group"] == group_code]
        groups.append(
            {
                "group": group_code,
                "group_label": str(group["group_label"]),
                "order": int(group["order"]),
                "templates": group_templates,
            }
        )
    return deepcopy(groups)


def list_risk_rule_templates() -> list[dict[str, Any]]:
    return [
        template
        for group in list_risk_rule_template_groups()
        for template in group["templates"]
    ]


def _build_template(definition: dict[str, Any]) -> dict[str, Any]:
    field_keys = list(definition["field_keys"])
    business_stage = str(definition.get("business_stage") or "reimbursement")
    expense_category = definition.get("expense_category")
    group = str(definition["group"])
    return {
        "template_id": definition["template_id"],
        "group": group,
        "group_label": _GROUP_LABELS[group],
        "title": definition["title"],
        "description": definition["description"],
        "business_domain": str(definition.get("business_domain") or AgentAssetDomain.EXPENSE.value),
        "business_stage": business_stage,
        "business_stage_label": EXPENSE_BUSINESS_STAGE_LABELS.get(business_stage, "费用报销"),
        "expense_category": expense_category,
        "expense_category_label": EXPENSE_RISK_CATEGORY_LABELS.get(str(expense_category or ""), ""),
        "requires_attachment": bool(definition.get("requires_attachment")),
        "natural_language": definition["natural_language"],
        "fields": _field_rows(field_keys),
        "dsl_example": _manifest(
            field_keys=field_keys,
            conditions=definition["conditions"],
            hit_logic=definition["hit_logic"],
            summary=definition["summary"],
            message=definition["message"],
            semantic_type=definition["semantic_type"],
        ),
    }


def _field_rows(field_keys: list[str]) -> list[dict[str, str]]:
    rows: list[dict[str, str]] = []
    for key in field_keys:
        field = _FIELD_BY_KEY.get(key)
        if field is None:
            continue
        rows.append(
            {
                "key": field.key,
                "label": field.label,
                "display": f"{field.label}[{field.key}]",
                "source": field.source,
                "type": field.field_type,
            }
        )
    return rows


def _manifest(
    *,
    field_keys: list[str],
    conditions: list[dict[str, Any]],
    hit_logic: dict[str, Any],
    summary: str,
    message: str,
    semantic_type: str,
) -> dict[str, Any]:
    return {
        "template_key": COMPOSITE_RULE_TEMPLATE_KEY,
        "params": {
            "template_key": COMPOSITE_RULE_TEMPLATE_KEY,
            "semantic_type": semantic_type,
            "field_keys": field_keys,
            "conditions": conditions,
            "hit_logic": hit_logic,
            "condition_summary": summary,
            "message_template": message,
            "keywords": [],
        },
    }


_TEMPLATE_DEFINITIONS: tuple[dict[str, Any], ...] = (
    {
        "template_id": "budget_available_balance",
        "group": "budget",
        "title": "费用申请预算余额校验",
        "description": "申请金额超过预算可用余额时提示预算占用风险，适合费用申请阶段前置控制。",
        "business_stage": "expense_application",
        "expense_category": "all",
        "requires_attachment": False,
        "natural_language": (
            "费用申请时，先读取申请金额、预算可用余额、部门、费用类型和申请事由。"
            "若申请金额超过当前可用预算余额，且申请事由中没有预算追加、专项审批或紧急事项说明，"
            "则标记为中风险，要求补充预算审批说明后再继续流转。"
        ),
        "field_keys": [
            "claim.amount",
            "budget.remaining_amount",
            "claim.department_name",
            "item.item_type",
            "claim.reason",
        ],
        "conditions": [
            {
                "id": "amount_exceeds_available_budget",
                "operator": "numeric_compare",
                "left_fields": ["claim.amount"],
                "right_fields": ["budget.remaining_amount"],
                "compare": "gt",
            },
            {
                "id": "missing_budget_exception",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["预算追加", "专项审批", "紧急事项", "预算调整"],
            },
        ],
        "hit_logic": {"all": ["amount_exceeds_available_budget", "missing_budget_exception"]},
        "summary": "申请金额大于预算可用余额，且缺少预算追加或专项审批说明时命中。",
        "message": "申请金额超过预算可用余额，需补充预算审批说明。",
        "semantic_type": "budget_available_balance_check",
    },
    {
        "template_id": "duplicate_invoice_number",
        "group": "invoice",
        "title": "重复发票号码校验",
        "description": "识别同一发票号码在本次提交中重复出现的报销风险。",
        "business_stage": "reimbursement",
        "expense_category": "office",
        "requires_attachment": True,
        "natural_language": (
            "费用报销时，先确认已上传发票或票据附件，再读取附件识别出的发票号码、明细附件编号和报销事由。"
            "若同一发票号码或同一明细附件编号在本次提交中重复出现，且报销事由没有说明拆单、补票或更正提交原因，"
            "则标记为高风险，要求删除重复票据或补充说明。"
        ),
        "field_keys": ["attachment.invoice_no", "item.invoice_id", "claim.reason"],
        "conditions": [
            {
                "id": "invoice_evidence_present",
                "operator": "exists_any",
                "fields": ["attachment.invoice_no", "item.invoice_id"],
            },
            {
                "id": "same_invoice_no_repeated",
                "operator": "duplicate_value",
                "fields": ["attachment.invoice_no", "item.invoice_id"],
            },
            {
                "id": "missing_duplicate_reason",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["拆单", "补票", "更正", "冲红"],
            },
        ],
        "hit_logic": {
            "all": ["invoice_evidence_present", "same_invoice_no_repeated", "missing_duplicate_reason"]
        },
        "summary": "发票号码或明细附件编号重复，且缺少拆单、补票或更正说明时命中。",
        "message": "存在重复发票或重复附件编号，需删除重复票据或补充合理说明。",
        "semantic_type": "duplicate_invoice_check",
    },
    {
        "template_id": "travel_city_route_consistency",
        "group": "travel",
        "title": "差旅票据城市一致性校验",
        "description": "比对交通票、住宿票据城市与申报目的地，识别跨城、绕行或目的地不一致风险。",
        "business_stage": "reimbursement",
        "expense_category": "travel",
        "requires_attachment": True,
        "natural_language": (
            "差旅报销时，先检查是否已上传交通票据、住宿票据或其他能识别城市的附件；"
            "再读取申报目的地、明细发生地点、交通票行程城市、住宿发票城市和报销事由。"
            "若交通票或住宿票据中的城市无法与申报目的地、明细地点形成一致关系，"
            "或票据路线中出现无法由本次票据起终点和申报目的地解释的额外中转城市，"
            "且报销事由中没有说明绕行、跨城办事或临时改签原因，"
            "则标记为高风险，要求补充行程说明或退回修改。"
        ),
        "field_keys": [
            "claim.location",
            "item.item_location",
            "attachment.route_cities",
            "attachment.hotel_city",
            "claim.reason",
        ],
        "conditions": [
            {
                "id": "attachment_city_evidence_present",
                "operator": "exists_any",
                "fields": ["attachment.route_cities", "attachment.hotel_city"],
            },
            {
                "id": "city_outside_business_scope",
                "operator": "not_in_scope",
                "left_fields": ["attachment.route_cities", "attachment.hotel_city"],
                "right_fields": ["claim.location", "item.item_location"],
            },
            {
                "id": "missing_route_exception",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["绕行", "跨城办事", "临时改签", "临时任务"],
            },
        ],
        "hit_logic": {
            "all": [
                "attachment_city_evidence_present",
                "city_outside_business_scope",
                "missing_route_exception",
            ]
        },
        "summary": "票据城市集合与申报行程集合无交集或出现额外中转城市，且缺少合理例外说明时命中。",
        "message": "票据城市与申报行程不一致，需补充绕行、跨城或改签说明。",
        "semantic_type": "travel_route_city_consistency",
    },
    {
        "template_id": "travel_lodging_date_range",
        "group": "travel",
        "title": "差旅住宿日期范围校验",
        "description": "校验住宿票据日期是否落在差旅开始和结束日期范围内。",
        "business_stage": "reimbursement",
        "expense_category": "travel",
        "requires_attachment": True,
        "natural_language": (
            "差旅住宿报销时，先确认已上传住宿发票或酒店水单；"
            "再读取报销事由、申报目的地、住宿城市、住宿开始日期、住宿结束日期、出差开始日期和出差结束日期。"
            "若住宿发生时间早于出差开始或晚于出差结束，且没有延期、改签、临时任务等说明，"
            "则标记为高风险，要求补充住宿原因、行程证明或重新提交票据。"
        ),
        "field_keys": [
            "attachment.stay_start_date",
            "attachment.stay_end_date",
            "claim.trip_start_date",
            "claim.trip_end_date",
            "attachment.hotel_city",
            "claim.location",
            "claim.reason",
        ],
        "conditions": [
            {
                "id": "lodging_date_evidence_present",
                "operator": "exists_any",
                "fields": ["attachment.stay_start_date", "attachment.stay_end_date"],
            },
            {
                "id": "lodging_date_outside_trip_range",
                "operator": "date_outside_range",
                "date_fields": ["attachment.stay_start_date", "attachment.stay_end_date"],
                "range_start_fields": ["claim.trip_start_date"],
                "range_end_fields": ["claim.trip_end_date"],
            },
            {
                "id": "missing_lodging_exception",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["延期", "改签", "临时任务", "行程变更"],
            },
        ],
        "hit_logic": {
            "all": [
                "lodging_date_evidence_present",
                "lodging_date_outside_trip_range",
                "missing_lodging_exception",
            ]
        },
        "summary": "住宿日期不在差旅日期范围内，且缺少延期、改签或临时任务说明时命中。",
        "message": "住宿日期超出差旅行程范围，需补充行程证明或重新提交票据。",
        "semantic_type": "lodging_date_range_consistency",
    },
    {
        "template_id": "entertainment_per_capita_limit",
        "group": "entertainment",
        "title": "招待人均金额超标校验",
        "description": "按参与人数计算人均招待金额，超过标准且缺少说明时提示风险。",
        "business_stage": "reimbursement",
        "expense_category": "meal",
        "requires_attachment": True,
        "natural_language": (
            "业务招待报销时，读取申报总金额、参与人数、人均金额、报销事由和附件票据。"
            "若人均金额超过公司招待标准 500 元，且事由中没有高级审批、重要客户接待或专项审批说明，"
            "则标记为中风险，要求补充招待对象和审批依据。"
        ),
        "field_keys": [
            "claim.amount",
            "claim.attendee_count",
            "claim.per_capita_amount",
            "claim.reason",
            "attachment.invoice_no",
        ],
        "conditions": [
            {
                "id": "per_capita_amount_exceeds_limit",
                "operator": "numeric_compare",
                "left_fields": ["claim.per_capita_amount"],
                "threshold": 500,
                "compare": "gt",
            },
            {
                "id": "missing_special_approval_reason",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["高级审批", "重要客户", "专项审批", "特殊接待"],
            },
        ],
        "hit_logic": {
            "all": ["per_capita_amount_exceeds_limit", "missing_special_approval_reason"]
        },
        "summary": "人均金额大于招待标准阈值，且缺少合理审批说明时命中。",
        "message": "业务招待人均金额超过公司标准，需补充审批依据。",
        "semantic_type": "entertainment_per_capita_limit_check",
    },
    {
        "template_id": "procurement_goods_category_mismatch",
        "group": "procurement_ap",
        "title": "采购票据品名与费用类型一致性校验",
        "description": "检查发票商品服务名称是否与费用类型、采购用途一致。",
        "business_stage": "reimbursement",
        "expense_category": "office",
        "requires_attachment": True,
        "natural_language": (
            "采购类费用报销时，先确认已上传发票或采购票据；"
            "再读取商品服务名称、费用类型、明细事由、申报金额和报销事由。"
            "若发票商品服务名称与费用类型或明细事由无法形成一致关系，"
            "且报销事由没有说明代采、项目采购或费用归集原因，"
            "则标记为中风险，要求补充采购用途或更换正确费用类型。"
        ),
        "field_keys": [
            "attachment.goods_name",
            "item.item_type",
            "item.item_reason",
            "claim.amount",
            "claim.reason",
        ],
        "conditions": [
            {
                "id": "goods_evidence_present",
                "operator": "exists_any",
                "fields": ["attachment.goods_name"],
            },
            {
                "id": "goods_outside_expense_scope",
                "operator": "not_in_scope",
                "left_fields": ["attachment.goods_name"],
                "right_fields": ["item.item_type", "item.item_reason"],
            },
            {
                "id": "missing_procurement_exception",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["代采", "项目采购", "费用归集", "统一采购"],
            },
        ],
        "hit_logic": {
            "all": [
                "goods_evidence_present",
                "goods_outside_expense_scope",
                "missing_procurement_exception",
            ]
        },
        "summary": "发票品名与费用类型或明细事由不一致，且缺少采购归集说明时命中。",
        "message": "采购票据品名与费用类型不一致，需补充采购用途说明。",
        "semantic_type": "procurement_goods_category_consistency",
    },
    {
        "template_id": "corporate_card_date_consistency",
        "group": "corporate_card",
        "title": "企业卡交易日期一致性校验",
        "description": "用于企业卡消费日期与费用明细日期不一致的仿真规则。",
        "business_stage": "reimbursement",
        "expense_category": "travel",
        "requires_attachment": True,
        "natural_language": (
            "企业卡费用报销时，读取企业卡交易日期、费用明细发生日期、开票日期、申报金额和报销事由。"
            "若企业卡交易日期或开票日期明显不在明细发生日期对应的业务期间内，"
            "且报销事由没有说明补录、跨月结算或集中开票原因，"
            "则标记为低风险，提示补充交易说明。"
        ),
        "field_keys": [
            "attachment.issue_date",
            "item.item_date",
            "claim.amount",
            "claim.reason",
        ],
        "conditions": [
            {
                "id": "card_date_evidence_present",
                "operator": "exists_any",
                "fields": ["attachment.issue_date", "item.item_date"],
            },
            {
                "id": "issue_date_outside_item_date",
                "operator": "date_outside_range",
                "date_fields": ["attachment.issue_date"],
                "range_start_fields": ["item.item_date"],
                "range_end_fields": ["item.item_date"],
                "tolerance_days": 7,
            },
            {
                "id": "missing_card_exception",
                "operator": "not_contains_any",
                "fields": ["claim.reason"],
                "keywords": ["补录", "跨月结算", "集中开票", "统一结算"],
            },
        ],
        "hit_logic": {
            "all": ["card_date_evidence_present", "issue_date_outside_item_date", "missing_card_exception"]
        },
        "summary": "开票日期偏离明细发生日期超过容忍范围，且缺少企业卡结算说明时命中。",
        "message": "企业卡交易或开票日期与业务发生日期不一致，需补充说明。",
        "semantic_type": "corporate_card_date_consistency",
    },
    {
        "template_id": "general_missing_business_reason",
        "group": "general",
        "title": "报销事由完整性校验",
        "description": "识别事由过于笼统、缺少业务对象或用途说明的低风险提示。",
        "business_stage": "reimbursement",
        "expense_category": "all",
        "requires_attachment": False,
        "natural_language": (
            "费用报销时，读取报销事由、费用类型、申报金额、部门和明细事由。"
            "若报销事由没有包含项目、客户、会议、用途或审批等业务背景信息，"
            "且申报金额高于 300 元，则标记为低风险，提示补充业务背景说明。"
        ),
        "field_keys": [
            "claim.reason",
            "item.item_reason",
            "item.item_type",
            "claim.amount",
            "claim.department_name",
        ],
        "conditions": [
            {
                "id": "amount_needs_reason_context",
                "operator": "numeric_compare",
                "left_fields": ["claim.amount"],
                "threshold": 300,
                "compare": "gt",
            },
            {
                "id": "missing_business_context",
                "operator": "not_contains_any",
                "fields": ["claim.reason", "item.item_reason"],
                "keywords": ["项目", "客户", "会议", "用途", "审批"],
            },
        ],
        "hit_logic": {"all": ["amount_needs_reason_context", "missing_business_context"]},
        "summary": "申报金额超过低额阈值，且事由缺少业务背景关键词时命中。",
        "message": "报销事由缺少业务背景，请补充用途或审批说明。",
        "semantic_type": "general_business_reason_completeness",
    },
)
