"""KPI实际值自动归集采集器 — 管理会计OS (P1-④ 2026-08-28)

按 kpi_value_sources 取数映射，从源头表(科目余额/进销存/现金流水)自动汇总写入 kpi_values。
- 源头: voucher_details(网银凭证明细) / product_inventory(库存汇总) / product_inventory_detail(库存明细) / cash_plans(收付款计划)
- 严格按 entity_id + period 过滤，避免跨账套/跨期串数
- 幂等: 同 kpi_id+period 已有 auto_collect 记录则更新；人工 excel/manual 写入不覆盖
- 调度: 系统 crontab 每日 06:30 (参考 auto_verify_cron.py 模式)

用法:
  /usr/bin/python3 scripts/kpi_value_collector.py            # 全量采集当月
  /usr/bin/python3 scripts/kpi_value_collector.py 2026-08    # 指定期间
  /usr/bin/python3 scripts/kpi_value_collector.py 2026-08 5  # 指定期间+KPI
"""
import sys
import logging
from datetime import datetime
from typing import Optional, Tuple

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("cma.kpi_collector")

# 保证从 backend 目录直接运行时能 import app
if __name__ == "__main__":
    import os
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from sqlalchemy import func
from app.database import get_session_local
from app.models import (
    KPIValueSource, KPIValueCollectLog, KPIValue, KPIDefinition,
    VoucherDetail, CashPlan,
)


# 源头表 → ORM模型映射（动态 import 避免循环依赖）
def _source_model(table: str):
    if table == "voucher_details":
        return VoucherDetail
    if table == "cash_plans":
        return CashPlan
    # product_inventory / product_inventory_detail 无ORM模型 → SQLAlchemy Table 反射
    from sqlalchemy import Table, MetaData
    from app.database import get_engine
    md = MetaData()
    return Table(table, md, autoload_with=get_engine())


def _field_expression(model, field: str, aggregate: str = "sum"):
    """聚合表达式: sum/avg/count/max/min"""
    col = getattr(model, field)
    if aggregate == "count":
        return func.count(col)
    if aggregate == "avg":
        return func.avg(col)
    if aggregate == "max":
        return func.max(col)
    if aggregate == "min":
        return func.min(col)
    return func.sum(col)


def collect_for_mapping(db, mapping: KPIValueSource, period: str, write_kpi: bool = True) -> Tuple[Optional[float], str]:
    """执行单条取数映射，返回 (采集值, 说明)。write_kpi=False 时为试跑模式（不写库）。"""
    table = mapping.source_table
    field = mapping.source_field
    aggregate = mapping.aggregate or "sum"
    filter_rule = mapping.filter_rule or {}
    period_field = mapping.period_field or "period"
    unit = mapping.unit_conversion or 1

    model = _source_model(table)

    # 构建查询
    col = getattr(model, field, None)
    if col is None:
        return None, f"字段 {field} 不存在于表 {table}"

    q = db.query(_field_expression(model, field, aggregate))

    # entity_id 过滤（所有源头表都有）
    q = q.filter(model.entity_id == mapping.entity_id)

    # 期间过滤
    if period_field == "voucher_date":
        # voucher_date 是 DATE 类型 → 按 %Y-%m 前缀匹配
        q = q.filter(func.date_format(model.voucher_date, "%Y-%m") == period)
    else:
        pf = getattr(model, period_field, None)
        if pf is None:
            return None, f"期间字段 {period_field} 不存在于表 {table}"
        q = q.filter(pf == period)

    # 过滤规则: subject_code / direction / plan_type / carry_forward
    subject_code = filter_rule.get("subject_code")
    if subject_code and hasattr(model, "subject_code"):
        q = q.filter(model.subject_code == subject_code)
    direction = filter_rule.get("direction")
    if direction:
        # direction 覆盖: credit→只算贷方, debit→只算借方
        if direction == "credit" and hasattr(model, "credit_amount"):
            q = db.query(_field_expression(model, "credit_amount", aggregate))
            q = q.filter(model.entity_id == mapping.entity_id)
            if period_field == "voucher_date":
                q = q.filter(func.date_format(model.voucher_date, "%Y-%m") == period)
            else:
                q = q.filter(getattr(model, period_field) == period)
            field = "credit_amount"
        elif direction == "debit" and hasattr(model, "debit_amount"):
            q = db.query(_field_expression(model, "debit_amount", aggregate))
            q = q.filter(model.entity_id == mapping.entity_id)
            if period_field == "voucher_date":
                q = q.filter(func.date_format(model.voucher_date, "%Y-%m") == period)
            else:
                q = q.filter(getattr(model, period_field) == period)
            field = "debit_amount"
    plan_type = filter_rule.get("plan_type")
    if plan_type and hasattr(model, "plan_type"):
        q = q.filter(model.plan_type == plan_type)
    if filter_rule.get("exclude_carry_forward") and hasattr(model, "carry_forward"):
        q = q.filter(model.carry_forward == 0)

    value = q.scalar()
    value = float(value or 0)
    value = round(value * unit, 2)

    message = f"表{table}.{field} {aggregate}(period={period}) × {unit}"
    if subject_code:
        message += f", 科目{subject_code}"
    if direction:
        message += f", 方向{direction}"
    if plan_type:
        message += f", 类型{plan_type}"

    if write_kpi:
        # upsert kpi_values: 同 kpi_id+period 已有 auto_collect 记录则更新
        existing = db.query(KPIValue).filter(
            KPIValue.kpi_id == mapping.kpi_id,
            KPIValue.period == period,
            KPIValue.source_type == "auto_collect",
        ).first()
        if existing:
            existing.actual_value = value
            existing.source_batch = _batch_no()
            existing.remark = f"自动归集: {table}"
            existing.data_status = "pending"
        else:
            db.add(KPIValue(
                entity_id=mapping.entity_id,
                kpi_id=mapping.kpi_id,
                period=period,
                actual_value=value,
                source_type="auto_collect",
                source_batch=_batch_no(),
                data_status="pending",
                remark=f"自动归集: {table}",
            ))

    return value, message


