#!/usr/bin/env python3
"""Build expense-control demo risk rule JSON manifests."""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path

SERVER_DIR = Path(__file__).resolve().parents[1]
RISK_RULE_DIR = SERVER_DIR / "rules" / "risk-rules"


BUDGET_EXPENSE_TYPES = ("all",)
SUPPORTED_DEMO_EXPENSE_TYPES = {
    "all",
    "travel",
    "hotel",
    "transport",
    "meal",
    "office",
    "communication",
}


FIELD_LABELS = {
    "claim.amount": ("报销金额", "number", "claim"),
    "claim.expense_type": ("费用类型", "enum", "claim"),
    "claim.department_name": ("部门", "text", "claim"),
    "claim.reason": ("事由", "text", "claim"),
    "item.item_reason": ("明细说明", "text", "item"),
    "application.id": ("申请单", "text", "application"),
    "application.status": ("申请状态", "enum", "application"),
    "application.approved_amount": ("申请审批金额", "number", "application"),
    "application.expense_type": ("申请费用类型", "enum", "application"),
    "application.department_name": ("申请部门", "text", "application"),
    "application.start_date": ("申请开始日期", "date", "application"),
    "application.end_date": ("申请结束日期", "date", "application"),
    "budget.line_id": ("预算行", "text", "budget"),
    "budget.available_amount": ("预算可用金额", "number", "budget"),
    "budget.used_rate": ("预算使用率", "number", "budget"),
    "budget.status": ("预算状态", "enum", "budget"),
    "budget.department_name": ("预算部门", "text", "budget"),
    "budget.quarter": ("预算季度", "text", "budget"),
    "budget.project_code": ("预算项目", "text", "budget"),
    "material.attachment_count": ("附件数量", "number", "material"),
    "material.contract_uploaded": ("合同已上传", "boolean", "material"),
    "material.acceptance_uploaded": ("验收材料已上传", "boolean", "material"),
    "material.plan_uploaded": ("方案已上传", "boolean", "material"),
    "material.attendee_list_uploaded": ("参与人清单已上传", "boolean", "material"),
    "material.invoice_uploaded": ("发票已上传", "boolean", "material"),
}


BASE_FIELDS = (
    "claim.amount",
    "claim.expense_type",
    "claim.department_name",
    "claim.reason",
    "item.item_reason",
)
BUDGET_FIELDS = BASE_FIELDS + (
    "budget.line_id",
    "budget.available_amount",
    "budget.used_rate",
    "budget.status",
    "budget.department_name",
    "budget.quarter",
    "budget.project_code",
)
APPLICATION_FIELDS = BASE_FIELDS + (
    "application.id",
    "application.status",
    "application.approved_amount",
    "application.expense_type",
    "application.department_name",
)
MATERIAL_FIELDS = BASE_FIELDS + (
    "material.attachment_count",
    "material.contract_uploaded",
    "material.acceptance_uploaded",
    "material.plan_uploaded",
    "material.attendee_list_uploaded",
    "material.invoice_uploaded",
)


@dataclass(frozen=True, slots=True)
class DemoRiskRule:
    code: str
    name: str
    description: str
    category: str
    ontology_signal: str
    finance_rule_code: str
    finance_rule_sheet: str
    business_stage: tuple[str, ...]
    expense_types: tuple[str, ...]
    condition_summary: str
    keywords: tuple[str, ...]
    severity: str
    action: str
    risk_score: int
    risk_level: str
    budget_required: bool = True
    requires_attachment: bool = False
    field_keys: tuple[str, ...] = field(default_factory=lambda: BASE_FIELDS)


