"""admin 广告收益报表:**逐条广告事件**列表(每行一次广告,含展示 + 发奖对账)。

只读。每行 = 一次广告事件(不再按用户聚合):
- **激励视频**:一次观看 = 1 条展示(ad_ecpm)+ 1 条发奖(ad_reward),按 ad_session_id 合并成一行,
  直接给出 eCPM / 收益 + 状态 / 应发 / 实发 / 一致;点开看该条金币复算因子。
- **信息流(比价/领券)**:一次比价 / 一次领券 = 一条整场发奖(ad_feed_reward)一行,给出 eCPM /
  发奖金币 + 应发 / 实发 / 一致;点开看金币复算因子。⚠️ draw 的逐条展示(ad_ecpm,impressionId 各自
  独立、与整场发奖无公共键、无法归到「哪一次」)**不再单独占行**(2026-07 按「一次比价/领券放一块」调整)——
  其展示数 / eCPM / 预估收益仍进全量统计(合计 / 趋势 / 分类大盘 / 穿山甲对照),只是主表不逐条铺开。
- 兜底:激励视频有展示无发奖(中途关 / 未达发奖)、有发奖无展示(未上报 eCPM)仍各自成行。

展示与收益来自 ad_ecpm_record(收益 = eCPM元 ÷ 1000);应发 / 实发金币复用金币审计逐条复算
(ad_audit.audit_rows,与正式发奖同一公式口径,不另写公式)。合计与对账在全量上统计,
不受 limit(只截断 items)影响。

每行带 ad_type(reward_video/feed/draw)与 feed_scene(comparison/coupon/welfare),供前端区分
「比价 Draw 收益」与「领券 Draw 收益」(比价/领券共用同一代码位,只能靠 feed_scene 分)。

⚠️ 局限:① 历史信息流/Draw 发奖 ad_type 为 NULL 的旧记录统一视为 feed(向后兼容);Draw 仅
ad_type=="draw" 的新记录单独成类。② 跨天 S2S 回调:同一次广告的展示与发奖偶尔落相邻日,各自按
report_date / reward_date 归日。
"""
from __future__ import annotations

from datetime import UTC, datetime, timedelta
from datetime import date as _date

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.admin.repositories import ad_audit
from app.admin.repositories import stats as admin_stats
from app.core import rewards
from app.models.ad_ecpm import AdEcpmRecord
from app.models.user import User
from app.repositories import ad_pangle_revenue, app_config

# 已上线过的正式业务代码位要永久保留，避免运营切换当前配置后，历史报表把旧业务位误判成测试流量。
_KNOWN_PROD_BUSINESS_CODE_IDS = frozenset({"104098712", "104099389"})

# 测试应用中实际承载业务链路的代码位。广告测试 demo 的插屏/半屏/信息流测试位不在这里，
# 避免“业务口径”把开发诊断曝光混进客户端与穿山甲对账。
_TEST_BUSINESS_CODE_IDS = frozenset({"104127529", "104127626", "104137445"})


def business_code_ids(db: Session, app_env: str | None) -> set[str]:
    """返回指定应用环境下可用于业务收益对账的 GroMore 聚合代码位。"""
    prod_config = app_config.get_ad_config(db)
    prod_ids = set(_KNOWN_PROD_BUSINESS_CODE_IDS) | {
        str(prod_config.get(key) or "").strip()
        for key in ("reward_code_id", "compare_draw_code_id", "coupon_draw_code_id")
    }
    prod_ids.discard("")
    if app_env == "prod":
        return prod_ids
    if app_env == "test":
        return set(_TEST_BUSINESS_CODE_IDS)
    return prod_ids | set(_TEST_BUSINESS_CODE_IDS)


def _cn_hour(dt: datetime) -> int:
    """created_at(UTC 口径)→ 北京时间小时(0–23)。naive 当 UTC 处理(sqlite),tz-aware 直接换算(pg)。"""
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=UTC)
    return dt.astimezone(rewards.CN_TZ).hour


