"""差异预警引擎 — 管理会计OS
实际 vs 预算/目标对比，超阈值自动推送预警

功能:
  1. 实际 vs 预算差异计算（差异额/差异率）
  2. 同比/环比差异计算
  3. 趋势异常检测（连续N期下滑/上升）
  4. 差异预警触发（集成到现有预警系统）
"""
import logging
import json
from datetime import datetime
from typing import Optional

from app.database import get_session_local
from app.models import KPIDefinition, KPIValue, KPIAlert, BudgetPlan, SystemConfig

logger = logging.getLogger("cma.deviation")


# 越高越好型KPI默认列表（P2-⑤ 2026-08-28: 提为 system_configs 可配置）
DEFAULT_HIGHER_BETTER = [
    "SALES_TOTAL", "CUSTOMER_COUNT", "SALES_PROFIT_RATE",
    "RECEIVABLE_TURNOVER", "TURNOVER_RATE",
    "CUSTOMER_SATISFACTION", "ORDER_DELIVERY_RATE",
]
CONFIG_KEY_HIGHER_BETTER = "kpi_alert_higher_better"


def get_higher_better_codes(db, entity_id: int = None) -> list:
    """读取越高越好型KPI编码列表（system_configs 可维护，无配置回落默认）"""
    cfg = db.query(SystemConfig).filter(
        SystemConfig.config_key == CONFIG_KEY_HIGHER_BETTER
    ).first()
    if cfg and cfg.config_value:
        try:
            codes = json.loads(cfg.config_value)
            if isinstance(codes, list):
                return [str(c) for c in codes]
        except Exception:
            logger.warning("system_configs[%s] 解析失败, 回落默认", CONFIG_KEY_HIGHER_BETTER)
    return list(DEFAULT_HIGHER_BETTER)


# ============================================================
# 差异计算
# ============================================================

def calc_deviation(actual: float, budget: float) -> dict:
    """计算差异额和差异率"""
    if budget is None or budget == 0:
        return {
            "deviation_amount": None,
            "deviation_rate": None,
            "is_over_budget": None,
        }
    amount = round(actual - budget, 2)
    rate = round(amount / budget * 100, 2)
    return {
        "deviation_amount": amount,
        "deviation_rate": rate,
        "is_over_budget": amount > 0,
    }


def get_budget_for_kpi(db, kpi_id: int, period: str, version: str = None) -> Optional[float]:
    """获取指定KPI在指定期间的预算值"""
    query = db.query(BudgetPlan).filter(
        BudgetPlan.kpi_id == kpi_id,
        BudgetPlan.period == period,
        BudgetPlan.status == "active",
    )
    if version:
        query = query.filter(BudgetPlan.version == version)
    plan = query.order_by(BudgetPlan.updated_at.desc()).first()
    return plan.budget_value if plan else None


def get_actual_for_kpi(db, kpi_id: int, period: str) -> Optional[float]:
    """获取指定KPI在指定期间的实际值"""
    val = db.query(KPIValue).filter(
        KPIValue.kpi_id == kpi_id,
        KPIValue.period == period,
    ).order_by(KPIValue.calculated_at.desc()).first()
    return val.actual_value if val else None


def calc_period_deviation(db, kpi_id: int, period: str) -> dict:
    """单KPI单期的差异计算"""
    actual = get_actual_for_kpi(db, kpi_id, period)
    budget = get_budget_for_kpi(db, kpi_id, period)

    kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
    kpi_code = kpi.kpi_code if kpi else "unknown"

    # 如果没预算值，用 target_value 作为替代
    if budget is None and kpi:
        # 尝试把年度目标按月均分
        month = int(period.split("-")[1])
        target = kpi.target_value
        if target and target > 0 and kpi.frequency == "monthly":
            budget = round(target / 12, 2)

    deviation = calc_deviation(actual, budget) if actual is not None else None

    result = {
        "kpi_id": kpi_id,
        "kpi_code": kpi_code,
        "period": period,
        "actual_value": actual,
        "budget_value": budget,
    }
    if deviation:
        result.update(deviation)
    return result


# ============================================================
# 同比/环比差异
# ============================================================