def _budget_rule(
    code: str,
    name: str,
    description: str,
    condition_summary: str,
    keywords: tuple[str, ...],
    severity: str,
    action: str,
    risk_score: int,
) -> DemoRiskRule:
    return DemoRiskRule(
        code=code,
        name=name,
        description=description,
        category="预算管控",
        ontology_signal="budget_over_limit",
        finance_rule_code="budget.execution.policy",
        finance_rule_sheet="预算执行规则",
        business_stage=("expense_application", "reimbursement", "budget_execution"),
        expense_types=BUDGET_EXPENSE_TYPES,
        condition_summary=condition_summary,
        keywords=keywords,
        severity=severity,
        action=action,
        risk_score=risk_score,
        risk_level="high" if risk_score >= 80 else "medium",
        field_keys=BUDGET_FIELDS,
    )


def _rule_payload(rule: DemoRiskRule) -> dict[str, object]:
    now = datetime.now(UTC).isoformat()
    fields = [
        {
            "key": key,
            "label": FIELD_LABELS[key][0],
            "type": FIELD_LABELS[key][1],
            "source": FIELD_LABELS[key][2],
        }
        for key in rule.field_keys
    ]
    return {
        "schema_version": "2.0",
        "rule_code": rule.code,
        "name": rule.name,
        "description": rule.description,
        "enabled": True,
        "requires_attachment": rule.requires_attachment,
        "risk_dimension": "expense_control_demo",
        "risk_category": rule.category,
        "ontology_signal": rule.ontology_signal,
        "evaluator": "template_rule",
        "template_key": "keyword_match_v1",
        "finance_rule_code": rule.finance_rule_code,
        "finance_rule_sheet": rule.finance_rule_sheet,
        "business_stage": list(rule.business_stage),
        "expense_types": list(rule.expense_types),
        "budget_required": rule.budget_required,
        "applies_to": {
            "domains": ["expense"],
            "expense_types": list(rule.expense_types),
            "business_stages": list(rule.business_stage),
        },
        "inputs": {"fields": fields},
        "params": {
            "template_key": "keyword_match_v1",
            "field_keys": list(rule.field_keys),
            "search_fields": [
                "claim.reason",
                "item.item_reason",
                "claim.expense_type",
            ],
            "keywords": list(rule.keywords),
            "condition_summary": rule.condition_summary,
            "finance_rule_code": rule.finance_rule_code,
            "finance_rule_sheet": rule.finance_rule_sheet,
            "business_stage": list(rule.business_stage),
            "expense_types": list(rule.expense_types),
            "budget_required": rule.budget_required,
        },
        "outcomes": {
            "pass": {"severity": "none", "action": "continue"},
            "fail": {
                "severity": rule.severity,
                "action": rule.action,
                "risk_score": rule.risk_score,
            },
        },
        "metadata": {
            "owner": "风控与审计部",
            "stability": "platform",
            "source_ref": "费用管控 Demo 风险规则库",
            "created_at": now,
            "created_by": "system",
            "risk_score": rule.risk_score,
            "risk_level": rule.risk_level,
            "rule_title": rule.name,
            "finance_rule_code": rule.finance_rule_code,
            "finance_rule_sheet": rule.finance_rule_sheet,
            "business_stage": list(rule.business_stage),
            "expense_types": list(rule.expense_types),
            "budget_required": rule.budget_required,
        },
        "severity": rule.severity,
        "risk_score": rule.risk_score,
        "risk_level": rule.risk_level,
    }