def _date_range(date_from: str, date_to: str) -> list[str]:
    """闭区间内逐日 'YYYY-MM-DD' 串(含首尾)。date_from > date_to 时返回空。"""
    d0 = _date.fromisoformat(date_from)
    d1 = _date.fromisoformat(date_to)
    out: list[str] = []
    d = d0
    while d <= d1:
        out.append(d.isoformat())
        d += timedelta(days=1)
    return out


# 报表 ad_type 与审计 scene 取值一致(reward_video / feed / draw):feed 与 draw 同查发奖表
# ad_feed_reward_record,由 audit 内部按 ad_type 区分(feed 含历史 NULL,draw 仅 ad_type=="draw")。
_AUDIT_SCENES = {"reward_video", "feed", "draw"}

# 激励视频未满足有效播放条件时不计客户端预估收益。客户端仍会在 onAdShow
# 上报 eCPM，随后才在关闭时补报以下终态，因此必须在展示/发奖合并后修正收益。
_ZERO_REVENUE_REWARD_VIDEO_STATUSES = frozenset({"closed_early", "too_short"})


# 发奖复算明细字段(展开下钻看「金币怎么算出来的」)——从 audit 行原样取这些 key。
_REWARD_DETAIL_KEYS = (
    "record_id", "created_at", "status", "ecpm", "ecpm_factor", "units",
    "lt_index_start", "lt_index_end", "lt_factor_start", "lt_factor_end",
    "expected_coin", "actual_coin", "matched",
)


def _reward_detail(row: dict) -> dict:
    """从 audit 行抽出发奖复算明细(给前端展开行渲染因子1/因子2/份数/LT/应发实发)。"""
    detail = {key: row[key] for key in _REWARD_DETAIL_KEYS}
    # 聚合父行可能包含多个 ADN，来源必须保留在每一条发奖明细上。
    detail["adn"] = row.get("adn")
    detail["slot_id"] = row.get("slot_id")
    return detail


