"""机会检测器 — 路线图R2 数据找人扩大 (2026-08-30)

北极星③：主动推送扩大 —— 异常 + 机会两类。
本脚本检测三类机会（复用 budget/kpi 数据，不新建表）：
  1. KPI向好 (kpi_improving)   : 最近3期执行率>110% 且最新期呈上升趋势
  2. 预算余量 (budget_headroom): 可用预算>30%（预算执行率<70%）
  3. 滚动机会 (rolling_up)     : 预测值上升（kpi_forecast_log 最新>上期）

输出：机会列表 [{type, title, detail, kpi_id, kpi_name, period}]
"""
import sys
import os
import json
from datetime import datetime

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from app.database import get_session_local
from app.models import KPIDefinition, KPIValue, BudgetPlan, KpiForecastLog

HIGH_RATIO = 1.1      # 执行率>110% = 超预期
LOW_EXEC_RATIO = 0.7  # 执行率<70% = 预算余量大（可用>30%）


def _exec_ratio(actual, target):
    if target is None or target == 0:
        return None
    return actual / target


def detect_kpi_improving(db, entity_id: int, min_ratio: float = HIGH_RATIO) -> list:
    """KPI向好：最近3期执行率均>110%，且最新期>上期（上升中）"""
    out = []
    kpis = db.query(KPIDefinition).filter(
        KPIDefinition.entity_id == entity_id,
        KPIDefinition.status == "active",
    ).all()
    now = datetime.now()
    for k in kpis:
        if not k.target_value or k.target_value <= 0:
            continue
        vals = db.query(KPIValue).filter(
            KPIValue.kpi_id == k.id,
            KPIValue.actual_value.isnot(None),
        ).order_by(KPIValue.period.desc()).limit(3).all()
        if len(vals) < 3:
            continue
        ratios = [_exec_ratio(v.actual_value, k.target_value) for v in vals]
        if any(r is None or r < min_ratio for r in ratios):
            continue
        # 最新期 > 上期（上升趋势）；若最新期低于上期但整体仍>110%，也算（持续向好）
        latest, prev = vals[0], vals[1]
        trend = "上升" if latest.actual_value > prev.actual_value else "高位"
        out.append({
            "type": "kpi_improving",
            "title": f"📈 {k.kpi_name} 持续向好",
            "detail": (f"{latest.period}实际{latest.actual_value:g}/目标{k.target_value:g}"
                       f" 达成率{ratios[0]*100:.0f}%({trend})，近3期均超110%"),
            "kpi_id": k.id,
            "kpi_name": k.kpi_name,
            "period": latest.period,
        })
    return out


def detect_budget_headroom(db, entity_id: int, max_ratio: float = LOW_EXEC_RATIO) -> list:
    """预算余量：当月预算执行率<70%（可用预算>30%）

    注意：跳过实际值为负的行（现金流/利润为负是异常不是余量），
    同 KPI 同期间多版本预算只取一条（去重）。
    """
    out = []
    period = datetime.now().strftime("%Y-%m")
    rows = db.query(BudgetPlan).filter(
        BudgetPlan.entity_id == entity_id,
        BudgetPlan.status == "active",
        BudgetPlan.period == period,
        BudgetPlan.budget_value > 0,
    ).all()
    seen = set()
    for b in rows:
        key = (b.kpi_id, b.period)
        if key in seen:
            continue
        seen.add(key)
        actual = db.query(KPIValue).filter(
            KPIValue.kpi_id == b.kpi_id,
            KPIValue.period == b.period,
            KPIValue.actual_value.isnot(None),
        ).order_by(KPIValue.calculated_at.desc()).first()
        if not actual or actual.actual_value is None or actual.actual_value <= 0:
            continue
        ratio = actual.actual_value / b.budget_value
        if ratio < max_ratio:
            k = db.query(KPIDefinition).filter(KPIDefinition.id == b.kpi_id).first()
            kpi_name = k.kpi_name if k else f"KPI#{b.kpi_id}"
            headroom = (1 - ratio) * 100
            out.append({
                "type": "budget_headroom",
                "title": f"💼 {kpi_name} 预算余量充足",
                "detail": (f"{period}预算{b.budget_value:g}/实际{actual.actual_value:g}"
                           f" 执行率{ratio*100:.0f}%，可用预算余量约{headroom:.0f}%"),
                "kpi_id": b.kpi_id,
                "kpi_name": kpi_name,
                "period": period,
            })
    return out


def detect_rolling_up(db, entity_id: int) -> list:
    """滚动机会：预测值上升（最新预测 > 上期预测）"""
    out = []
    # 每个KPI取最近两条预测记录
    kpi_ids = [r[0] for r in db.query(KpiForecastLog.kpi_id).filter(
        KpiForecastLog.entity_id == entity_id).distinct().limit(50).all()]
    for kid in kpi_ids:
        rows = db.query(KpiForecastLog).filter(
            KpiForecastLog.entity_id == entity_id,
            KpiForecastLog.kpi_id == kid,
            KpiForecastLog.forecast_value.isnot(None),
        ).order_by(KpiForecastLog.created_at.desc(), KpiForecastLog.id.desc()).limit(2).all()
        if len(rows) < 2:
            continue
        latest, prev = rows[0], rows[1]
        if latest.forecast_value > prev.forecast_value:
            k = db.query(KPIDefinition).filter(KPIDefinition.id == kid).first()
            kpi_name = k.kpi_name if k else f"KPI#{kid}"
            pct = (latest.forecast_value / prev.forecast_value - 1) * 100 if prev.forecast_value else 0
            out.append({
                "type": "rolling_up",
                "title": f"🔮 {kpi_name} 预测上行",
                "detail": (f"预测值 {prev.forecast_value:g} → {latest.forecast_value:g}"
                           f" (+{pct:.1f}%)，{latest.period}期间"),
                "kpi_id": kid,
                "kpi_name": kpi_name,
                "period": latest.period,
            })
    return out


def detect_all(db, entity_id: int = 1) -> dict:
    """检测全部机会，按类型分组"""
    return {
        "kpi_improving": detect_kpi_improving(db, entity_id),
        "budget_headroom": detect_budget_headroom(db, entity_id),
        "rolling_up": detect_rolling_up(db, entity_id),
    }


def flatten(detected: dict) -> list:
    out = []
    for cat in ("kpi_improving", "budget_headroom", "rolling_up"):
        out.extend(detected.get(cat, []))
    return out


def main():
    db = get_session_local()()
    try:
        detected = detect_all(db)
        total = sum(len(v) for v in detected.values())
        print(json.dumps(detected, ensure_ascii=False, indent=2))
        print(f"\n机会总数: {total}")
    finally:
        db.close()


if __name__ == "__main__":
    main()