RULES: tuple[DemoRiskRule, ...] = (
    _budget_rule(
        "risk.budget.available_balance_insufficient",
        "预算可用余额不足",
        "提交后预算余额为负，或当前可用预算不足以覆盖本次申请/报销金额。",
        "预算可用金额小于本次金额时触发。",
        ("预算不足", "可用余额不足", "超预算"),
        "high",
        "manual_review",
        88,
    ),
    _budget_rule(
        "risk.budget.usage_warning_80",
        "预算使用率达到 80% 预警",
        "报销或申请通过后，部门/项目/费用类型预算使用率达到 80% 以上。",
        "预算使用率大于等于 80% 且低于 100% 时触发。",
        ("预算预警", "80%", "使用率过高"),
        "medium",
        "warning",
        70,
    ),
    _budget_rule(
        "risk.budget.usage_over_100",
        "预算使用率超过 100% 管控",
        "报销或申请通过后，预算使用率超过 100%，需要阻断或升级审批。",
        "预算使用率超过 100% 时触发。",
        ("预算超支", "超过100%", "禁止提交"),
        "critical",
        "block_submit",
        96,
    ),
    _budget_rule(
        "risk.budget.frozen_or_closed_used",
        "使用冻结或关闭预算",
        "单据引用了已冻结、已关闭或已作废的预算行。",
        "预算状态不是启用时触发。",
        ("冻结预算", "关闭预算", "预算作废"),
        "high",
        "block_submit",
        90,
    ),
    _budget_rule(
        "risk.budget.missing_budget_line",
        "缺少预算口径",
        "需要预算管控的费用未关联年度、季度、部门、项目或费用类型预算。",
        "费用类型要求预算管控但预算行为空时触发。",
        ("无预算", "预算口径缺失", "未关联预算"),
        "high",
        "manual_review",
        82,
    ),
    _budget_rule(
        "risk.budget.cross_department_without_authorization",
        "跨部门预算未授权",
        "报销部门与预算归属部门不一致，且没有跨部门预算授权。",
        "单据部门与预算部门不一致且无授权说明时触发。",
        ("跨部门预算", "部门不一致", "未授权"),
        "high",
        "manual_review",
        86,
    ),
    _budget_rule(
        "risk.budget.cross_quarter_without_explanation",
        "跨季度预算未说明",
        "单据发生期间与预算季度不一致，且缺少跨季度使用说明。",
        "发生季度与预算季度不一致且未说明时触发。",
        ("跨季度预算", "季度不一致", "未说明"),
        "medium",
        "manual_review",
        76,
    ),
    _budget_rule(
        "risk.budget.project_department_mismatch",
        "项目预算与部门不匹配",
        "单据引用的项目预算不属于当前部门或当前成本中心。",
        "项目预算归属与报销部门不一致时触发。",
        ("项目预算", "成本中心不匹配", "部门不匹配"),
        "high",
        "manual_review",
        84,
    ),
    _budget_rule(
        "risk.budget.duplicate_reserve",
        "重复占用预算",
        "同一申请、项目或合同已占用预算，本次单据再次占用同一预算口径。",
        "相同业务标识存在未释放预算占用时触发。",
        ("重复占用", "预算锁定", "重复申请"),
        "medium",
        "manual_review",
        74,
    ),
    _budget_rule(
        "risk.budget.consume_without_release",
        "预算占用未释放",
        "申请取消、退回或驳回后，预算占用未释放导致后续可用预算失真。",
        "申请非有效状态但仍存在预算占用时触发。",
        ("占用未释放", "退回未释放", "预算释放"),
        "medium",
        "manual_review",
        72,
    ),
    DemoRiskRule(
        "risk.application.large_expense_without_preapproval",
        "大额费用未事前申请",
        "达到财务制度中大额标准的费用，未找到有效事前申请即进入报销。",
        "申请前置",
        "application_required",
        "finance.preapproval.policy",
        "费用申请前置规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "金额达到大额阈值且缺少已通过申请单时触发。",
        ("大额费用", "未申请", "先申请后报销"),
        "high",
        "manual_review",
        86,
        "high",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.application.marketing_without_campaign",
        "市场推广费无活动申请",
        "市场活动、投放、展会等推广费用，缺少已审批的活动申请或投放方案。",
        "申请前置",
        "application_required",
        "expense.application.policy",
        "费用申请前置规则",
        ("reimbursement",),
        ("marketing",),
        "市场推广费报销缺少活动申请或方案时触发。",
        ("市场推广", "活动申请", "投放方案"),
        "high",
        "manual_review",
        84,
        "high",
        field_keys=APPLICATION_FIELDS + ("material.plan_uploaded",),
    ),
    DemoRiskRule(
        "risk.application.training_without_plan",
        "培训费无培训计划",
        "培训费报销缺少年度培训计划、专项培训申请或审批记录。",
        "申请前置",
        "application_required",
        "expense.application.policy",
        "费用申请前置规则",
        ("reimbursement",),
        ("training",),
        "培训费用没有匹配到培训计划或事前申请时触发。",
        ("培训计划", "培训申请", "未申请"),
        "high",
        "manual_review",
        83,
        "high",
        field_keys=APPLICATION_FIELDS + ("material.plan_uploaded",),
    ),
    DemoRiskRule(
        "risk.application.meeting_without_application",
        "会务费无会议申请",
        "会务场地、物料、会务服务等费用缺少会议申请或会议预算审批。",
        "申请前置",
        "application_required",
        "expense.application.policy",
        "费用申请前置规则",
        ("reimbursement",),
        ("meeting",),
        "会务费报销缺少会议申请、会议预算或会议通知时触发。",
        ("会议申请", "会议预算", "会务费"),
        "high",
        "manual_review",
        82,
        "high",
        field_keys=APPLICATION_FIELDS + ("material.plan_uploaded",),
    ),
    DemoRiskRule(
        "risk.application.software_without_purchase",
        "软件服务费无采购申请",
        "软件订阅、SaaS 服务或系统实施费用缺少采购申请或审批链。",
        "申请前置",
        "application_required",
        "expense.application.policy",
        "费用申请前置规则",
        ("reimbursement",),
        ("software",),
        "软件服务费缺少采购申请或合同审批时触发。",
        ("软件采购", "SaaS", "采购申请"),
        "high",
        "manual_review",
        85,
        "high",
        field_keys=APPLICATION_FIELDS + ("material.contract_uploaded",),
    ),
    DemoRiskRule(
        "risk.application.office_bulk_without_purchase",
        "办公用品大额采购未申请",
        "批量办公用品或设备采购达到阈值但未走采购申请。",
        "申请前置",
        "application_required",
        "expense.application.policy",
        "费用申请前置规则",
        ("reimbursement",),
        ("office",),
        "办公用品单次金额达到采购阈值且缺少采购申请时触发。",
        ("办公采购", "大额办公用品", "采购申请"),
        "medium",
        "manual_review",
        78,
        "medium",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.application.meal_high_value_without_preapproval",
        "大额业务招待未申请",
        "业务招待金额或人均金额超过制度阈值但未事前审批。",
        "申请前置",
        "application_required",
        "expense.application.policy",
        "费用申请前置规则",
        ("reimbursement",),
        ("meal",),
        "业务招待金额超过申请阈值且没有通过申请时触发。",
        ("业务招待", "人均超标", "未申请"),
        "high",
        "manual_review",
        84,
        "high",
        field_keys=APPLICATION_FIELDS + ("material.attendee_list_uploaded",),
    ),
    DemoRiskRule(
        "risk.application.travel_large_without_preapproval",
        "大额差旅未申请",
        "多人出差、长周期出差或高金额差旅报销缺少出差申请。",
        "申请前置",
        "application_required",
        "rule.expense.company_travel_expense_reimbursement",
        "差旅住宿费标准",
        ("reimbursement",),
        ("travel",),
        "差旅金额达到大额阈值且缺少有效出差申请时触发。",
        ("差旅申请", "大额差旅", "未申请"),
        "high",
        "manual_review",
        82,
        "high",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.reimbursement.amount_over_application",
        "报销金额超过申请金额",
        "报销总金额超过已审批申请金额，需要按偏差规则复核。",
        "报销偏差",
        "amount_over_application",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "报销金额大于申请审批金额时触发。",
        ("超过申请金额", "报销偏差", "申请金额"),
        "medium",
        "manual_review",
        76,
        "medium",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.reimbursement.amount_over_application_10pct",
        "报销金额超过申请金额 10%",
        "报销金额比申请审批金额高出 10% 以上，需要升级审批或禁止提交。",
        "报销偏差",
        "amount_over_application",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "报销金额超过申请审批金额 10% 以上时触发。",
        ("超过申请10%", "金额偏差", "升级审批"),
        "high",
        "manual_review",
        88,
        "high",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.reimbursement.expense_type_mismatch_application",
        "报销费用类型与申请不一致",
        "报销单费用类型与关联申请单费用类型不一致。",
        "报销偏差",
        "expense_type_mismatch",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "报销费用类型与申请费用类型不一致时触发。",
        ("费用类型不一致", "申请报销不匹配", "类型偏差"),
        "medium",
        "manual_review",
        74,
        "medium",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.reimbursement.department_mismatch_application",
        "报销部门与申请部门不一致",
        "报销部门、成本中心与关联申请单不一致，且缺少调整说明。",
        "报销偏差",
        "department_mismatch",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "报销部门与申请部门不一致时触发。",
        ("部门不一致", "成本中心偏差", "申请部门"),
        "medium",
        "manual_review",
        72,
        "medium",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.reimbursement.period_outside_application",
        "报销发生期间超出申请期间",
        "费用发生日期不在已审批申请的起止日期范围内。",
        "报销偏差",
        "period_outside_application",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "发生日期超出申请有效期间时触发。",
        ("期间不一致", "超出申请期间", "日期偏差"),
        "medium",
        "manual_review",
        70,
        "medium",
        field_keys=APPLICATION_FIELDS + ("application.start_date", "application.end_date"),
    ),
    DemoRiskRule(
        "risk.reimbursement.rejected_application_claimed",
        "已驳回申请被用于报销",
        "报销单关联的申请单为驳回、撤回或已取消状态。",
        "报销偏差",
        "invalid_application_status",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "关联申请状态不是已通过或已完成时触发。",
        ("申请驳回", "申请撤回", "无效申请"),
        "high",
        "block_submit",
        92,
        "high",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.reimbursement.duplicate_against_application",
        "同一申请重复报销",
        "同一申请单或同一合同/项目存在多笔疑似重复报销。",
        "报销偏差",
        "duplicate_reimbursement",
        "application.reimbursement.linkage.policy",
        "申请报销关联规则",
        ("reimbursement",),
        BUDGET_EXPENSE_TYPES,
        "同一申请的已报销金额与本次金额超过申请金额时触发。",
        ("重复报销", "同一申请", "重复占用"),
        "high",
        "manual_review",
        86,
        "high",
        field_keys=APPLICATION_FIELDS,
    ),
    DemoRiskRule(
        "risk.standard.meal_participants_missing",
        "业务招待缺少参与人清单",
        "业务招待费要求提供客户名称、参与人清单和招待说明。",
        "材料完整性",
        "material_missing",
        "expense.material.policy",
        "材料完整性规则",
        ("reimbursement",),
        ("meal",),
        "业务招待费缺少参与人清单或客户信息时触发。",
        ("参与人清单", "客户信息", "业务招待"),
        "medium",
        "manual_review",
        72,
        "medium",
        requires_attachment=True,
        field_keys=MATERIAL_FIELDS,
    ),
    DemoRiskRule(
        "risk.standard.software_contract_missing",
        "软件服务费缺少合同",
        "软件服务费、SaaS 订阅或系统实施费用缺少合同、订单或验收材料。",
        "材料完整性",
        "contract_missing",
        "expense.material.policy",
        "材料完整性规则",
        ("expense_application", "reimbursement"),
        ("software",),
        "软件服务费要求合同或订单但未提供时触发。",
        ("合同缺失", "软件服务", "验收材料"),
        "high",
        "manual_review",
        84,
        "high",
        requires_attachment=True,
        field_keys=MATERIAL_FIELDS,
    ),
    DemoRiskRule(
        "risk.standard.marketing_acceptance_missing",
        "市场推广费缺少验收材料",
        "市场推广活动缺少投放截图、活动复盘、验收单或结案材料。",
        "材料完整性",
        "acceptance_missing",
        "expense.material.policy",
        "材料完整性规则",
        ("reimbursement",),
        ("marketing",),
        "市场推广费要求验收或结案材料但未提供时触发。",
        ("验收材料", "活动结案", "投放截图"),
        "high",
        "manual_review",
        82,
        "high",
        requires_attachment=True,
        field_keys=MATERIAL_FIELDS,
    ),
    DemoRiskRule(
        "risk.standard.office_fixed_asset_as_office",
        "固定资产伪装为办公用品费",
        "办公用品费明细疑似包含固定资产、电子设备或应走采购入库的物品。",
        "费用标准",
        "expense_type_mismatch",
        "expense.classification.policy",
        "费用类型归类规则",
        ("reimbursement",),
        ("office",),
        "办公用品费包含固定资产关键词或超过采购阈值时触发。",
        ("固定资产", "电脑", "显示器", "办公设备"),
        "medium",
        "manual_review",
        78,
        "medium",
        field_keys=BASE_FIELDS,
    ),
    DemoRiskRule(
        "risk.standard.meeting_attendee_list_missing",
        "会务费缺少参会名单",
        "会务费报销缺少参会名单、会议通知、会议照片或会议纪要。",
        "材料完整性",
        "material_missing",
        "expense.material.policy",
        "材料完整性规则",
        ("reimbursement",),
        ("meeting",),
        "会务费要求参会名单或会议材料但未提供时触发。",
        ("参会名单", "会议通知", "会议纪要"),
        "medium",
        "manual_review",
        74,
        "medium",
        requires_attachment=True,
        field_keys=MATERIAL_FIELDS,
    ),
)