def calc_period_diff(db, kpi_id: int, current_period: str, diff_type: str = "yoy") -> dict:
    """计算同比（上年同期）或环比（上期）差异"""
    year, month = current_period.split("-")
    y, m = int(year), int(month)

    if diff_type == "yoy":
        # 同比：上年同期
        prev_period = f"{y-1}-{m:02d}"
        label = "同比"
    elif diff_type == "mom":
        # 环比：上个月
        prev_m = m - 1
        prev_y = y
        if prev_m <= 0:
            prev_m += 12
            prev_y -= 1
        prev_period = f"{prev_y}-{prev_m:02d}"
        label = "环比"
    else:
        return {"error": f"未知比较类型: {diff_type}"}

    current = get_actual_for_kpi(db, kpi_id, current_period)
    previous = get_actual_for_kpi(db, kpi_id, prev_period)

    kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
    kpi_code = kpi.kpi_code if kpi else "unknown"

    if current is None or previous is None or previous == 0:
        return {
            "kpi_id": kpi_id,
            "kpi_code": kpi_code,
            "type": diff_type,
            "label": label,
            "current_period": current_period,
            "prev_period": prev_period,
            "current_value": current,
            "prev_value": previous,
            "diff_amount": None,
            "diff_rate": None,
        }

    diff_amount = round(current - previous, 2)
    diff_rate = round(diff_amount / previous * 100, 2)
    return {
        "kpi_id": kpi_id,
        "kpi_code": kpi_code,
        "type": diff_type,
        "label": label,
        "current_period": current_period,
        "prev_period": prev_period,
        "current_value": current,
        "prev_value": previous,
        "diff_amount": diff_amount,
        "diff_rate": diff_rate,
    }


# ============================================================
# 趋势检测
# ============================================================

def check_trend_anomaly(db, kpi_id: int, period: str, consecutive: int = 3) -> dict:
    """检测连续N期下滑或上升的趋势异常"""
    kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
    if not kpi:
        return {"anomaly": False}

    # 获取包括当前期在内的近期数据
    year, month = period.split("-")
    y, m = int(year), int(month)

    values = []
    for i in range(consecutive + 2):  # 多取2期做参考
        p = f"{y}-{m:02d}"
        v = get_actual_for_kpi(db, kpi_id, p)
        if v is not None:
            values.append({"period": p, "value": v})
        m -= 1
        if m <= 0:
            m += 12
            y -= 1

    values.reverse()  # 按时间正序
    if len(values) < consecutive:
        return {"anomaly": False, "reason": "数据不足"}

    last_n = values[-consecutive:]
    all_decreasing = all(last_n[i]["value"] > last_n[i + 1]["value"] for i in range(len(last_n) - 1))
    all_increasing = all(last_n[i]["value"] < last_n[i + 1]["value"] for i in range(len(last_n) - 1))

    if all_decreasing:
        return {
            "anomaly": True,
            "type": "continuous_decline",
            "level": "yellow" if consecutive >= 3 else "green",
            "periods": [v["period"] for v in last_n],
            "values": [v["value"] for v in last_n],
            "message": f"{kpi.kpi_name} 连续{consecutive}期下滑",
        }
    if all_increasing:
        return {
            "anomaly": True,
            "type": "continuous_rise",
            "level": "yellow" if consecutive >= 3 else "green",
            "periods": [v["period"] for v in last_n],
            "values": [v["value"] for v in last_n],
            "message": f"{kpi.kpi_name} 连续{consecutive}期上升（可能过热）",
        }

    return {"anomaly": False}


# ============================================================
# 差异预警触发
# ============================================================