def _batch_no() -> str:
    return f"auto-{datetime.now().strftime('%Y%m%d%H%M%S')}"


def run_collector(db, entity_id: Optional[int] = None, period: Optional[str] = None, kpi_id: Optional[int] = None) -> dict:
    """运行采集器：遍历 active 映射 → 汇总 → upsert kpi_values → 写采集日志"""
    if period is None:
        period = datetime.now().strftime("%Y-%m")

    query = db.query(KPIValueSource).filter(KPIValueSource.status == "active")
    if entity_id is not None:
        query = query.filter(KPIValueSource.entity_id == entity_id)
    if kpi_id is not None:
        query = query.filter(KPIValueSource.kpi_id == kpi_id)
    mappings = query.all()

    if not mappings:
        return {"success": True, "collected": 0, "failed": 0, "message": "无激活取数映射"}

    collected, failed = 0, 0
    errors = []
    for m in mappings:
        try:
            value, message = collect_for_mapping(db, m, period, write_kpi=True)
            db.add(KPIValueCollectLog(
                entity_id=m.entity_id,
                kpi_id=m.kpi_id,
                period=period,
                source_table=m.source_table,
                collected_value=value,
                status="success",
                message=message,
            ))
            collected += 1
        except Exception as e:
            failed += 1
            errors.append({"kpi_id": m.kpi_id, "source_table": m.source_table, "error": str(e)})
            db.add(KPIValueCollectLog(
                entity_id=m.entity_id,
                kpi_id=m.kpi_id,
                period=period,
                source_table=m.source_table,
                collected_value=None,
                status="failed",
                message=str(e)[:500],
            ))
            logger.error("采集失败 kpi=%s table=%s: %s", m.kpi_id, m.source_table, e)

    db.commit()
    logger.info("采集完成: 成功%s 失败%s (period=%s)", collected, failed, period)
    return {
        "success": failed == 0,
        "collected": collected,
        "failed": failed,
        "period": period,
        "errors": errors[:20],
    }


if __name__ == "__main__":
    period_arg = sys.argv[1] if len(sys.argv) > 1 else None
    kpi_arg = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
    db = get_session_local()()
    try:
        r = run_collector(db, period=period_arg, kpi_id=kpi_arg)
        print(f"实际值自动归集完成: 成功{r['collected']} 失败{r['failed']} (period={r.get('period')})")
        for e in r.get("errors", []):
            print(f"  失败: kpi={e['kpi_id']} table={e['source_table']} -> {e['error']}")
    finally:
        db.close()
