"""看广告金币审计:复算 expected_coin 并与实发对比。

只读。复用 [app.core.rewards] 的公式函数(不另写公式,避免与正式发奖口径漂移):
- 看视频:每条 granted = 1 份,第 N 份 = 该用户 granted 的 reward_video **账号累计**顺序号
  (与 ad_reward.grant_ad_reward 里 `_granted_cumulative + 1` 一致;LT 因子不按天重置,
  故复算时要把当日序号叠加上该用户在本日**之前**的累计已发份数)。
- 信息流:**每条 granted = 1 份**(与 ad_feed_reward.grant_feed_reward 同口径:看满一份即发该条
  满额,**不按 unit_count 逐份累加**),LT 序号 = 该用户 granted **条数**账号累计
  (与 ad_feed_reward.granted_unit_total 的 COUNT 一致;不按天重置,复算需叠加本日之前的累计条数)。

非 granted(capped/ecpm_missing)不占用份序号、应发恒 0,据此校验闸口是否确实没发。
"""
from __future__ import annotations

from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session

from app.core import rewards
from app.models.ad_ecpm import AdEcpmRecord
from app.models.ad_feed_reward import AdFeedRewardRecord
from app.models.ad_reward import AdRewardRecord
from app.repositories.ad_feed_reward import FEED_REWARD_UNIT_SECONDS


def _prior_granted_counts(
    db: Session, *, date: str, user_id: int | None
) -> dict[int, int]:
    """各用户在 date **之前**已发奖的 reward_video 累计份数,作为当日复算的 LT 序号起点。
    LT 因子改账号累计后,当日第 1 份并非全局第 1 份,需叠加历史累计。"""
    stmt = (
        select(AdRewardRecord.user_id, func.count())
        .where(
            AdRewardRecord.reward_date < date,
            AdRewardRecord.reward_scene == "reward_video",
            AdRewardRecord.status == "granted",
        )
        .group_by(AdRewardRecord.user_id)
    )
    if user_id is not None:
        stmt = stmt.where(AdRewardRecord.user_id == user_id)
    return {uid: n for uid, n in db.execute(stmt).all()}


def _reward_video_rows(
    db: Session, *, date: str, user_id: int | None
) -> list[dict]:
    """看视频记录复算。按 (user_id, created_at) 升序还原账号累计第 N 份(含本日之前的累计)。"""
    stmt = (
        select(AdRewardRecord)
        .where(
            AdRewardRecord.reward_date == date,
            AdRewardRecord.reward_scene == "reward_video",
        )
        .order_by(AdRewardRecord.user_id, AdRewardRecord.created_at, AdRewardRecord.id)
    )
    if user_id is not None:
        stmt = stmt.where(AdRewardRecord.user_id == user_id)

    records = list(db.execute(stmt).scalars())
    # S2S 发奖回调不携带实际填充 ADN；用相同用户和 ad_session_id 的展示记录回填。
    session_ids = {record.ad_session_id for record in records if record.ad_session_id}
    impression_by_session = {
        (record.user_id, record.ad_session_id): record
        for record in db.execute(
            select(AdEcpmRecord).where(AdEcpmRecord.ad_session_id.in_(session_ids))
        ).scalars()
    } if session_ids else {}

    # 用本日之前的累计份数做起点,当日 granted 在其上继续递增 → 与 _granted_cumulative+1 对齐
    granted_n: dict[int, int] = _prior_granted_counts(db, date=date, user_id=user_id)
    rows: list[dict] = []
    for rec in records:
        impression = impression_by_session.get((rec.user_id, rec.ad_session_id))
        if rec.status == "granted":
            nth = granted_n.get(rec.user_id, 0) + 1
            granted_n[rec.user_id] = nth
            expected = rewards.calculate_ad_reward_coin(rec.ecpm_raw, nth)
            rows.append({
                "scene": "reward_video",
                "record_id": rec.id,
                "user_id": rec.user_id,
                "ad_session_id": rec.ad_session_id,
                "adn": impression.adn if impression is not None else None,
                "slot_id": impression.slot_id if impression is not None else None,
                "app_env": rec.app_env,
                "our_code_id": rec.our_code_id,
                "created_at": rec.created_at,
                "status": rec.status,
                "ecpm": rec.ecpm_raw,
                "ecpm_factor": rewards.ad_ecpm_factor(rewards.parse_ecpm_yuan(rec.ecpm_raw)),
                "units": 1,
                "lt_index_start": nth,
                "lt_index_end": nth,
                "lt_factor_start": rewards.ad_lt_factor(nth),
                "lt_factor_end": rewards.ad_lt_factor(nth),
                "expected_coin": expected,
                "actual_coin": rec.coin,
                "matched": expected == rec.coin,
            })
        else:
            # capped / ecpm_missing:不发金币,校验实发确为 0
            rows.append({
                "scene": "reward_video",
                "record_id": rec.id,
                "user_id": rec.user_id,
                "ad_session_id": rec.ad_session_id,
                "adn": impression.adn if impression is not None else None,
                "slot_id": impression.slot_id if impression is not None else None,
                "app_env": rec.app_env,
                "our_code_id": rec.our_code_id,
                "created_at": rec.created_at,
                "status": rec.status,
                "ecpm": rec.ecpm_raw,
                "ecpm_factor": None,
                "units": 1,
                "lt_index_start": None,
                "lt_index_end": None,
                "lt_factor_start": None,
                "lt_factor_end": None,
                "expected_coin": 0,
                "actual_coin": rec.coin,
                "matched": rec.coin == 0,
            })
    return rows


