"""预算管理 API — 管理会计OS
预算值的CRUD、自动分解、版本管理
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, or_
from typing import Optional
from datetime import datetime

from app.database import get_db
from app.deps import get_entity_id
from app.auth_middleware import require_auth, require_role
from app.models import BudgetPlan, KPIDefinition, OperationLog, KPIValue

router = APIRouter(prefix="/api/cma/budget", tags=["预算管理"],
    dependencies=[Depends(require_role("ceo", "finance", "it"))],
)


@router.get("/plans")
def list_budget_plans(
    kpi_id: Optional[int] = Query(None),
    map_id: Optional[int] = Query(None, description="按战略地图过滤预算"),
    period: Optional[str] = Query(None),
    year: Optional[int] = Query(None),
    version: Optional[str] = Query(None),
    keyword: Optional[str] = Query(None, description="搜索KPI名称"),
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """查询预算计划列表"""
    query = db.query(BudgetPlan).join(
        KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id
    ).filter(KPIDefinition.entity_id == entity_id)

    if map_id:
        # 按地图隔离: 显示该地图的预算 + 未绑定地图的历史预算(NULL, 兼容迁移)
        query = query.filter(or_(BudgetPlan.map_id == map_id, BudgetPlan.map_id.is_(None)))
    if kpi_id:
        query = query.filter(BudgetPlan.kpi_id == kpi_id)
    if period:
        query = query.filter(BudgetPlan.period == period)
    if year:
        query = query.filter(BudgetPlan.budget_year == year)
    if version:
        query = query.filter(BudgetPlan.version == version)
    if keyword:
        query = query.filter(KPIDefinition.kpi_name.contains(keyword))

    plans = query.order_by(BudgetPlan.budget_year.desc(), BudgetPlan.budget_month.asc()).all()

    result = []
    for p in plans:
        kpi = db.query(KPIDefinition).filter(KPIDefinition.id == p.kpi_id).first()
        result.append({
            "id": p.id,
            "kpi_id": p.kpi_id,
            "map_id": p.map_id,
            "kpi_code": kpi.kpi_code if kpi else "",
            "kpi_name": kpi.kpi_name if kpi else "",
            "dimension": kpi.dimension if kpi else "",
            "unit": kpi.unit if kpi else "",
            "period": p.period,
            "budget_value": p.budget_value,
            "budget_year": p.budget_year,
            "budget_month": p.budget_month,
            "version": p.version,
            "status": p.status,
            "remark": p.remark,
            "created_at": p.created_at.isoformat() if p.created_at else None,
        })
    return {"data": result, "total": len(result)}


@router.post("/plans")
def create_budget_plan(
    data: dict,
    db: Session = Depends(get_db),
    current_user=Depends(require_auth),
):
    """创建或更新单条预算计划"""
    kpi_id = data.get("kpi_id")
    period = data.get("period")
    budget_value = data.get("budget_value")

    if not all([kpi_id, period, budget_value is not None]):
        raise HTTPException(400, "缺少必要参数: kpi_id, period, budget_value")

    # 检查KPI是否存在
    kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
    if not kpi:
        raise HTTPException(404, "KPI不存在")

    year, month = period.split("-")
    version = data.get("version", "v1.0")
    map_id = data.get("map_id")  # 预算归属地图（2026-08-27: 按地图隔离）

    # 检查是否已有记录（去重, 含map_id）
    existing = db.query(BudgetPlan).filter(
        BudgetPlan.kpi_id == kpi_id,
        BudgetPlan.period == period,
        BudgetPlan.version == version,
        BudgetPlan.map_id == map_id,
        BudgetPlan.status == "active",
    ).first()

    if existing:
        existing.budget_value = budget_value
        existing.remark = data.get("remark", existing.remark)
        db.commit()
        db.refresh(existing)
        return {"message": "预算已更新", "id": existing.id}
    else:
        plan = BudgetPlan(
            kpi_id=kpi_id,
            map_id=map_id,
            period=period,
            budget_value=budget_value,
            budget_year=int(year),
            budget_month=int(month),
            version=version,
            status="active",
            remark=data.get("remark", ""),
            created_by=current_user.name if hasattr(current_user, "name") else "",
        )
        db.add(plan)
        db.commit()
        db.refresh(plan)

        # 记录操作日志
        log = OperationLog(
            action="create",
            target_type="budget",
            target_id=plan.id,
            detail=__import__("json").dumps({"kpi_id": kpi_id, "period": period, "value": budget_value}, ensure_ascii=False),
        )
        db.add(log)
        db.commit()

        return {"message": "预算已创建", "id": plan.id}


@router.put("/plans/{plan_id}")
def update_budget_plan(
    plan_id: int,
    data: dict,
    db: Session = Depends(get_db),
):
    """更新预算计划"""
    plan = db.query(BudgetPlan).filter(BudgetPlan.id == plan_id).first()
    if not plan:
        raise HTTPException(404, "预算计划不存在")

    if "budget_value" in data:
        plan.budget_value = data["budget_value"]
    if "remark" in data:
        plan.remark = data["remark"]
    if "version" in data:
        plan.version = data["version"]
    if "status" in data:
        plan.status = data["status"]

    db.commit()
    return {"message": "预算已更新"}


@router.delete("/plans/{plan_id}")
def delete_budget_plan(
    plan_id: int,
    db: Session = Depends(get_db),
):
    """删除预算计划"""
    plan = db.query(BudgetPlan).filter(BudgetPlan.id == plan_id).first()
    if not plan:
        raise HTTPException(404, "预算计划不存在")
    db.delete(plan)
    db.commit()
    return {"message": "预算已删除"}


@router.post("/auto-decompose")
def auto_decompose_budget(
    data: dict,
    db: Session = Depends(get_db),
    current_user=Depends(require_auth),
):
    """自动分解年度预算到月度（均分或按历史权重）
    支持两种模式：
    1. 单KPI：传 kpi_id + annual_budget
    2. 批量：不传 kpi_id，分解该年所有已有年度预算的KPI
    """
    kpi_id = data.get("kpi_id")
    year = data.get("year", datetime.now().year)
    annual_budget = data.get("annual_budget")
    method = data.get("method", "equal")  # equal / weighted
    version = data.get("version", "v1.0")

    # ── 批量模式：不传kpi_id → 分解该年所有有年度预算的KPI ──
    if not kpi_id:
        # 只取年度行（period=YYYY-00）作为年度总额，避免把月度行也加进来导致滚雪球（非幂等bug修复）
        year_budget_rows = db.query(BudgetPlan).filter(
            BudgetPlan.entity_id == 1,
            BudgetPlan.budget_year == year,
            BudgetPlan.period == f"{year}-00",
            BudgetPlan.status == "active",
        ).all()

        # 按KPI聚合年度预算总额——同一KPI存在多个version年度行时只取一行
        # （优先匹配请求version，否则取第一条），避免多版本叠加导致总额虚高（幂等加固）
        from collections import defaultdict
        per_kpi = defaultdict(list)
        for r in year_budget_rows:
            per_kpi[r.kpi_id].append(r)
        kpi_annual = {}
        kpi_version_used = {}
        for kid, rows in per_kpi.items():
            chosen = next((r for r in rows if r.version == version), rows[0])
            kpi_annual[kid] = chosen.budget_value or 0
            kpi_version_used[kid] = chosen.version

        if not kpi_annual:
            raise HTTPException(400, "该年度没有可分解的预算，请先在预算执行中录入年度预算")

        results = []
        created_count = 0
        for kid, annual in kpi_annual.items():
            kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kid).first()
            if not kpi:
                continue

            # 计算各月权重
            if method == "weighted":
                last_year = year - 1
                values = db.query(KPIValue).filter(
                    KPIValue.kpi_id == kid,
                    KPIValue.period.like(f"{last_year}-%"),
                    KPIValue.actual_value.isnot(None),
                ).order_by(KPIValue.period.asc()).all()
                total = sum(v.actual_value for v in values)
                weights = {v.period: v.actual_value / total for v in values} if total > 0 else {}
            else:
                weights = {}

            monthly = []
            for m in range(1, 13):
                period = f"{year}-{m:02d}"
                weight = weights.get(period, 1 / 12) if method == "weighted" and weights else 1 / 12
                monthly_value = round(annual * weight, 2)

                existing = db.query(BudgetPlan).filter(
                    BudgetPlan.kpi_id == kid,
                    BudgetPlan.period == period,
                    BudgetPlan.version == version,
                    BudgetPlan.status == "active",
                ).first()

                if existing:
                    existing.budget_value = monthly_value
                    existing.updated_at = datetime.now()
                else:
                    db.add(BudgetPlan(
                        entity_id=1,
                        kpi_id=kid,
                        period=period,
                        budget_value=monthly_value,
                        budget_year=year,
                        budget_month=m,
                        version=version,
                        status="active",
                    ))
                    created_count += 1
                monthly.append(monthly_value)

            results.append({
                "kpi_id": kid,
                "kpi_code": kpi.kpi_code,
                "kpi_name": kpi.kpi_name,
                "annual_budget": round(annual, 2),
                "version_used": kpi_version_used.get(kid),
                "method": "equal" if not weights else "weighted",
                "monthly": monthly,
                "monthly_count": 12,
            })

        db.commit()
        return {
            "message": f"批量分解完成：{len(results)}个KPI",
            "count": len(results),
            "results": results,
            "created": created_count,
        }

    # ── 单KPI模式（原有逻辑）──
    if annual_budget is None:
        raise HTTPException(400, "缺少必要参数: annual_budget")

    kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
    if not kpi:
        raise HTTPException(404, "KPI不存在")

    # 计算各月权重
    if method == "weighted":
        # 按去年各月实际值的比例分配
        last_year = year - 1
        values = db.query(KPIValue).filter(
            KPIValue.kpi_id == kpi_id,
            KPIValue.period.like(f"{last_year}-%"),
            KPIValue.actual_value.isnot(None),
        ).order_by(KPIValue.period.asc()).all()

        total = sum(v.actual_value for v in values)
        if total > 0:
            weights = {v.period: v.actual_value / total for v in values}
        else:
            method = "equal"
        created = []
        for m in range(1, 13):
            period = f"{year}-{m:02d}"
            weight = weights.get(period, 1 / 12) if method == "weighted" else 1 / 12
            monthly_value = round(annual_budget * weight, 2)

            existing = db.query(BudgetPlan).filter(
                BudgetPlan.kpi_id == kpi_id,
                BudgetPlan.period == period,
                BudgetPlan.version == version,
                BudgetPlan.status == "active",
            ).first()

            if existing:
                existing.budget_value = monthly_value
            else:
                bp = BudgetPlan(
                    kpi_id=kpi_id, period=period,
                    budget_value=monthly_value, budget_year=year,
                    budget_month=m, version=version, status="active",
                    created_by=current_user.name if hasattr(current_user, "name") else "",
                )
                db.add(bp)
            created.append({"period": period, "value": monthly_value})
    else:
        # 均分
        monthly = round(annual_budget / 12, 2)
        created = []
        for m in range(1, 13):
            period = f"{year}-{m:02d}"
            existing = db.query(BudgetPlan).filter(
                BudgetPlan.kpi_id == kpi_id,
                BudgetPlan.period == period,
                BudgetPlan.version == version,
                BudgetPlan.status == "active",
            ).first()
            if existing:
                existing.budget_value = monthly
            else:
                bp = BudgetPlan(
                    kpi_id=kpi_id, period=period,
                    budget_value=monthly, budget_year=year,
                    budget_month=m, version=version, status="active",
                    created_by=current_user.name if hasattr(current_user, "name") else "",
                )
                db.add(bp)
            created.append({"period": period, "value": monthly})

    db.commit()
    return {
        "message": f"年度预算已分解为{len(created)}个月度预算",
        "kpi_id": kpi_id,
        "kpi_name": kpi.kpi_name,
        "year": year,
        "annual_budget": annual_budget,
        "method": method,
        "monthly_budgets": created,
    }


@router.get("/deviation-report")
def get_deviation_report(
    kpi_id: Optional[int] = Query(None),
    period: Optional[str] = Query(None),
    year: Optional[int] = Query(None),
    month: Optional[int] = Query(None),
    dimension: Optional[str] = Query(None),
    alert_level: Optional[str] = Query(None),
    db: Session = Depends(get_db),
):
    """获取差异分析报告（汇总多个KPI的实际vs预算差异）"""
    if period is None:
        if year and month:
            period = f"{year}-{month:02d}"
        elif year:
            period = f"{year}-{datetime.now().month:02d}"
        else:
            period = datetime.now().strftime("%Y-%m")

    query = db.query(KPIDefinition).filter(KPIDefinition.status == "active")
    if kpi_id:
        query = query.filter(KPIDefinition.id == kpi_id)
    if dimension:
        query = query.filter(KPIDefinition.dimension == dimension)

    kpis = query.all()
    from app.utils.deviation_engine import calc_period_deviation, calc_period_diff

    items = []
    summary = {
        "total_kpis": 0,
        "has_budget": 0,
        "over_budget": 0,
        "under_budget": 0,
        "avg_deviation_rate": 0,
    }

    rates = []
    for kpi in kpis:
        item = calc_period_deviation(db, kpi.id, period)
        # 补充KPI基础信息（修复前端"预算执行KPI无名称"）
        item["kpi_name"] = kpi.kpi_name
        item["dimension"] = kpi.dimension
        item["unit"] = kpi.unit or ""
        # 战略目标 vs 预算差异（2026-08-27: 战略=愿景/预算=计划, 允许不同但差异可见可解释）
        target = kpi.target_monthly or kpi.target_value
        item["strategic_target"] = target
        if item.get("budget_value") is not None and target:
            gap = round((item["budget_value"] - target) / target * 100, 1)
            item["target_gap_pct"] = gap
            item["target_gap_level"] = "high" if abs(gap) > 20 else ("medium" if abs(gap) > 10 else "ok")
        else:
            item["target_gap_pct"] = None
            item["target_gap_level"] = "none"
        items.append(item)
        summary["total_kpis"] += 1

        if item.get("budget_value") is not None:
            summary["has_budget"] += 1
        if item.get("is_over_budget"):
            summary["over_budget"] += 1
        elif item.get("deviation_rate") is not None and item["deviation_rate"] < 0:
            summary["under_budget"] += 1
        if item.get("deviation_rate") is not None:
            rates.append(abs(item["deviation_rate"]))

        # 补充同比/环比
        if item.get("actual_value") is not None:
            item["yoy"] = calc_period_diff(db, kpi.id, period, "yoy")
            item["mom"] = calc_period_diff(db, kpi.id, period, "mom")

    summary["avg_deviation_rate"] = round(sum(rates) / len(rates), 2) if rates else 0

    # 前端 alert_level 过滤
    if alert_level:
        def get_level(rate):
            if rate is None:
                return None
            if rate > 20:
                return "red"
            if rate > 10:
                return "yellow"
            return "normal"
        items = [i for i in items if get_level(i.get("deviation_rate")) == alert_level]

    return {
        "period": period,
        "summary": summary,
        "items": items,
    }


# ──────────────────────────────────────────────
# 滚动/固定预算切换
# ──────────────────────────────────────────────

@router.get("/config")
def get_budget_config(db: Session = Depends(get_db)):
    """获取预算模式配置"""
    from app.models import SystemConfig
    cfg = db.query(SystemConfig).filter(SystemConfig.config_key == "budget_mode").first()
    if not cfg:
        return {"budget_mode": "fixed", "rolling_months": 12, "description": "固定预算（年度）"}
    import json
    try:
        val = json.loads(cfg.config_value)
    except (json.JSONDecodeError, TypeError):
        val = {"mode": "fixed", "rolling_months": 12}
    return val


@router.post("/config")
def set_budget_config(
    data: dict,
    db: Session = Depends(get_db),
    current_user=Depends(require_auth),
):
    """设置预算模式"""
    from app.models import SystemConfig
    import json

    mode = data.get("mode", "fixed")
    rolling_months = data.get("rolling_months", 12)
    if mode not in ("fixed", "rolling"):
        raise HTTPException(400, "预算模式必须是 fixed 或 rolling")

    cfg = db.query(SystemConfig).filter(SystemConfig.config_key == "budget_mode").first()
    val = json.dumps({"mode": mode, "rolling_months": rolling_months}, ensure_ascii=False)
    if cfg:
        cfg.config_value = val
    else:
        cfg = SystemConfig(
            config_key="budget_mode",
            config_value=val,
            description="预算模式: fixed=固定预算, rolling=滚动预算",
        )
        db.add(cfg)
    db.commit()
    return {"message": f"预算模式已切换为{'滚动预算' if mode == 'rolling' else '固定预算'}", "budget_mode": mode, "rolling_months": rolling_months}


# ──────────────────────────────────────────────
# 滚动预算自动延展
# ──────────────────────────────────────────────

@router.post("/roll-forward")
def budget_roll_forward(
    db: Session = Depends(get_db),
    current_user=Depends(require_auth),
):
    """
    滚动预算自动延展：
    - 删除最早一个月的预测数据
    - 新增未来一个月的预测数据（取最近三个月均值）
    - 返回延展结果
    """
    from app.models import SystemConfig
    cfg = db.query(SystemConfig).filter(SystemConfig.config_key == "budget_mode").first()
    import json
    if not cfg:
        raise HTTPException(400, "未配置预算模式，请先设置")
    try:
        val = json.loads(cfg.config_value)
    except (json.JSONDecodeError, TypeError):
        raise HTTPException(400, "预算模式配置异常")

    if val.get("mode") != "rolling":
        raise HTTPException(400, "当前为固定预算模式，无需延展")

    now = datetime.now()
    current_year, current_month = now.year, now.month

    # 获取所有active的预算记录
    plans = db.query(BudgetPlan).filter(BudgetPlan.status == "active").all()

    # 按KPI分组
    from collections import defaultdict
    kpi_plans = defaultdict(list)
    for p in plans:
        kpi_plans[p.kpi_id].append(p)

    rolled_kpis = []
    for kpi_id, p_list in kpi_plans.items():
        # 按期间排序
        p_list.sort(key=lambda x: (x.budget_year, x.budget_month))

        # 找出最早的一个月并删除
        if p_list:
            oldest = p_list[0]
            db.query(BudgetPlan).filter(BudgetPlan.id == oldest.id).delete()

        # 计算新增月份的预算值（取最近三个月均值）
        recent_values = [p.budget_value for p in p_list[-3:]] if len(p_list) >= 3 else [p.budget_value for p in p_list]
        avg_value = round(sum(recent_values) / len(recent_values), 2) if recent_values else 0

        # 计算新的月份（当前月 + 12个月后）
        new_year = current_year
        new_month = current_month + val.get("rolling_months", 12)
        while new_month > 12:
            new_month -= 12
            new_year += 1

        new_period = f"{new_year}-{new_month:02d}"

        # 检查是否已存在
        existing = db.query(BudgetPlan).filter(
            BudgetPlan.kpi_id == kpi_id,
            BudgetPlan.period == new_period,
            BudgetPlan.status == "active",
        ).first()

        if not existing:
            bp = BudgetPlan(
                kpi_id=kpi_id,
                period=new_period,
                budget_value=avg_value,
                budget_year=new_year,
                budget_month=new_month,
                version="rolling",
                status="active",
                remark=f"滚动延展自{current_year}-{current_month:02d}",
                created_by=current_user.name if hasattr(current_user, "name") else "",
            )
            db.add(bp)
            rolled_kpis.append({
                "kpi_id": kpi_id,
                "removed_period": f"{p_list[0].budget_year}-{p_list[0].budget_month:02d}" if p_list else None,
                "added_period": new_period,
                "predicted_value": avg_value,
            })

    db.commit()
    return {
        "message": f"滚动预算已延展，处理了 {len(rolled_kpis)} 个KPI",
        "rolled_kpis": rolled_kpis,
        "current_month": f"{current_year}-{current_month:02d}",
        "rolling_months": val.get("rolling_months", 12),
    }


# ──────────────────────────────────────────────
# 实际vs预测对比
# ──────────────────────────────────────────────

@router.get("/comparison")
def get_budget_comparison(
    kpi_id: Optional[int] = Query(None),
    year: Optional[int] = Query(None),
    db: Session = Depends(get_db),
):
    """
    获取实际值vs预测值对比数据
    返回：各月预算值、实际值、偏差率，以及分界点标记
    """
    from app.models import KPIValue, SystemConfig
    import json

    now = datetime.now()
    y = year or now.year

    # 判断预算模式
    cfg = db.query(SystemConfig).filter(SystemConfig.config_key == "budget_mode").first()
    budget_mode = "fixed"
    rolling_months = 12
    if cfg:
        try:
            val = json.loads(cfg.config_value)
            budget_mode = val.get("mode", "fixed")
            rolling_months = val.get("rolling_months", 12)
        except (json.JSONDecodeError, TypeError):
            pass

    # 确定查询的月份范围
    if budget_mode == "rolling":
        # 滚动预算：从当月起的 rolling_months 个月
        start_year, start_month = now.year, now.month
        periods = []
        for i in range(rolling_months):
            m = start_month + i
            yy = start_year
            while m > 12:
                m -= 12
                yy += 1
            periods.append(f"{yy}-{m:02d}")
    else:
        # 固定预算：全年1-12月
        periods = [f"{y}-{m:02d}" for m in range(1, 13)]

    # 查询预算数据
    query = db.query(BudgetPlan).join(
        KPIDefinition, BudgetPlan.kpi_id == KPIDefinition.id
    )
    if kpi_id:
        query = query.filter(BudgetPlan.kpi_id == kpi_id)
    query = query.filter(BudgetPlan.period.in_(periods), BudgetPlan.status == "active")
    budget_plans = query.all()

    # 按KPI+期间索引
    bp_map = {}
    for bp in budget_plans:
        key = (bp.kpi_id, bp.period)
        bp_map[key] = bp.budget_value

    # 查询实际值
    kpi_ids = set(bp.kpi_id for bp in budget_plans)
    actual_values = {}
    if kpi_ids:
        values = db.query(KPIValue).filter(
            KPIValue.kpi_id.in_(kpi_ids),
            KPIValue.period.in_(periods),
            KPIValue.actual_value.isnot(None),
        ).all()
        for v in values:
            key = (v.kpi_id, v.period)
            actual_values[key] = v.actual_value

    # 构建对比数据
    now_period = now.strftime("%Y-%m")
    months_data = []
    for period in periods:
        monthly = {"period": period, "is_current_period": period == now_period}
        total_budget = 0
        total_actual = 0
        count_budget = 0
        count_actual = 0
        for kpi_id_item in kpi_ids:
            bp_key = (kpi_id_item, period)
            if bp_key in bp_map:
                total_budget += bp_map[bp_key] or 0
                count_budget += 1
            if bp_key in actual_values:
                total_actual += actual_values[bp_key] or 0
                count_actual += 1

        monthly["budget_total"] = round(total_budget, 2)
        monthly["actual_total"] = round(total_actual, 2)
        monthly["kpi_count"] = len(kpi_ids)

        # 分界点标记
        if budget_mode == "rolling":
            # 滚动预算下，当前月之后为预测值
            monthly["is_prediction"] = period > now_period
        else:
            monthly["is_prediction"] = period > now_period

        # 偏差率
        if monthly["budget_total"] and monthly["budget_total"] > 0:
            monthly["deviation_rate"] = round(
                (monthly["actual_total"] - monthly["budget_total"]) / monthly["budget_total"] * 100, 2
            ) if monthly["actual_total"] is not None else None
        else:
            monthly["deviation_rate"] = None

        months_data.append(monthly)

    return {
        "periods": periods,
        "budget_mode": budget_mode,
        "year": y,
        "current_period": now_period,
        "months_data": months_data,
        "total_kpis": len(kpi_ids),
    }


@router.get("/comparison/kpi/{kpi_id}")
def get_kpi_comparison(
    kpi_id: int,
    year: Optional[int] = Query(None),
    db: Session = Depends(get_db),
):
    """
    获取单个KPI的实际vs预测对比数据（用于图表展示）
    """
    from app.models import KPIValue, SystemConfig
    import json

    now = datetime.now()
    y = year or now.year

    kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
    if not kpi:
        raise HTTPException(404, "KPI不存在")

    periods = [f"{y}-{m:02d}" for m in range(1, 13)]

    # 预算值
    budgets = db.query(BudgetPlan).filter(
        BudgetPlan.kpi_id == kpi_id,
        BudgetPlan.period.in_(periods),
        BudgetPlan.status == "active",
    ).all()
    budget_map = {bp.period: bp.budget_value for bp in budgets}

    # 实际值
    actuals = db.query(KPIValue).filter(
        KPIValue.kpi_id == kpi_id,
        KPIValue.period.in_(periods),
        KPIValue.actual_value.isnot(None),
    ).all()
    actual_map = {av.period: av.actual_value for av in actuals}

    now_period = now.strftime("%Y-%m")
    data_points = []
    for period in periods:
        bv = budget_map.get(period)
        av = actual_map.get(period)
        dr = None
        if bv and bv > 0 and av is not None:
            dr = round((av - bv) / bv * 100, 2)

        data_points.append({
            "period": period,
            "budget_value": bv,
            "actual_value": av,
            "deviation_rate": dr,
            "is_prediction": period > now_period,
            "is_current_period": period == now_period,
        })

    return {
        "kpi_id": kpi.id,
        "kpi_code": kpi.kpi_code,
        "kpi_name": kpi.kpi_name,
        "unit": kpi.unit or "",
        "year": y,
        "current_period": now_period,
        "data_points": data_points,
    }


# ──────────────────────────────────────────────
# 预测偏差告警
# ──────────────────────────────────────────────

@router.post("/deviation-check")
def check_budget_deviation(
    data: dict,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
    current_user=Depends(require_auth),
):
    """
    检查实际vs预测偏差，当偏差超过阈值时自动生成预警
    (2026-08-28 P1-③/P2-⑤: 统一走 build_deviation_alert，写入归因JSON+场景建议)
    """
    from app.models import KPIValue, BudgetDeviationAlert
    from sqlalchemy import func
    from app.utils.deviation_engine import build_deviation_alert

    threshold = data.get("threshold", 20)  # 默认20%
    period = data.get("period") or datetime.now().strftime("%Y-%m")

    # 查询该期间有预算的KPI（多租户隔离 entity_id）
    budget_plans = db.query(BudgetPlan).filter(
        BudgetPlan.entity_id == entity_id,
        BudgetPlan.period == period,
        BudgetPlan.status == "active",
    ).all()

    if not budget_plans:
        return {
            "message": f"期间 {period} 无预算数据",
            "alerts_generated": 0,
            "alerts": [],
        }

    alerts_generated = 0
    alerts = []

    for bp in budget_plans:
        kpi_obj = db.query(KPIDefinition).filter(
            KPIDefinition.id == bp.kpi_id,
            KPIDefinition.entity_id == entity_id,
        ).first()
        if not kpi_obj:
            continue

        # 统一告警逻辑（方向性/阈值/归因/场景建议）
        result = build_deviation_alert(db, kpi_obj, period, entity_id=entity_id, min_rate=threshold)
        if not result["triggered"]:
            continue

        deviation = result["deviation"]
        budget_val = deviation.get("budget_value")
        actual_val = deviation.get("actual_value")
        deviation_rate = deviation.get("deviation_rate")
        deviation_value = deviation.get("deviation_amount")
        if deviation_value is None:
            deviation_value = round((actual_val or 0) - (budget_val or 0), 2)

        # 检查是否已存在相同的预警
        existing_alert = db.query(BudgetDeviationAlert).filter(
            BudgetDeviationAlert.kpi_id == bp.kpi_id,
            BudgetDeviationAlert.period == period,
            BudgetDeviationAlert.status == "open",
        ).first()

        if existing_alert:
            # 已存在open告警: 补齐归因（原open告警可能无归因, 幂等补写）
            if existing_alert.attribution is None and result["attribution"]:
                existing_alert.attribution = result["attribution"]
                existing_alert.alert_type = result["alert_type"]
                existing_alert.scenario_id = result["scenario_id"]
                db.flush()
            continue

        alert = BudgetDeviationAlert(
            kpi_id=bp.kpi_id,
            period=period,
            budget_value=budget_val,
            actual_value=actual_val,
            deviation_rate=deviation_rate,
            deviation_value=deviation_value,
            alert_level=result["level"],
            status="open",
            suggestion=result["suggestion"],
            alert_type=result["alert_type"],
            attribution=result["attribution"],
            scenario_id=result["scenario_id"],
        )
        db.add(alert)
        alerts_generated += 1

        alerts.append({
            "kpi_id": bp.kpi_id,
            "kpi_code": kpi_obj.kpi_code if kpi_obj else "",
            "kpi_name": kpi_obj.kpi_name if kpi_obj else "",
            "period": period,
            "budget_value": budget_val,
            "actual_value": actual_val,
            "deviation_rate": deviation_rate,
            "deviation_value": deviation_value,
            "alert_level": result["level"],
            "suggestion": result["suggestion"],
            "alert_type": result["alert_type"],
            "attribution": result["attribution"],
        })

    db.commit()

    return {
        "message": f"检查完成，生成了 {alerts_generated} 条预警",
        "period": period,
        "threshold": threshold,
        "alerts_generated": alerts_generated,
        "alerts": alerts,
    }


@router.get("/deviation-alerts")
def list_deviation_alerts(
    kpi_id: Optional[int] = Query(None),
    period: Optional[str] = Query(None),
    alert_level: Optional[str] = Query(None),
    status: Optional[str] = Query(None),
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """查询预算偏差预警记录 (2026-08-28: 列表新增 alert_type/attribution/scenario_id，entity_id隔离)"""
    from app.models import BudgetDeviationAlert
    query = db.query(BudgetDeviationAlert).filter(BudgetDeviationAlert.entity_id == entity_id)
    if kpi_id:
        query = query.filter(BudgetDeviationAlert.kpi_id == kpi_id)
    if period:
        query = query.filter(BudgetDeviationAlert.period == period)
    if alert_level:
        query = query.filter(BudgetDeviationAlert.alert_level == alert_level)
    if status:
        query = query.filter(BudgetDeviationAlert.status == status)

    alerts = query.order_by(BudgetDeviationAlert.created_at.desc()).all()
    result = []
    for a in alerts:
        kpi_obj = db.query(KPIDefinition).filter(KPIDefinition.id == a.kpi_id).first()
        result.append({
            "id": a.id,
            "kpi_id": a.kpi_id,
            "kpi_code": kpi_obj.kpi_code if kpi_obj else "",
            "kpi_name": kpi_obj.kpi_name if kpi_obj else "",
            "period": a.period,
            "budget_value": a.budget_value,
            "actual_value": a.actual_value,
            "deviation_rate": a.deviation_rate,
            "deviation_value": a.deviation_value,
            "alert_level": a.alert_level,
            "status": a.status,
            "suggestion": a.suggestion,
            "alert_type": a.alert_type,
            "attribution": a.attribution,
            "scenario_id": a.scenario_id,
            "created_at": a.created_at.isoformat() if a.created_at else None,
        })
    return {"data": result, "total": len(result)}


@router.get("/deviation-alerts/{alert_id}/attribution")
def get_deviation_alert_attribution(
    alert_id: int,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """告警归因详情 — 告警 + 归因JSON + 场景建议（联查 scenario_suggestions）(P1-③ 2026-08-28)"""
    from app.models import BudgetDeviationAlert, ScenarioSuggestion
    from app.utils.alert_attribution import match_scenario

    alert = db.query(BudgetDeviationAlert).filter(
        BudgetDeviationAlert.id == alert_id,
        BudgetDeviationAlert.entity_id == entity_id,
    ).first()
    if not alert:
        raise HTTPException(404, "预警记录不存在")

    kpi_obj = db.query(KPIDefinition).filter(KPIDefinition.id == alert.kpi_id).first()

    # 归因（若旧告警无归因字段，现场组装一次）
    attribution = alert.attribution
    if attribution is None:
        from app.utils.alert_attribution import build_attribution
        try:
            attribution, inferred_type = build_attribution(db, alert.kpi_id, alert.period, alert.alert_type)
            alert.attribution = attribution
            if alert.alert_type is None:
                alert.alert_type = inferred_type
            db.commit()
        except Exception:
            attribution = {}

    scenario = None
    if alert.scenario_id or alert.alert_type:
        scenario = match_scenario(db, alert.alert_type)

    return {
        "id": alert.id,
        "kpi_id": alert.kpi_id,
        "kpi_code": kpi_obj.kpi_code if kpi_obj else "",
        "kpi_name": kpi_obj.kpi_name if kpi_obj else "",
        "period": alert.period,
        "budget_value": alert.budget_value,
        "actual_value": alert.actual_value,
        "deviation_rate": alert.deviation_rate,
        "deviation_value": alert.deviation_value,
        "alert_level": alert.alert_level,
        "status": alert.status,
        "suggestion": alert.suggestion,
        "alert_type": alert.alert_type,
        "attribution": attribution or {},
        "scenario": scenario,
        "created_at": alert.created_at.isoformat() if alert.created_at else None,
    }


@router.get("/alert-direction")
def get_alert_direction(
    db: Session = Depends(get_db),
):
    """越高越好型KPI方向配置 (P2-⑤ 2026-08-28: system_configs 可维护)"""
    from app.utils.deviation_engine import get_higher_better_codes, CONFIG_KEY_HIGHER_BETTER
    from app.models import SystemConfig

    cfg = db.query(SystemConfig).filter(
        SystemConfig.config_key == CONFIG_KEY_HIGHER_BETTER
    ).first()
    codes = get_higher_better_codes(db)
    return {
        "config_key": CONFIG_KEY_HIGHER_BETTER,
        "codes": codes,
        "is_configured": bool(cfg and cfg.config_value),
    }


@router.put("/alert-direction")
def update_alert_direction(
    data: dict,
    db: Session = Depends(get_db),
    current_user=Depends(require_auth),
):
    """维护越高越好型KPI方向配置 (P2-⑤) body: {codes: ["SALES_TOTAL", ...]}"""
    import json as _json
    from app.utils.deviation_engine import CONFIG_KEY_HIGHER_BETTER
    from app.models import SystemConfig

    codes = data.get("codes")
    if not isinstance(codes, list):
        raise HTTPException(400, "codes 必须是非空数组")
    codes = [str(c) for c in codes]

    cfg = db.query(SystemConfig).filter(
        SystemConfig.config_key == CONFIG_KEY_HIGHER_BETTER
    ).first()
    if cfg:
        cfg.config_value = _json.dumps(codes, ensure_ascii=False)
    else:
        db.add(SystemConfig(
            config_key=CONFIG_KEY_HIGHER_BETTER,
            config_value=_json.dumps(codes, ensure_ascii=False),
            description="越高越好型KPI编码列表（实际低于预算才告警）",
        ))
    db.commit()
    return {"success": True, "config_key": CONFIG_KEY_HIGHER_BETTER, "codes": codes}


@router.put("/deviation-alerts/{alert_id}")
def update_deviation_alert(
    alert_id: int,
    data: dict,
    db: Session = Depends(get_db),
):
    """更新偏差预警（如标记已解决）"""
    from app.models import BudgetDeviationAlert
    alert = db.query(BudgetDeviationAlert).filter(BudgetDeviationAlert.id == alert_id).first()
    if not alert:
        raise HTTPException(404, "预警记录不存在")
    if "status" in data:
        alert.status = data["status"]
    db.commit()
    return {"message": "预警已更新"}


# ──────────────────────────────────────────────
# 功能6: 预算方法三选一向导 (CMA P1 - 增量/零基/弹性)
# ──────────────────────────────────────────────

@router.post("/method-comparison")
def budget_method_comparison(
    data: dict,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """
    预算方法三选一对比计算
    接收: { entity: "hanke", last_month_budget: 91, current_revenue: 122, ... }
    返回三种方法的计算结果
    (2026-08-28 P2-①: zero_based 优先读逐项论证项 budget_zero_based_items,
     传入 zero_based_kpi_id+zero_based_period 且有论证项 → 逐项求和 is_demo=false;
     无论证项 → fallback 旧公式 is_demo=true)
    """
    entity = data.get("entity", "hanke")
    last_month_budget = data.get("last_month_budget", 91)  # 上月预算（万）
    current_revenue = data.get("current_revenue", 122)  # 当前收入（万）
    fixed_costs = data.get("fixed_costs", {
        "rent": 15,       # 房租（万）
        "labor": 40,      # 人工（万）
        "entertainment": 16,  # 招待费（万）
        "misc": 12,       # 杂项（万）
    })
    variable_cost_rate = data.get("variable_cost_rate", 0.4862)  # 变动成本率

    # 1. 增量预算: 基于上月统一调整
    increment_rate = data.get("increment_rate", 0.05)  # 5%增幅
    incremental_result = round(last_month_budget * (1 + increment_rate), 1)
    incremental_detail = f"上月{last_month_budget}万 × (1+{increment_rate*100:.0f}%) = {incremental_result}万"

    # 2. 零基预算: 优先逐项论证（P2-① 真零基）
    zbb_kpi_id = data.get("zero_based_kpi_id")
    zbb_period = data.get("zero_based_period")
    zbb_is_demo = True
    zbb_items = []
    if zbb_kpi_id and zbb_period:
        from app.models import BudgetZeroBasedItem
        zbb_items = db.query(BudgetZeroBasedItem).filter(
            BudgetZeroBasedItem.entity_id == entity_id,
            BudgetZeroBasedItem.kpi_id == zbb_kpi_id,
            BudgetZeroBasedItem.period == zbb_period,
        ).all()

    if zbb_items:
        # 真零基: 逐项求和（仅 approved+draft 都算，draft为未定稿）
        zbb_total = round(sum(i.proposed_value for i in zbb_items), 1)
        zbb_is_demo = False
        zbb_detail = "零基逐项论证: " + " + ".join(
            f"{i.item_name}{i.proposed_value}万" for i in zbb_items
        ) + f" = {zbb_total}万"
        zbb_savings = round(last_month_budget - zbb_total, 1)
    else:
        # fallback 旧演示公式（标注 is_demo）
        zbb_entertainment = round(fixed_costs.get("entertainment", 16) / 2, 1)  # 砍半
        zbb_misc = round(fixed_costs.get("misc", 12) * 0.7, 1)  # 压缩30%
        zbb_total = round(
            fixed_costs.get("rent", 15)
            + fixed_costs.get("labor", 40)
            + zbb_entertainment
            + zbb_misc,
            1,
        )
        zbb_savings = round(last_month_budget - zbb_total, 1)
        zbb_detail = (
            f"房租{fixed_costs.get('rent', 15)}万(固定)+人工{fixed_costs.get('labor', 40)}万(砍不掉)"
            f"+招待{zbb_entertainment}万(砍半)+杂项{zbb_misc}万(压缩)"
            f"={zbb_total}万 ← 省{zbb_savings}万"
        )

    # 3. 弹性预算: 根据收入水平动态调整
    flexible_fixed = round(fixed_costs.get("rent", 15) + fixed_costs.get("labor", 40) * 0.5, 1)
    flexible_variable = round(current_revenue * variable_cost_rate * 0.4, 1)
    flexible_total = round(flexible_fixed + flexible_variable, 1)
    flexible_variance = round(last_month_budget - flexible_total, 1)
    flex_detail = (
        f"收入{current_revenue}万 → 对应费用预算 = {flexible_total}万"
        f"（固定部分{flexible_fixed}万+变动部分{flexible_variable}万）"
        f"，实际{last_month_budget}万 → 差异{flexible_variance}万 → {'效率问题' if flexible_variance > 0 else '节省'}"
    )

    # 推荐方法
    recommended = "zero_based"

    return {
        "entity": entity,
        "entity_name": "陕西酣客（白酒经销）" if entity == "hanke" else "陕西博海科技（IT服务）",
        "methods": [
            {
                "id": "incremental",
                "name": "增量预算",
                "name_en": "Incremental Budgeting",
                "result_value": incremental_result,
                "detail": incremental_detail,
                "pros": "简单快速",
                "cons": "浪费持续",
                "is_recommended": False,
            },
            {
                "id": "zero_based",
                "name": "零基预算",
                "name_en": "Zero-Based Budgeting (ZBB)",
                "result_value": zbb_total,
                "savings": zbb_savings,
                "detail": zbb_detail,
                "is_demo": zbb_is_demo,
                "item_count": len(zbb_items),
                "pros": "最合理",
                "cons": "耗时",
                "is_recommended": True,
            },
            {
                "id": "flexible",
                "name": "弹性预算",
                "name_en": "Flexible Budgeting",
                "result_value": flexible_total,
                "variance": flexible_variance,
                "detail": flex_detail,
                "pros": "动态响应",
                "cons": "需要详细分类",
                "is_recommended": False,
            },
        ],
        "recommended": recommended,
        "recommended_name": "零基预算",
    }


@router.post("/apply-method")
def apply_budget_method(
    data: dict,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
    current_user=Depends(require_auth),
):
    """应用所选预算编制方法到预算计划（2026-08-26：三法并存，按用户场景选择后落地）
    接收: { method: 'incremental'|'zero_based'|'flexible', year: 2026, entity: 'hanke', ... }
    说明: 方法计算结果 → 写入/更新预算计划（version标注方法名，便于追溯）
    (2026-08-28 P2-②: KPI派生规则可配置 budget_derivation_rules,
     percentage_of → base_kpi实际值×rate; incremental → 上月×(1+rate);
     无规则 fallback 默认比例(净利2%/费用率22%/毛利18%), 响应带 rule_source)
    """
    from app.models import BudgetDerivationRule

    method = data.get("method", "zero_based")
    year = data.get("year", datetime.now().year)
    entity = data.get("entity", "hanke")

    # 复用method-comparison计算（获得三法结果）
    comp = budget_method_comparison({
        "entity": entity,
        "last_month_budget": data.get("last_month_budget", 91),
        "current_revenue": data.get("current_revenue", 122),
        "fixed_costs": data.get("fixed_costs", {
            "rent": 15, "labor": 40, "entertainment": 16, "misc": 12,
        }),
        "variable_cost_rate": data.get("variable_cost_rate", 0.4862),
        "increment_rate": data.get("increment_rate", 0.05),
        "zero_based_kpi_id": data.get("zero_based_kpi_id"),
        "zero_based_period": data.get("zero_based_period"),
    }, db=db, entity_id=entity_id)

    # 找所选方法的结果
    selected = None
    for m in comp["methods"]:
        if m["id"] == method:
            selected = m
            break
    if not selected:
        raise HTTPException(400, "未知预算方法: " + method)

    # 找到该年的核心KPI（营业收入/净利润/费用率等）
    kpis = db.query(KPIDefinition).filter(
        KPIDefinition.entity_id == entity_id,
        KPIDefinition.status == "active",
        KPIDefinition.kpi_code.in_(["F_REVENUE", "F_NET_PROFIT", "F_COST_RATIO", "F_GROSS_MARGIN"]),
    ).all()
    if not kpis:
        raise HTTPException(400, "未找到可应用的KPI")

    # 加载派生规则（P2-②）
    rules = db.query(BudgetDerivationRule).filter(
        BudgetDerivationRule.entity_id == entity_id,
        BudgetDerivationRule.status == "active",
    ).all()
    rules_by_kpi = {r.kpi_id: r for r in rules}

    # 版本
    version = f"{method}-{datetime.now().strftime('%Y%m%d')}"
    applied = []
    used_configured = False

    for kpi in kpis:
        period = f"{year}-00"
        # 删除旧版本的同KPI年度预算
        db.query(BudgetPlan).filter(
            BudgetPlan.kpi_id == kpi.id,
            BudgetPlan.period == period,
            BudgetPlan.version.like(f"{method}-%"),
        ).delete()

        # 各KPI的应用值：收入用方法结果，其他优先派生规则（P2-②）
        if kpi.kpi_code == "F_REVENUE":
            budget_val = selected["result_value"]
            rule_source = "default"
            formula_note = "方法结果"
        else:
            rule = rules_by_kpi.get(kpi.id)
            if rule and rule.params:
                rate = float(rule.params.get("rate", 0.02))
                if rule.rule_type == "percentage_of" and rule.base_kpi_id:
                    # 来源KPI实际值 × 比例
                    base_val = None
                    base_actual = db.query(KPIValue).filter(
                        KPIValue.kpi_id == rule.base_kpi_id,
                        KPIValue.actual_value.isnot(None),
                    ).order_by(KPIValue.calculated_at.desc()).first()
                    if base_actual:
                        base_val = base_actual.actual_value
                    if base_val is not None:
                        budget_val = round(base_val * rate, 1)
                        rule_source = "configured"
                        formula_note = f"派生: 来源KPI实际值{base_val} × {rate}"
                    else:
                        budget_val = round(selected["result_value"] * rate, 1)
                        rule_source = "configured_fallback"
                        formula_note = f"派生规则无来源实际值, 按方法结果×{rate}"
                elif rule.rule_type == "incremental":
                    # 上月预算 × (1+rate)
                    prev_period = f"{year-1}-00"
                    prev_plan = db.query(BudgetPlan).filter(
                        BudgetPlan.entity_id == entity_id,
                        BudgetPlan.kpi_id == kpi.id,
                        BudgetPlan.period == prev_period,
                        BudgetPlan.status == "active",
                    ).order_by(BudgetPlan.updated_at.desc()).first()
                    if prev_plan and prev_plan.budget_value is not None:
                        budget_val = round(prev_plan.budget_value * (1 + rate), 1)
                        rule_source = "configured"
                        formula_note = f"派生: 上年预算{prev_plan.budget_value} × (1+{rate})"
                    else:
                        budget_val = round(selected["result_value"] * rate, 1)
                        rule_source = "configured_fallback"
                        formula_note = f"派生规则无上年预算, 按方法结果×{rate}"
                else:
                    # formula 类型: 暂按方法结果×rate 兜底
                    budget_val = round(selected["result_value"] * rate, 1)
                    rule_source = "configured"
                    formula_note = f"派生规则(formula): 方法结果×{rate}"
            else:
                # fallback 默认比例
                if kpi.kpi_code == "F_NET_PROFIT":
                    budget_val = round(selected["result_value"] * 0.02, 1)  # 净利率约2%
                elif kpi.kpi_code == "F_COST_RATIO":
                    budget_val = round(selected["result_value"] * 0.22, 1)  # 费用率约22%
                else:  # F_GROSS_MARGIN
                    budget_val = round(selected["result_value"] * 0.18, 1)  # 毛利率约18%
                rule_source = "default"
                formula_note = "默认比例"

        if rule_source in ("configured", "configured_fallback"):
            used_configured = True

        bp = BudgetPlan(
            entity_id=entity_id,
            kpi_id=kpi.id,
            period=period,
            budget_value=budget_val,
            budget_year=year,
            budget_month=0,
            version=version,
            status="active",
            remark=f"{selected['name']}应用({selected['result_value']}万) 来源{method} | {formula_note}",
            calc_logic=formula_note,
        )
        db.add(bp)
        applied.append({"kpi_code": kpi.kpi_code, "budget_value": budget_val, "rule_source": rule_source})

    db.commit()
    return {
        "message": f"已应用「{selected['name']}」到预算计划",
        "method": method,
        "method_name": selected["name"],
        "version": version,
        "total_budget": selected["result_value"],
        "detail": selected["detail"],
        "applied": applied,
        "rule_source": "configured" if used_configured else "default",
        "note": "选择哪种方法取决于场景：增量=稳定业务快速编；零基=成本优化专项；弹性=收入波动大。方法结果写入年度预算(period=YYYY-00)，可在版本管理中查看。KPI派生规则可在「派生规则配置」中维护（P2-②）。",
    }


# ============ 预算版本管理 API（2026-08-25 补充，修复前端"加载版本失败"） ============

@router.get("/versions")
def list_budget_versions(
    year: Optional[int] = Query(None, description="预算年份"),
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """预算版本列表 — 按版本聚合统计"""
    query = db.query(BudgetPlan).filter(BudgetPlan.entity_id == entity_id)
    if year:
        query = query.filter(BudgetPlan.budget_year == year)

    rows = query.all()
    # 按version+status聚合
    version_map = {}
    for r in rows:
        key = (r.version or "v1.0", r.status or "active")
        if key not in version_map:
            version_map[key] = {
                "version": r.version or "v1.0",
                "status": r.status or "active",
                "kpi_count": 0,
                "total_budget": 0.0,
                "periods": set(),
                "updated_at": r.updated_at,
            }
        v = version_map[key]
        v["kpi_count"] += 1
        v["total_budget"] += r.budget_value or 0
        v["periods"].add(r.period)
        if r.updated_at and (v["updated_at"] is None or r.updated_at > v["updated_at"]):
            v["updated_at"] = r.updated_at

    result = []
    for key, v in version_map.items():
        result.append({
            "version": v["version"],
            "status": v["status"],
            "kpi_count": v["kpi_count"],
            "total_budget": round(v["total_budget"], 2),
            "period_count": len(v["periods"]),
            "year": year,
            "updated_at": v["updated_at"].strftime("%Y-%m-%d %H:%M") if v["updated_at"] else "",
        })
    result.sort(key=lambda x: x["version"], reverse=True)
    return result


@router.post("/versions/submit")
def submit_budget_version(
    data: dict,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """提交版本审批：active → submitted"""
    version = data.get("version", "v1.0")
    rows = db.query(BudgetPlan).filter(
        BudgetPlan.entity_id == entity_id,
        BudgetPlan.version == version,
    ).all()
    if not rows:
        raise HTTPException(404, f"版本 {version} 不存在")
    for r in rows:
        r.status = "submitted"
    db.commit()
    return {"success": True, "version": version, "status": "submitted", "count": len(rows)}


@router.post("/versions/approve")
def approve_budget_version(
    data: dict,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """审批版本：submitted → approved/rejected"""
    version = data.get("version", "v1.0")
    action = data.get("action", "approved")
    rows = db.query(BudgetPlan).filter(
        BudgetPlan.entity_id == entity_id,
        BudgetPlan.version == version,
    ).all()
    if not rows:
        raise HTTPException(404, f"版本 {version} 不存在")
    new_status = "approved" if action == "approved" else "rejected"
    for r in rows:
        r.status = new_status
    db.commit()
    return {"success": True, "version": version, "status": new_status, "count": len(rows)}


@router.post("/versions/diff")
def diff_budget_versions(
    data: dict,
    db: Session = Depends(get_db),
    entity_id: int = Depends(get_entity_id),
):
    """版本差异对比：v1.0 vs v2.0 逐KPI差异"""
    version_a = data.get("version_a", "")
    version_b = data.get("version_b", "")
    year = data.get("year")
    if not version_a or not version_b:
        raise HTTPException(400, "需要 version_a 和 version_b")

    def load_version(ver: str):
        query = db.query(BudgetPlan).filter(
            BudgetPlan.entity_id == entity_id,
            BudgetPlan.version == ver,
        )
        if year:
            query = query.filter(BudgetPlan.budget_year == year)
        return {r.kpi_id: r for r in query.all()}

    va = load_version(version_a)
    vb = load_version(version_b)

    kpi_ids = set(va.keys()) | set(vb.keys())
    kpis = db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()
    kpi_map = {k.id: k for k in kpis}

    diffs = []
    total_a = total_b = 0.0
    changed_count = 0
    for kid in sorted(kpi_ids):
        ra = va.get(kid)
        rb = vb.get(kid)
        val_a = ra.budget_value if ra else 0
        val_b = rb.budget_value if rb else 0
        total_a += val_a
        total_b += val_b
        k = kpi_map.get(kid)
        diff = val_b - val_a
        if abs(diff) > 0.001:
            changed_count += 1
        diffs.append({
            "kpi_code": k.kpi_code if k else "",
            "kpi_name": k.kpi_name if k else f"KPI-{kid}",
            "version_a": round(val_a, 2),
            "version_b": round(val_b, 2),
            "diff": round(diff, 2),
        })

    diffs.sort(key=lambda x: -abs(x["diff"]))
    return {
        "summary": {
            "version_a": version_a,
            "version_b": version_b,
            "kpi_total": len(kpi_ids),
            "changed_count": changed_count,
            "total_a": round(total_a, 2),
            "total_b": round(total_b, 2),
            "total_diff": round(total_b - total_a, 2),
        },
        "diffs": diffs,
    }


# ════════════════════════════════════════════════════════════
# 预算↔现金流联动（断点修复#1, 2026-08-27）
# ════════════════════════════════════════════════════════════

@router.post("/sync-cash-plans")
def sync_cash_plans(
    entity_id: int = Depends(get_entity_id),
    db: Session = Depends(get_db),
):
    """预算→现金流计划联动: 按预算KPI生成/更新收付款计划（修复断点#1）
    (2026-08-28 P2-⑥: 分类规则表优先, 未命中进待分类队列不再静默跳过)

    收入类KPI(营收/回款/新客) → receive
    成本类KPI(费用/厂补/采购) → pay
    分类来源: ①cash_plan_classify_rules规则表(精确KPI→关键词) ②默认关键词兜底 ③待分类队列
    upsert: 同KPI+同日期+同类型 更新不重复
    """
    from app.models import CashPlan, CashPlanClassifyRule, CashPlanUnclassified
    from datetime import datetime

    # 默认关键词兜底（兼容存量，规则表优先）
    RECEIVE_KEYS = ("营收", "收入", "销售", "回款", "新客", "收款", "净利润", "毛利")
    PAY_KEYS = ("费用", "成本", "厂补", "采购", "返利", "应付", "损耗", "投入")

    # 加载分类规则表（P2-⑥）
    rules = db.query(CashPlanClassifyRule).filter(
        CashPlanClassifyRule.entity_id == entity_id,
        CashPlanClassifyRule.status == "active",
    ).order_by(CashPlanClassifyRule.priority.asc()).all()
    kpi_rules = {r.kpi_id: r for r in rules if r.kpi_id}
    pattern_rules = [r for r in rules if not r.kpi_id and r.kpi_code_pattern]

    def classify_plan_type(kpi) -> Optional[str]:
        """返回 receive/pay/None(未分类)"""
        # ① 精确KPI匹配（优先）
        if kpi.id in kpi_rules:
            return kpi_rules[kpi.id].plan_type
        # ② 关键词/编码模式匹配（规则表）
        name = (kpi.kpi_name or "") + (kpi.kpi_code or "")
        for r in pattern_rules:
            if r.kpi_code_pattern and r.kpi_code_pattern in name:
                return r.plan_type
        # ③ 默认关键词兜底（兼容存量行为）
        if any(k in name for k in RECEIVE_KEYS):
            return "receive"
        if any(k in name for k in PAY_KEYS):
            return "pay"
        return None

    budgets = db.query(BudgetPlan).filter(
        BudgetPlan.entity_id == entity_id, BudgetPlan.status == "active"
    ).all()
    kpi_ids = {b.kpi_id for b in budgets}
    kpis = {k.id: k for k in db.query(KPIDefinition).filter(KPIDefinition.id.in_(kpi_ids)).all()} if kpi_ids else {}

    created, updated, unclassified_count = 0, 0, 0
    for b in budgets:
        kpi = kpis.get(b.kpi_id)
        if not kpi:
            continue
        plan_type = classify_plan_type(kpi)
        if plan_type is None:
            # 无法判类别 → 写入待分类队列（不静默跳过，P2-⑥）
            existing_un = db.query(CashPlanUnclassified).filter(
                CashPlanUnclassified.entity_id == entity_id,
                CashPlanUnclassified.kpi_id == b.kpi_id,
                CashPlanUnclassified.period == b.period,
                CashPlanUnclassified.status == "pending",
            ).first()
            if not existing_un:
                db.add(CashPlanUnclassified(
                    entity_id=entity_id,
                    kpi_id=b.kpi_id,
                    kpi_name=kpi.kpi_name or kpi.kpi_code,
                    period=b.period,
                    budget_value=b.budget_value,
                    reason="未匹配任何分类规则",
                    status="pending",
                ))
                unclassified_count += 1
            continue

        year, month = b.budget_year or 2026, b.budget_month or 1
        try:
            plan_date = datetime(year, month, 1)
        except Exception:
            continue
        # upsert: 同KPI+同日期+同类型
        existing = db.query(CashPlan).filter(
            CashPlan.entity_id == entity_id,
            CashPlan.related_kpi_id == b.kpi_id,
            CashPlan.plan_type == plan_type,
            CashPlan.plan_date == plan_date,
        ).first()
        if existing:
            existing.amount = b.budget_value
            existing.budget_plan_id = b.id
            existing.source = "budget_sync"
            updated += 1
        else:
            db.add(CashPlan(
                entity_id=entity_id, plan_type=plan_type,
                related_kpi_id=b.kpi_id, budget_plan_id=b.id,
                amount=b.budget_value, plan_date=plan_date,
                description=f"预算联动: {kpi.kpi_name or kpi.kpi_code}",
                status="pending", source="budget_sync",
            ))
            created += 1
    db.commit()
    return {
        "message": f"现金流联动完成: 新建{created}条, 更新{updated}条, 待分类{unclassified_count}条",
        "created": created, "updated": updated,
        "unclassified_count": unclassified_count,
    }