def ad_revenue_report(
    db: Session,
    *,
    date_from: str,
    date_to: str,
    user_id: int | None = None,
    ad_type: str | None = None,
    feed_scene: str | None = None,
    app_env: str | None = None,
    revenue_scope: str = "all",
    granularity: str = "day",
    limit: int = 500,
    offset: int = 0,
    sort: str = "time",
) -> dict:
    """日期区间(北京时间,闭区间)**逐条广告事件**列表 + 发奖对账。单日时 date_from==date_to。

    每个 item = 一次广告事件(展示与发奖按 ad_session_id 合并;信息流展示 / 发奖各自成行)。
    ad_type: None=全部 / reward_video / feed / draw。feed_scene: None=全部 /
    comparison / coupon / welfare,作为全局筛选(同时作用于明细、合计与 daily/hourly 趋势)。
    granularity=hour 时每行带北京小时(由各自时间算),并额外返回全量 hourly 序列。
    事件按时间倒序(新→旧)排列;limit/offset 对排序后的全量做分页切片(items 为当前页),
    total 与 total_* / daily / hourly 在全量上统计,不受分页影响。
    """
    by_hour = granularity == "hour"

    # 1) 发奖行(逐日 audit 复算):建 (user_id, ad_session_id) → [行] 映射用于和展示合并;
    #    同时保留全量列表,未被展示合并的成「纯发奖」事件。
    reward_by_session: dict[tuple[int, str], list[dict]] = {}
    all_reward_rows: list[dict] = []
    # 报表 ad_type → audit scene:reward_video/feed 直传;**draw(前端「Draw 信息流」)映射成 feed_all**
    # ——业务已全切 Draw,把「Draw 信息流」当作整个信息流口径(含历史误标 feed/NULL),否则筛选会漏历史。
    if ad_type == "draw":
        audit_scene = "feed_all"
    elif ad_type in _AUDIT_SCENES:
        audit_scene = ad_type
    else:
        audit_scene = None
    if ad_type is None or audit_scene is not None:
        for d in _date_range(date_from, date_to):
            for row in ad_audit.audit_rows(db, date=d, user_id=user_id, scene=audit_scene):
                row["_report_date"] = d
                all_reward_rows.append(row)
                sid = row.get("ad_session_id")
                if sid:
                    reward_by_session.setdefault((row["user_id"], sid), []).append(row)

    used_reward_ids: set[int] = set()
    events: list[dict] = []

    def _pop_reward(uid: int, sid: str | None) -> dict | None:
        """取一条与 (uid, sid) 匹配且未被用过的发奖行(激励视频展示↔发奖按会话 1:1 合并)。"""
        if not sid:
            return None
        for r in reward_by_session.get((uid, sid), ()):
            if r["record_id"] not in used_reward_ids:
                used_reward_ids.add(r["record_id"])
                return r
        return None

    # 2) 展示记录(ad_ecpm):每条一个事件;能匹配到发奖则合并成「展示 + 发奖」一行。
    stmt = select(AdEcpmRecord).where(
        AdEcpmRecord.report_date >= date_from,
        AdEcpmRecord.report_date <= date_to,
    )
    if user_id is not None:
        stmt = stmt.where(AdEcpmRecord.user_id == user_id)
    if ad_type == "draw":
        # draw = 所有信息流展示(业务已全 Draw,含历史误标 feed);展示行只进统计,不占主表行
        stmt = stmt.where(AdEcpmRecord.ad_type.in_(["draw", "feed"]))
    elif ad_type is not None:
        stmt = stmt.where(AdEcpmRecord.ad_type == ad_type)
    for rec in db.execute(stmt).scalars():
        rwd = _pop_reward(rec.user_id, rec.ad_session_id)
        ev = {
            "event_key": f"imp-{rec.id}",
            "report_date": rec.report_date,
            "user_id": rec.user_id,
            "ad_type": rec.ad_type,
            "feed_scene": rec.feed_scene,
            "app_env": rec.app_env,
            "our_code_id": rec.our_code_id,
            "created_at": rec.created_at,
            "hour": _cn_hour(rec.created_at) if by_hour else None,
            "has_impression": True,
            "impressions": 1,
            "ecpm": rec.ecpm_raw,
            # 单次展示收益(元)= eCPM元 ÷ 1000(每千次→单次)。eCPM 先钳到 AD_ECPM_MAX_FEN(¥500 CPM)
            # 再折收益,与发奖口径 [rewards.calculate_ad_reward_coin] 一致(2026-06-29 修:原裸 parse_ecpm_yuan
            # 不钳,伪造/异常天价 eCPM 会把报表预估收益冲到任意大;金币侧已钳、收益侧漏钳)。
            "revenue_yuan": round(
                min(rewards.parse_ecpm_yuan(rec.ecpm_raw), rewards.AD_ECPM_MAX_FEN / 100.0) / 1000.0, 6,
            ),
            "adn": rec.adn,
            "slot_id": rec.slot_id,
            "sub_rewards": [],
            "sub_count": 1,
        }
        if rwd is not None:
            ev.update({
                "has_reward": True,
                "status": rwd["status"],
                "expected_coin": int(rwd["expected_coin"]),
                "actual_coin": int(rwd["actual_coin"]),
                "matched": bool(rwd["matched"]),
                "reward_detail": _reward_detail(rwd),
            })
            if (
                rec.ad_type == "reward_video"
                and rwd["status"] in _ZERO_REVENUE_REWARD_VIDEO_STATUSES
            ):
                ev["revenue_yuan"] = 0.0
        else:
            # 纯展示(信息流逐条展示、激励视频缺发奖记录):不计对账,matched=True。
            ev.update({
                "has_reward": False, "status": None,
                "expected_coin": 0, "actual_coin": 0, "matched": True,
                "reward_detail": None,
            })
        events.append(ev)

    # 3) 未被展示合并的发奖行 → 事件:
    #    - 激励视频(reward_video):逐条成「纯发奖」事件(每次一个 ad_session_id;有发奖无展示等)。
    #    - 信息流(feed/draw):同一次比价/领券的多条广告共享**整场 ad_session_id**(客户端整场复用),
    #      按 (user_id, ad_session_id) 聚成**一次比价 / 一次领券**父事件;sub_rewards 为组内逐条明细,
    #      应发/实发取组内合计;业务已全 Draw → 类型统一 "draw"。session 缺失(极少旧数据)各自单独成组。
    feed_groups: dict[tuple[int, str], list[dict]] = {}
    for row in all_reward_rows:
        if row["record_id"] in used_reward_ids:
            continue
        if row["scene"] == "reward_video":
            events.append({
                "event_key": f"rwd-{row['record_id']}",
                "report_date": row["_report_date"],
                "user_id": row["user_id"],
                "ad_type": "reward_video",
                "feed_scene": row.get("feed_scene"),
                "app_env": row.get("app_env"),
                "our_code_id": row.get("our_code_id"),
                "created_at": row["created_at"],
                "hour": _cn_hour(row["created_at"]) if by_hour else None,
                "has_impression": False,
                "impressions": 0,
                "ecpm": row["ecpm"],
                "revenue_yuan": 0.0,
                "adn": row.get("adn"),
                "slot_id": row.get("slot_id"),
                "has_reward": True,
                "status": row["status"],
                "expected_coin": int(row["expected_coin"]),
                "actual_coin": int(row["actual_coin"]),
                "matched": bool(row["matched"]),
                "reward_detail": _reward_detail(row),
                "sub_rewards": [],
                "sub_count": 1,
            })
        else:
            # 聚合单位 = 一次完整比价/领券流程:优先用 trace_id(比价带 comparisonTraceId、领券带 sessionTraceId,
            # 整个流程不变;即使中途点广告致浮层关闭重弹、ad_session_id 变了,trace_id 仍不变 → 全流程聚成一行)。
            # 无 trace_id(历史领券未上报 / 旧数据)回退整场 ad_session_id;再无则 record_id 各自成组、不误并。
            grp_key = row.get("trace_id") or row.get("ad_session_id") or f"_rid-{row['record_id']}"
            feed_groups.setdefault((row["user_id"], grp_key), []).append(row)

    # 信息流分组 → 「一次比价 / 一次领券」父事件(收益恒 0:收益只算展示侧,避免与展示行重复计)。
    for (uid, grp_key), group in feed_groups.items():
        group.sort(key=lambda r: (r["created_at"], r["record_id"]))
        rep = group[-1]  # 代表条(最新一条):时间/场景/应用/代码位取它
        expected_sum = sum(int(g["expected_coin"]) for g in group)
        actual_sum = sum(int(g["actual_coin"]) for g in group)
        # 父行 eCPM:组内各条 eCPM(分)均值(展示用,各条不同);无有效值则取代表条
        ecpm_fens = [rewards.parse_ecpm_fen(g["ecpm"]) for g in group if g.get("ecpm")]
        avg_ecpm = str(round(sum(ecpm_fens) / len(ecpm_fens))) if ecpm_fens else rep.get("ecpm")
        # 主表逐行显示用:这次发奖广告的预估收益之和(发奖侧 eCPM 折算,钳顶同展示侧)。只放进
        # row_revenue_yuan 给主表逐行展示,不进 revenue_yuan/合计/趋势——避免与展示侧 total 重复计。
        row_revenue = round(sum(
            min(rewards.parse_ecpm_yuan(g["ecpm"]), rewards.AD_ECPM_MAX_FEN / 100.0) / 1000.0
            for g in group if g.get("ecpm")
        ), 6)
        events.append({
            "event_key": f"feedgrp-{uid}-{grp_key}",
            "report_date": rep["_report_date"],
            "user_id": uid,
            "ad_type": "draw",  # 业务已全切 Draw 信息流,聚合行统一 draw
            "feed_scene": rep.get("feed_scene"),
            "app_env": rep.get("app_env"),
            "our_code_id": rep.get("our_code_id"),
            "created_at": rep["created_at"],
            "hour": _cn_hour(rep["created_at"]) if by_hour else None,
            "has_impression": False,
            "impressions": 0,
            "ecpm": avg_ecpm,
            "revenue_yuan": 0.0,
            "row_revenue_yuan": row_revenue,
            "adn": None,
            "slot_id": None,
            "has_reward": True,
            "status": rep["status"],  # 代表状态(逐条见展开)
            "expected_coin": expected_sum,
            "actual_coin": actual_sum,
            "matched": all(bool(g["matched"]) for g in group),
            "reward_detail": None,
            "sub_rewards": [_reward_detail(g) for g in group],
            "sub_count": len(group),
        })

    # 「场景」作为全局筛选(与 user_id/ad_type 一致):同时作用于明细、合计与 daily/hourly 趋势。
    # feed_scene 仅信息流 / Draw 有值,激励视频与旧数据为 None;选中后只保留该场景事件。
    if feed_scene is not None:
        events = [e for e in events if e.get("feed_scene") == feed_scene]

    # app_env 过滤:显式传 "prod"/"test" 只看该环境;不传=全部。该参数也会传给下方穿山甲聚合，
    # 保证客户端预估与 GroMore 汇总使用同一应用环境口径。
    if app_env is not None:
        events = [e for e in events if e.get("app_env") == app_env]

    # 业务口径仅保留正式配置/测试业务链路实际使用的代码位。穿山甲“全量”还包含广告测试
    # demo、插屏等没有客户端收益上报的曝光，两边直接比较会天然产生假差额。
    business_ids: set[str] | None = None
    if revenue_scope == "business":
        business_ids = business_code_ids(db, app_env)
        events = [e for e in events if e.get("our_code_id") in business_ids]

    # 排序:time=按时间倒序(新→旧);ecpm=按 eCPM 数值倒序(eCPM 原值是字符串「分」,转数值排;
    # 纯发奖行用其发奖采用的 eCPM,缺失/非法计 0 排末尾)。
    if sort == "ecpm":
        events.sort(key=lambda e: rewards.parse_ecpm_fen(e["ecpm"]), reverse=True)
    else:
        events.sort(key=lambda e: (e["report_date"], e["created_at"]), reverse=True)

    # 补手机号(admin 展示用,完整不脱敏,与用户 / 钱包 / 比价记录页一致):批量一次查,避免 N+1。
    uids = {e["user_id"] for e in events}
    phone_map: dict[int, str] = {}
    if uids:
        phone_map = {
            uid: phone
            for uid, phone in db.execute(
                select(User.id, User.phone).where(User.id.in_(uids))
            ).all()
        }
    for e in events:
        e["user_phone"] = phone_map.get(e["user_id"])

    total_impressions = sum(e["impressions"] for e in events)
    total_revenue_yuan = round(sum(e["revenue_yuan"] for e in events), 6)
    total_expected_coin = sum(e["expected_coin"] for e in events)
    total_actual_coin = sum(e["actual_coin"] for e in events)
    mismatch_count = sum(1 for e in events if e["has_reward"] and not e["matched"])

    # 按日期汇总(全量,不受 limit):供前端按天趋势图。
    daily_map: dict[str, dict] = {}
    for e in events:
        d = daily_map.get(e["report_date"])
        if d is None:
            d = {"date": e["report_date"], "impressions": 0, "revenue_yuan": 0.0,
                 "expected_coin": 0, "actual_coin": 0}
            daily_map[e["report_date"]] = d
        d["impressions"] += e["impressions"]
        d["revenue_yuan"] += e["revenue_yuan"]
        d["expected_coin"] += e["expected_coin"]
        d["actual_coin"] += e["actual_coin"]
    daily = [
        {**d, "revenue_yuan": round(d["revenue_yuan"], 6)}
        for d in sorted(daily_map.values(), key=lambda x: x["date"])
    ]

    # 穿山甲后台收益(GroMore 数据 API,T+1 入库 ad_pangle_daily_revenue):汇总 + 按天趋势级展示,
    # 与上面客户端自报 eCPM 折算的预估并列对照(看 gap)。穿山甲数据**无用户/场景/类型维度**,故仅在
    # 「全量视图」(未按 user_id / ad_type / feed_scene 过滤)给值;一旦带这些过滤,穿山甲数无法对应口径
    # → 置 None,前端显示「-」并提示。逐条事件行不动(仍是客户端预估)。
    pangle_filterable = user_id is None and ad_type is None and feed_scene is None
    total_pangle_revenue_yuan: float | None = None
    total_pangle_api_revenue_yuan: float | None = None
    if pangle_filterable:
        pangle_aggs = ad_pangle_revenue.aggregate_by_date(
            db,
            date_from=date_from,
            date_to=date_to,
            app_env=app_env,
            our_code_ids=business_ids,
        )
        if pangle_aggs:
            by_date = {a["date"]: a for a in pangle_aggs}
            for d in daily:
                pa = by_date.get(d["date"])
                d["pangle_revenue_yuan"] = pa["revenue_yuan"] if pa else None
                d["pangle_api_revenue_yuan"] = pa["api_revenue_yuan"] if pa else None
            total_pangle_revenue_yuan = round(sum(a["revenue_yuan"] for a in pangle_aggs), 6)
            api_vals = [a["api_revenue_yuan"] for a in pangle_aggs if a["api_revenue_yuan"] is not None]
            total_pangle_api_revenue_yuan = round(sum(api_vals), 6) if api_vals else None

    # 按小时汇总(全量,不受分页 limit/offset 影响):供前端按小时趋势图(单日 granularity=hour 时用)。
    # 只在 by_hour 下聚合(此时每个 event 带 hour);否则空。前端按天趋势仍用 daily。
    hourly: list[dict] = []
    if by_hour:
        hour_map: dict[int, dict] = {}
        for e in events:
            h = e["hour"]
            if h is None:
                continue
            hd = hour_map.get(h)
            if hd is None:
                hd = {"hour": h, "impressions": 0, "revenue_yuan": 0.0,
                      "expected_coin": 0, "actual_coin": 0}
                hour_map[h] = hd
            hd["impressions"] += e["impressions"]
            hd["revenue_yuan"] += e["revenue_yuan"]
            hd["expected_coin"] += e["expected_coin"]
            hd["actual_coin"] += e["actual_coin"]
        hourly = [
            {**hd, "revenue_yuan": round(hd["revenue_yuan"], 6)}
            for hd in sorted(hour_map.values(), key=lambda x: x["hour"])
        ]

    # 分广告类型小计(按 ad_type:展示条数 + 预估收益;eCPM 由前端用 收益÷展示×1000 算)。
    # 基于全量(已按 feed_scene 过滤)events;前端只取 draw / reward_video 两类展示。
    type_map: dict[str, dict] = {}
    for e in events:
        t = type_map.get(e["ad_type"])
        if t is None:
            t = {"impressions": 0, "revenue_yuan": 0.0}
            type_map[e["ad_type"]] = t
        t["impressions"] += e["impressions"]
        t["revenue_yuan"] += e["revenue_yuan"]
    type_stats = {
        k: {"impressions": v["impressions"], "revenue_yuan": round(v["revenue_yuan"], 6)}
        for k, v in type_map.items()
    }

    # 经营看板的两类 eCPM 必须按真实展示的 SDK eCPM 加权，不能用发奖状态修正后的
    # 收益反推。Draw 包含新 draw 与历史 feed；看视频包含福利与提现视频。
    category_map: dict[str, dict] = {}
    for event in events:
        category = (
            "draw" if event["ad_type"] in {"draw", "feed"}
            else "video" if event["ad_type"] in {"reward_video", "withdrawal_video"}
            else None
        )
        if category is None:
            continue
        stat = category_map.setdefault(category, {
            "impressions": 0,
            "revenue_yuan": 0.0,
            "ecpm_fen_sum": 0.0,
        })
        impressions = int(event["impressions"])
        stat["impressions"] += impressions
        stat["revenue_yuan"] += event["revenue_yuan"]
        stat["ecpm_fen_sum"] += rewards.parse_ecpm_fen(event["ecpm"]) * impressions
    category_stats = {
        key: {
            "impressions": value["impressions"],
            "revenue_yuan": round(value["revenue_yuan"], 6),
            "ecpm_yuan": round(
                value["ecpm_fen_sum"] / value["impressions"] / 100.0,
                6,
            ) if value["impressions"] else 0.0,
        }
        for key, value in category_map.items()
    }

    # 分场景小计(按 feed_scene:展示条数 + 预估收益),同 type_stats 基于全量 events——
    # 供数据大盘「领券广告 / 比价广告」卡用。此前大盘是在分页 items 里按 feed_scene 现算,
    # 2026-07-02 起信息流逐条展示行(唯一带收益 + 场景的行)不再进主表 items,现算恒为 0;
    # 改为服务端在全量上聚合下发(也顺带不受 limit 分页截断影响)。feed_scene 为空(激励视频 /
    # 旧数据)不计入任何场景桶。
    scene_map: dict[str, dict] = {}
    for e in events:
        sc = e.get("feed_scene")
        if not sc:
            continue
        s = scene_map.get(sc)
        if s is None:
            s = {"impressions": 0, "revenue_yuan": 0.0}
            scene_map[sc] = s
        s["impressions"] += e["impressions"]
        s["revenue_yuan"] += e["revenue_yuan"]
    scene_stats = {
        k: {"impressions": v["impressions"], "revenue_yuan": round(v["revenue_yuan"], 6)}
        for k, v in scene_map.items()
    }

    # DAU:复用数据大盘活跃用户口径(登录 + 开始比价 + 开始领券,按用户去重),按所选日期区间
    # 统计(含今日),历史 / 多天区间同样有值。ARPU = 区间预估收益 ÷ 区间活跃用户。全局口径,
    # 不随 user / ad_type / feed_scene / app_env 筛选变化(活跃用户口径无这些维度)。
    dau = admin_stats.period_active_dau(
        db, _date.fromisoformat(date_from), _date.fromisoformat(date_to)
    )

    # 主表「逐行」= 单次广告行为(2026-07 按「一次比价/领券放一块」聚合):激励视频 = 一次观看一行(展示+发奖
    # 按 ad_session_id 合并);一次比价 / 一次领券 = 该次整场多条广告按 ad_session_id 聚成一行(展开看逐条)。
    # 信息流(draw/feed)的逐条展示(ad_ecpm,impressionId 各自独立、与整场发奖无公共键)不再单独占行
    # ——其展示数 / eCPM / 预估收益已计入上面的全量统计(total_*、daily / hourly、type_stats、穿山甲对照),
    # 只是主表不逐条铺开;逐条明细在父行展开里看(sub_rewards)。合计 / 趋势 / 分类大盘均基于全量 events,
    # 不受此过滤影响;total / 分页只作用于主表行。
    main_rows = [
        e for e in events
        if not (e["ad_type"] in ("draw", "feed") and e["has_impression"] and not e["has_reward"])
    ]

    return {
        "total": len(main_rows),
        "truncated": len(main_rows) > offset + limit,
        "total_impressions": total_impressions,
        "total_revenue_yuan": total_revenue_yuan,
        # 穿山甲后台收益合计(元):预估 revenue + 收益Api;非全量视图(带 user/类型/场景过滤)或无数据为 None。
        "total_pangle_revenue_yuan": total_pangle_revenue_yuan,
        "total_pangle_api_revenue_yuan": total_pangle_api_revenue_yuan,
        "pangle_revenue_available": total_pangle_revenue_yuan is not None,
        "total_expected_coin": total_expected_coin,
        "total_actual_coin": total_actual_coin,
        "mismatch_count": mismatch_count,
        "daily": daily,
        "hourly": hourly,
        "type_stats": type_stats,
        "category_stats": category_stats,
        "scene_stats": scene_stats,
        "dau": dau,
        "items": main_rows[offset:offset + limit],
    }