def _feed_prior_granted_count(
    db: Session, *, date: str, user_id: int | None
) -> dict[int, int]:
    """各用户在 date **之前** granted 的信息流**条数**累计,作为当日复算的 LT 序号起点。

    与发奖侧 ad_feed_reward.granted_unit_total(COUNT status=granted)对齐:一条广告 = 1 份,
    LT 按账号累计**条数**递进。**不再用 SUM(unit_count)**——那是「一条按时长折多份」的过时口径,
    与现行发奖(每条 1 份)漂移,会让 unit_count>1 的记录复算虚高、对账恒「不符」。"""
    stmt = (
        select(
            AdFeedRewardRecord.user_id,
            func.count(),
        )
        .where(
            AdFeedRewardRecord.reward_date < date,
            AdFeedRewardRecord.status == "granted",
        )
        .group_by(AdFeedRewardRecord.user_id)
    )
    if user_id is not None:
        stmt = stmt.where(AdFeedRewardRecord.user_id == user_id)
    return {uid: int(n) for uid, n in db.execute(stmt).all()}


def _feed_scene_matches(rec: AdFeedRewardRecord, scene: str | None) -> bool:
    """该信息流记录是否落入请求的展示筛选 scene。
    - scene=="feed":ad_type in ("feed", NULL)(旧数据 NULL 视为 feed,向后兼容)
    - scene=="draw":ad_type=="draw"
    - scene=="feed_all":所有信息流(feed/draw/NULL 都要)——业务已全切 Draw 信息流,收益报表把「Draw 信息流」
      当作整个信息流口径(含历史误标 feed/NULL),用它避免筛选漏历史。
    - scene 为 None:不筛(两类都要)。
    """
    if scene == "feed":
        return rec.ad_type in (None, "feed")
    if scene == "draw":
        return rec.ad_type == "draw"
    if scene == "feed_all":
        return True
    return True


def _nonblank(value: str | None) -> str | None:
    value = value.strip() if value else None
    return value or None


def _unique_ad_source(records: list[AdEcpmRecord]) -> tuple[str | None, str | None]:
    """仅在候选展示记录指向唯一 ADN 时回填来源，避免错误归因。"""
    adns = {_nonblank(record.adn) for record in records}
    adns.discard(None)
    if len(adns) != 1:
        return None, None

    slots = {_nonblank(record.slot_id) for record in records}
    slots.discard(None)
    return next(iter(adns)), next(iter(slots)) if len(slots) == 1 else None


def _feed_source_fallbacks(
    db: Session, records: list[AdFeedRewardRecord]
) -> tuple[
    dict[tuple[int, str], tuple[str | None, str | None]],
    dict[tuple[int, str, str], tuple[str | None, str | None]],
]:
    """为旧信息流发奖记录构建安全来源索引。"""
    session_ids = {record.ad_session_id for record in records if record.ad_session_id}
    trace_ids = {record.trace_id for record in records if record.trace_id}
    if not session_ids and not trace_ids:
        return {}, {}

    filters = []
    if session_ids:
        filters.append(AdEcpmRecord.ad_session_id.in_(session_ids))
    if trace_ids:
        filters.append(AdEcpmRecord.trace_id.in_(trace_ids))
    impressions = list(db.execute(select(AdEcpmRecord).where(or_(*filters))).scalars())

    by_session: dict[tuple[int, str], list[AdEcpmRecord]] = {}
    by_trace_ecpm: dict[tuple[int, str, str], list[AdEcpmRecord]] = {}
    for impression in impressions:
        if impression.ad_session_id:
            by_session.setdefault((impression.user_id, impression.ad_session_id), []).append(impression)
        if impression.trace_id:
            by_trace_ecpm.setdefault(
                (impression.user_id, impression.trace_id, impression.ecpm_raw), []
            ).append(impression)

    return (
        {key: _unique_ad_source(value) for key, value in by_session.items()},
        {key: _unique_ad_source(value) for key, value in by_trace_ecpm.items()},
    )