def build_deviation_alert(db, kpi, period: str, entity_id: int = 1, min_rate: float = 10.0) -> dict:
    """统一告警构建 — 双出口共享一套逻辑 (P2-⑤ 2026-08-28)

    预算告警(budget_deviation_alerts) 与 KPIAlert 都调用本函数，差异仅级别映射：
      - budget 出口: warning/critical @ 20/50
      - KPIAlert 出口: yellow/red @ 10/30
    归因(P1-③): attribution 拆解 + 场景建议 由 alert_attribution 组装。

    返回:
      triggered: bool 是否触发
      level: budget出口级别 warning/critical
      kpi_alert_level: KPIAlert出口级别 yellow/red
      deviation: calc_period_deviation 结果
      suggestion: 模板建议文案
      alert_type: 归因场景类型
      attribution: 归因JSON dict
      scenario_id: 场景建议ID
    """
    from app.utils.alert_attribution import build_attribution, match_scenario

    deviation = calc_period_deviation(db, kpi.id, period)
    if deviation.get("deviation_rate") is None:
        return {"triggered": False}

    rate = abs(deviation["deviation_rate"])
    actual = deviation.get("actual_value")
    budget = deviation.get("budget_value")

    # 方向性：越高越好型（配置化，system_configs.kpi_alert_higher_better）
    higher_better = kpi.kpi_code in get_higher_better_codes(db, entity_id)

    if higher_better:
        # 实际低于预算才是问题
        if not (actual is not None and budget is not None and actual < budget and rate >= min_rate):
            return {"triggered": False}
        suggestion = (
            f"实际值低于预算 {rate}%，建议分析业务量未达预期的原因（子KPI拆解见归因），"
            f"制定增量获客或转化提升计划"
        )
    else:
        # 实际高于预算才是问题（成本型）
        if not (actual is not None and budget is not None and actual > budget and rate >= min_rate):
            return {"triggered": False}
        suggestion = (
            f"实际值超出预算 {rate}%，建议核查超支原因（科目明细拆解见归因）并采取控制措施"
        )

    # 级别映射（双出口）
    budget_level = "critical" if rate > 50 else "warning"
    kpi_alert_level = "red" if rate >= 30 else "yellow"

    # 归因组装 (P1-③)
    alert_type = None
    attribution = None
    scenario_id = None
    try:
        attribution, alert_type = build_attribution(db, kpi.id, period)
        scenario = match_scenario(db, alert_type)
        if scenario:
            scenario_id = scenario["scenario_id"]
    except Exception as e:  # 归因失败不阻断告警主流程
        logger.warning("归因组装失败 kpi=%s: %s", kpi.kpi_code, e)

    return {
        "triggered": True,
        "level": budget_level,
        "kpi_alert_level": kpi_alert_level,
        "deviation": deviation,
        "suggestion": suggestion,
        "alert_type": alert_type,
        "attribution": attribution,
        "scenario_id": scenario_id,
    }


def run_deviation_check(db_session, period: str = None) -> int:
    """运行差异预警检查，返回新增预警数（统一走 build_deviation_alert，P2-⑤）"""
    if period is None:
        period = datetime.now().strftime("%Y-%m")

    kpis = db_session.query(KPIDefinition).filter(
        KPIDefinition.status == "active"
    ).all()

    new_count = 0
    for kpi in kpis:
        # 1. 差异预警：实际 vs 预算（统一逻辑）
        result = build_deviation_alert(db_session, kpi, period)
        if result["triggered"]:
            deviation = result["deviation"]
            alert_msg = (
                f"{kpi.kpi_name}[{period}] 差异预警: 实际{deviation['actual_value']} "
                f"vs 预算{deviation['budget_value']}，"
                f"差异率{deviation['deviation_rate']}%"
            )

            # 检查是否已有同KPI同期间的差异预警
            existing = db_session.query(KPIAlert).filter(
                KPIAlert.kpi_id == kpi.id,
                KPIAlert.alert_message.contains("[差异预警]"),
                KPIAlert.alert_message.contains(period),
                KPIAlert.status.in_(["pending", "processing"]),
            ).first()

            if not existing:
                alert = KPIAlert(
                    kpi_id=kpi.id,
                    alert_level=result["kpi_alert_level"],
                    alert_message=f"[差异预警] {alert_msg}",
                    alert_type=result["alert_type"] or "actual",
                    suggestion=result["suggestion"],
                    status="pending",
                )
                db_session.add(alert)
                new_count += 1
                logger.info(f"  新增差异预警 [{result['kpi_alert_level']}] {kpi.kpi_name}: 差异率{deviation['deviation_rate']}%")

        # 2. 趋势异常检测（每期检查连续3期）
        trend = check_trend_anomaly(db_session, kpi.id, period, consecutive=3)
        if trend.get("anomaly") and trend.get("level") in ("yellow", "red"):
            trend_alert_msg = trend["message"]
            existing_trend = db_session.query(KPIAlert).filter(
                KPIAlert.kpi_id == kpi.id,
                KPIAlert.alert_message.contains("[趋势预警]"),
                KPIAlert.status.in_(["pending", "processing"]),
            ).first()

            if not existing_trend:
                alert = KPIAlert(
                    kpi_id=kpi.id,
                    alert_level=trend["level"],
                    alert_message=f"[趋势预警] {trend_alert_msg}",
                    status="pending",
                )
                db_session.add(alert)
                new_count += 1
                logger.info(f"  新增趋势预警 [{trend['level']}] {trend_alert_msg}")

    db_session.commit()
    return new_count


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )
    db = get_session_local()()
    try:
        n = run_deviation_check(db)
        print(f"差异预警检查完成: 新增 {n} 条")
    finally:
        db.close()