COMMUNICATION_RULES: tuple[DemoRiskRule, ...] = (
    DemoRiskRule(
        "risk.standard.communication_amount_over_policy",
        "通信费金额超过月度标准",
        "通信费、话费、流量费或宽带费超过公司月度标准，且缺少岗位必要性或专项审批说明。",
        "费用标准",
        "expense_standard_over_limit",
        "expense.communication.policy",
        "通信费报销规则",
        ("expense_application", "reimbursement"),
        ("communication",),
        "通信费金额超过公司标准且没有岗位、项目或专项审批说明时触发。",
        ("通信费", "话费", "流量费", "宽带费", "超标准"),
        "medium",
        "manual_review",
        68,
        "medium",
        field_keys=BASE_FIELDS + ("material.invoice_uploaded",),
    ),
    DemoRiskRule(
        "risk.standard.communication_account_mismatch",
        "通信账户归属与报销人不一致",
        "通信票据、运营商账单或号码归属信息与报销人不一致，且缺少代垫或统一缴费说明。",
        "费用归属",
        "expense_owner_mismatch",
        "expense.communication.policy",
        "通信费报销规则",
        ("reimbursement",),
        ("communication",),
        "通信账户归属与报销人不一致且没有代垫、统一缴费或部门公共号码说明时触发。",
        ("号码归属", "账户不一致", "代垫", "统一缴费", "公共号码"),
        "high",
        "manual_review",
        82,
        "high",
        requires_attachment=True,
        field_keys=MATERIAL_FIELDS,
    ),
)


def _is_supported_demo_rule(rule: DemoRiskRule) -> bool:
    return all(expense_type in SUPPORTED_DEMO_EXPENSE_TYPES for expense_type in rule.expense_types)


RULES = tuple(rule for rule in RULES if _is_supported_demo_rule(rule)) + COMMUNICATION_RULES


def main() -> None:
    RISK_RULE_DIR.mkdir(parents=True, exist_ok=True)
    for rule in RULES:
        path = RISK_RULE_DIR / f"{rule.code}.json"
        payload = _rule_payload(rule)
        path.write_text(
            json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
    print(f"Generated {len(RULES)} expense-control demo risk rule manifest(s).")


if __name__ == "__main__":
    main()