def _feed_source(
    record: AdFeedRewardRecord,
    *,
    by_session: dict[tuple[int, str], tuple[str | None, str | None]],
    by_trace_ecpm: dict[tuple[int, str, str], tuple[str | None, str | None]],
) -> tuple[str | None, str | None]:
    """返回本条发奖广告的来源；无唯一证据时保留原始空值。"""
    adn, slot_id = _nonblank(record.adn), _nonblank(record.slot_id)
    if adn and slot_id:
        return adn, slot_id

    candidate = by_session.get((record.user_id, record.ad_session_id or ""))
    if candidate is None and record.trace_id:
        candidate = by_trace_ecpm.get((record.user_id, record.trace_id, record.ecpm_raw))
    if candidate is None:
        return adn, slot_id
    candidate_adn, candidate_slot_id = candidate
    return adn or candidate_adn, slot_id or candidate_slot_id


def _feed_rows(
    db: Session, *, date: str, user_id: int | None, scene: str | None = None
) -> list[dict]:
    """信息流记录复算。**每条 granted = 1 份**(与发奖同口径,不按 unit_count 累加),
    LT 序号沿用账号累计**条数**(含本日之前)。

    **关键:LT 因子账号累计按全表 granted 条数累计(feed+draw 共享同一发奖池/上限),不按 ad_type 拆分**——
    故无论 scene 怎么筛展示,这里都遍历当日**全部**信息流记录维持 granted_count 累加;scene 只决定
    哪些行被**留下展示**(由 _feed_scene_matches 判断),不影响累计基线,保证复算序号与正式发奖一致。
    """
    stmt = (
        select(AdFeedRewardRecord)
        .where(AdFeedRewardRecord.reward_date == date)
        .order_by(AdFeedRewardRecord.user_id, AdFeedRewardRecord.created_at, AdFeedRewardRecord.id)
    )
    if user_id is not None:
        stmt = stmt.where(AdFeedRewardRecord.user_id == user_id)

    records = list(db.execute(stmt).scalars())
    by_session, by_trace_ecpm = _feed_source_fallbacks(db, records)

    # 本日之前的累计**条数**做起点,与发奖侧 granted_unit_total(COUNT granted)对齐
    granted_count: dict[int, int] = _feed_prior_granted_count(db, date=date, user_id=user_id)
    rows: list[dict] = []
    for rec in records:
        keep = _feed_scene_matches(rec, scene)  # 累计照常推进,这里只决定是否展示本行
        adn, slot_id = _feed_source(
            rec, by_session=by_session, by_trace_ecpm=by_trace_ecpm
        )
        if rec.status == "granted":
            # 一条广告 = 1 份(与 grant_feed_reward 同口径:看满一份即发该条满额,不按 unit_count 累加)。
            # nth = 账号累计第几**条**(含本日之前),与发奖侧 granted_unit_total+1 对齐;累计照常推进
            # (即便 scene 不匹配不展示也要 +1,保证序号与正式发奖一致)。
            nth = granted_count.get(rec.user_id, 0) + 1
            granted_count[rec.user_id] = nth
            if not keep:
                continue
            expected = rewards.calculate_ad_reward_coin(rec.ecpm_raw, nth)
            rows.append({
                "scene": "feed",
                "ad_type": rec.ad_type or "feed",
                "feed_scene": rec.feed_scene,
                "record_id": rec.id,
                "user_id": rec.user_id,
                "ad_session_id": rec.ad_session_id,
                "adn": adn,
                "slot_id": slot_id,
                "trace_id": rec.trace_id,
                "app_env": rec.app_env,
                "our_code_id": rec.our_code_id,
                "created_at": rec.created_at,
                "status": rec.status,
                "ecpm": rec.ecpm_raw,
                "ecpm_factor": rewards.ad_ecpm_factor(rewards.parse_ecpm_yuan(rec.ecpm_raw)),
                "units": 1,
                "lt_index_start": nth,
                "lt_index_end": nth,
                "lt_factor_start": rewards.ad_lt_factor(nth),
                "lt_factor_end": rewards.ad_lt_factor(nth),
                "expected_coin": expected,
                "actual_coin": rec.coin,
                "matched": expected == rec.coin,
            })
        else:
            if not keep:
                continue
            rows.append({
                "scene": "feed",
                "ad_type": rec.ad_type or "feed",
                "feed_scene": rec.feed_scene,
                "record_id": rec.id,
                "user_id": rec.user_id,
                "ad_session_id": rec.ad_session_id,
                "adn": adn,
                "slot_id": slot_id,
                "trace_id": rec.trace_id,
                "app_env": rec.app_env,
                "our_code_id": rec.our_code_id,
                "created_at": rec.created_at,
                "status": rec.status,
                "ecpm": rec.ecpm_raw,
                "ecpm_factor": None,
                "units": rec.unit_count,
                "lt_index_start": None,
                "lt_index_end": None,
                "lt_factor_start": None,
                "lt_factor_end": None,
                "expected_coin": 0,
                "actual_coin": rec.coin,
                "matched": rec.coin == 0,
            })
    return rows


def audit_rows(
    db: Session, *, date: str, user_id: int | None, scene: str | None = None
) -> list[dict]:
    """当日逐条发奖复算行(未排序)。scene: None=两类 / "reward_video" / "feed" / "draw"。

    "feed" 与 "draw" 都查 ad_feed_reward_record(同一发奖表),按 ad_type 区分:feed 含历史 NULL,
    draw 仅 ad_type=="draw"。信息流行额外带 `ad_type`/`feed_scene`,供收益报表区分比价/领券 Draw 收益。
    每行含 `app_env`/`our_code_id`/`expected_coin`/`actual_coin` 等,供金币审计逐条对账,
    也供广告收益报表把「应发/实发」按 用户×类型×应用×代码位 聚合(见 ad_revenue,复用同一复算口径)。
    **LT 因子账号累计仍按全表 unit 累计(feed+draw 共享),scene 只筛展示,不拆累计。**
    """
    rows: list[dict] = []
    if scene in (None, "reward_video"):
        rows.extend(_reward_video_rows(db, date=date, user_id=user_id))
    if scene in (None, "feed", "draw", "feed_all"):
        rows.extend(_feed_rows(db, date=date, user_id=user_id, scene=scene))
    return rows


def ad_coin_audit(
    db: Session,
    *,
    date: str,
    user_id: int | None,
    scene: str | None,
    limit: int,
    only_mismatch: bool = False,
) -> dict:
    """当日发奖复算。返回 {total, mismatch_count, truncated, items}。

    scene: None=两类都要 / "reward_video" / "feed";only_mismatch=True 只展示不一致(✗)行。
    关键:`total` 与 `mismatch_count` 在**全量**(截断前)上统计,故对账数字始终可信,不受 limit
    影响;`items` 才是展示集(only_mismatch 时只取 ✗ 行)按 created_at 倒序截断到 limit。
    份序号在全天数据上已算好,limit 只影响展示条数、不影响 expected 复算正确性。
    """
    rows = audit_rows(db, date=date, user_id=user_id, scene=scene)
    rows.sort(key=lambda r: (r["created_at"], r["record_id"]), reverse=True)

    total = len(rows)
    mismatch_count = sum(1 for r in rows if not r["matched"])
    display = [r for r in rows if not r["matched"]] if only_mismatch else rows
    return {
        "total": total,
        "mismatch_count": mismatch_count,
        "truncated": len(display) > limit,
        "items": display[:limit],
    }


def formula_snapshot() -> dict:
    """当前公式参数快照(给前端展示规则参照)。直接读 rewards 常量,与发奖同源。"""
    return {
        "coin_per_yuan": rewards.COIN_PER_YUAN,
        "feed_unit_seconds": FEED_REWARD_UNIT_SECONDS,
        "ecpm_factor_tiers": [list(t) for t in rewards.AD_ECPM_FACTOR_TABLE],
        "lt_factor_tiers": [list(t) for t in rewards.AD_LT_FACTOR_TABLE],
    }
