"""看激励视频发奖相关 schemas。

约定同其余模块:字段 snake_case、金额/次数存整数。
"""
from __future__ import annotations

from datetime import datetime

from pydantic import BaseModel, Field


class PangleCallbackOut(BaseModel):
    """回给穿山甲 GroMore 的回调响应。

    GroMore 服务端激励回调规范(supportcenter/26240)要求响应体为
    `{"is_verify": bool, "reason": int}`:
    - `is_verify=true` + `reason=0`:校验通过、发放奖励(发奖成功 或 当日已达上限均算"已处理")。
    - `is_verify=false`:不发放,`reason` 带错误码透传客户端 SDK(见 ad.py 的 REASON_*)。
    验签失败由 API 层直接返 403,不走这里。"""

    is_verify: bool = True
    reason: int = 0


class AdRewardStatusOut(BaseModel):
    """客户端查今日看广告发奖进度(福利页"看视频赚金币"用)。"""

    used_today: int = Field(..., description="今日已成功发奖次数")
    daily_limit: int = Field(..., description="每日发奖次数上限")
    remaining: int = Field(..., description="今日剩余可领次数")
    coin_per_ad: int = Field(..., description="历史兼容字段;正式发放按 eCPM 动态计算,当前返回 0")
    round_count: int = Field(
        ..., description="本轮已看次数,当前 round_size=1,客户端可由 cooldown_until 判断短冷却"
    )
    cooldown_until: datetime | None = Field(
        None,
        description="广告关闭后 3 秒短冷却结束时间(UTC ISO),null 表示不在冷却。"
        "冷却是 UX 约束,后端发奖不受影响,客户端 CTA 据此显示 MM:SS 倒计时且不可点",
    )
    # ===== 旧版每日观看总时长字段:当前产品仅保留每日 500 次上限,limit=0 表示未启用时长闸 =====
    watched_seconds_today: int = Field(0, description="今日已观看激励视频总秒数(前端累计上报)")
    watch_seconds_limit: int = Field(0, description="每日观看总时长上限(秒);0 表示当前未启用")
    watch_seconds_remaining: int = Field(0, description="今日剩余可观看秒数;limit=0 时客户端不据此拦截")


class AdRewardResultOut(BaseModel):
    """按 ad_session_id 查本次广告的**权威发奖结果**(福利页看完视频的弹窗金额只认它)。

    S2S 回调是异步的,客户端看完广告立刻轮询多半还查不到记录 —— 这种「还没到账」返回
    200 + status='pending' 让客户端重试,**不返 404**:404 只应表示路由不存在,两者混在
    一起客户端没法区分「后端没部署」和「再等等」。纯只读,不产生任何奖励。
    """

    ad_session_id: str = Field(..., description="回显请求的广告会话 id")
    status: str = Field(
        ...,
        description="pending(S2S 未到账,客户端应继续轮询) / granted(已发奖) / capped(当日超限未发) / "
        "ecpm_missing(缺 eCPM 未发) / closed_early(提前关闭未发);其余同 AdRewardRecord.status。"
        "客户端只在 granted 且 coin>0 时弹窗,其它一律不弹(不显示假数字)",
    )
    coin: int | None = Field(
        None,
        description="本次实发金币:granted 为真实到账额;未发奖的状态为 0;pending 为 null",
    )
    round_coin: int | None = Field(
        None,
        description="**本轮膨胀累计已发金币**(含本条)——客户端「恭喜累计获得奖励」弹窗显示的就是它。"
        "轮 = 用户点「去膨胀」到点「放弃赚钱」之间连看的若干条广告,边界由客户端的 boost_round_id 定。"
        "本条不是 granted(capped/closed_early/…)时**仍返本轮累计**,只是这条按 0 计。"
        "pending(没记录,取不到轮 id)、或该记录没有 boost_round_id(老客户端 / extra 丢失)时为 null,"
        "客户端见 null 退回只显示单条 coin",
    )


class EcpmReportIn(BaseModel):
    """客户端上报一次广告展示的 eCPM(内部收益统计/对账)。

    user_id 不在 body 里——由 JWT 取(Bearer),防伪造。ecpm 原样上报字符串,后端按分/千次展示处理(SDK getEcpm 原值,非元)。
    """

    ad_type: str = Field(..., description="广告类型:reward_video(激励视频) / draw(Draw 信息流) 等")
    ecpm: str = Field(..., description="穿山甲 getShowEcpm().getEcpm() 原始字符串,按分/千次展示处理(SDK getEcpm 原值,非元)")
    ad_session_id: str | None = Field(
        None, min_length=8, max_length=64,
        description="客户端生成的一次广告会话 id;激励视频 S2S extra 会透传同值",
    )
    adn: str | None = Field(None, description="实际投放 ADN(getSdkName),如 pangle")
    slot_id: str | None = Field(None, description="实际展示代码位(底层 mediation rit)")
    feed_scene: str | None = Field(
        None,
        max_length=16,
        description="点位场景:comparison(比价等待) / coupon(领券) / welfare(福利页);"
        "比价与领券共用同一 Draw 代码位,需客户端在各调用点显式标注,供收益报表区分比价/领券;激励视频为空",
    )
    trace_id: str | None = Field(
        None,
        max_length=64,
        description="本次比价/领券 trace_id(信息流场景带上):把这条展示收益归属到对应比价/领券,"
        "供领券数据/比价记录看板聚合本场广告收益;激励视频/福利为空",
    )
    exposure_ms: int | None = Field(
        None,
        ge=0,
        le=86_400_000,
        description="本条广告真实在屏曝光毫秒数；小于 1000ms 时收益强制按 0 计算。旧客户端不传则保持原口径",
    )
    app_env: str | None = Field(
        None, max_length=16, description="我们的穿山甲应用环境:prod(傻瓜比价正式) / test(测试应用)"
    )
    our_code_id: str | None = Field(
        None, max_length=64, description="我们后台配置的代码位 ID(AdConfig 的 104xxx,非底层 rit)"
    )


class EcpmReportOut(BaseModel):
    """eCPM 上报结果。best-effort,落库即 ok。"""

    ok: bool = True


class WatchReportIn(BaseModel):
    """客户端上报一次激励视频的实际观看时长(onAdClose 时,onAdShow→onAdClose 墙钟秒数)。

    user_id 由 JWT 取(Bearer),不在 body。seconds 服务端会夹 [0, MAX_SINGLE_WATCH_SECONDS]。
    """

    seconds: int = Field(..., ge=0, description="本次观看秒数")


class WatchReportOut(BaseModel):
    """上报后返回当日累计 + 剩余,客户端即时更新"今日还能看多久"。"""

    watched_seconds_today: int = Field(..., description="今日累计观看秒数")
    watch_seconds_limit: int = Field(..., description="每日上限(秒)")
    watch_seconds_remaining: int = Field(..., description="今日剩余可观看秒数")


class TestGrantIn(BaseModel):
    """[仅本地联调]模拟发奖入参。"""

    reward_scene: str = Field(
        "reward_video",
        description="模拟发奖场景。当前只支持 reward_video(普通激励视频);signin_boost(签到膨胀)"
        "已于 2026-07 下线,传它会 422",
    )
    ad_session_id: str | None = Field(
        None, min_length=8, max_length=64,
        description="本次广告会话 id(与 ecpm-report 同值)。reward_video 场景下据此查回客户端"
        "已上报的真实 eCPM 来按公式发奖;查不到或 eCPM≤0 时兜底 200,保证本地联调仍出非零金币",
    )
    boost_round_id: str | None = Field(
        None, max_length=64,
        description="本次广告属于哪一轮膨胀。正式链路走穿山甲 S2S 的 mediaExtra,test-grant 不经 S2S、"
        "拿不到 extra,故在 body 里补一个——不传的话 debug 包 reward-result 的 round_coin 恒为 null,"
        "「弹窗 40 → 60 → toast +60」那套验收在本地跑不起来",
    )


class TestGrantOut(BaseModel):
    """[仅本地联调]模拟发奖结果。带上今日进度,客户端可直接据此刷新展示。"""

    granted: bool = Field(..., description="本次是否真的发了金币(达每日上限则 False)")
    status: str = Field(
        ..., description="granted / capped / ecpm_missing / unknown_scene"
    )
    coin: int = Field(..., description="本次发放金币(capped 时为 0)")
    used_today: int = Field(..., description="今日已成功发奖次数")
    daily_limit: int = Field(..., description="每日发奖次数上限")
    remaining: int = Field(..., description="今日剩余可领次数")
    coin_per_ad: int = Field(..., description="历史兼容字段;正式发放按 eCPM 动态计算,当前返回 0")
    round_count: int = Field(..., description="本轮已看次数,详见 AdRewardStatusOut")
    cooldown_until: datetime | None = Field(
        None, description="本轮冷却结束时间(UTC),详见 AdRewardStatusOut"
    )


class FeedRewardIn(BaseModel):
    """比价/领券一整场信息流(轮播多条)结束后结算奖励。

    规则:全程不关广告才发,金额按整场**总观看时长**折份(每 10 秒 1 份)。client_event_id 用于
    客户端超时重试幂等。中途被用户关闭时传 aborted=True,整场不发(只记 closed_early)。
    """

    client_event_id: str = Field(..., min_length=8, max_length=64, description="客户端生成的幂等事件 id")
    ad_type: str = Field(
        "feed",
        max_length=16,
        description="广告类型:feed(信息流) / draw(Draw 信息流);默认 feed 兼容旧客户端",
    )
    ad_session_id: str | None = Field(
        None, min_length=8, max_length=64, description="客户端生成的一次信息流广告会话 id"
    )
    ecpm: str = Field(..., description="本场信息流 eCPM(代表值,按分/千次展示处理;SDK getEcpm 原值,非元)")
    duration_seconds: int = Field(..., ge=0, description="整场累计观看秒数(轮播各条相加)")
    adn: str | None = Field(None, description="实际投放 ADN")
    slot_id: str | None = Field(None, description="实际展示代码位")
    feed_scene: str | None = Field(
        None,
        max_length=16,
        description="点位场景:comparison(比价等待) / coupon(领券) / welfare(福利页);"
        "比价与领券共用同一信息流代码位,需客户端在各调用点显式标注,缺省=未分类",
    )
    trace_id: str | None = Field(
        None,
        max_length=64,
        description="本次比价 trace_id(比价场景必带):把这场广告金币归属到对应比价记录,"
        "比价记录页据此显示「比价赚 N 金币」。领券/福利场景为空",
    )
    app_env: str | None = Field(
        None, max_length=16, description="我们的穿山甲应用环境:prod(傻瓜比价正式) / test(测试应用)"
    )
    our_code_id: str | None = Field(
        None, max_length=64, description="我们后台配置的代码位 ID(AdConfig 的 104xxx,非底层 rit)"
    )
    aborted: bool = Field(
        False, description="用户中途 ✕ 关闭广告(未走完比价):整场不发,记 closed_early"
    )
    display_coin: int = Field(
        0, ge=0,
        description="客户端金币小球**本条显示**的金币(所见即所得):后端直接发这个数,钳到本条最大 1 份"
        "满额防刷。缺省 0 = 旧客户端不传,退回服务端「看满 10 秒发整份」",
    )


class FeedRewardOut(BaseModel):
    granted: bool = Field(..., description="本次是否入账。达上限时 false")
    status: str = Field(..., description="granted / capped / too_short / closed_early")
    coin: int = Field(..., description="本次发放金币")
    unit_count: int = Field(..., description="按 10 秒折算出的奖励份数")
    daily_limit: int = Field(..., description="每日信息流展示次数上限")


class FeedRewardUnitsOut(BaseModel):
    granted_units: int = Field(
        ...,
        description="账号累计已发奖的信息流**条**数(一条广告=一份额度),不按天重置;"
        "前端据此算因子2(LT)做实时金币进度条,与后端发奖公式对齐",
    )


class RewardNoShowIn(BaseModel):
    """激励视频展示了但用户提前关闭/跳过、未触发发奖——上报留痕(不发金币)。

    供广告收益报表把「有展示、没发金币」的原因也记录下来。user_id 由 JWT 取,不在 body。
    """

    ad_session_id: str = Field(
        ..., min_length=8, max_length=64, description="本次展示会话 id(与 ecpm 上报、S2S extra 一致)"
    )
    watched_seconds: int = Field(0, ge=0, description="关闭前已观看秒数(仅留痕参考,不入库)")
    ecpm: str | None = Field(None, description="本次展示 eCPM(分/千次,SDK getEcpm 原值);可空")
    adn: str | None = Field(None, description="实际投放 ADN")
    slot_id: str | None = Field(None, description="实际展示代码位(底层 mediation rit)")
    app_env: str | None = Field(
        None, max_length=16, description="我们的穿山甲应用环境:prod / test"
    )
    our_code_id: str | None = Field(
        None, max_length=64, description="我们后台配置的代码位 ID(104xxx)"
    )


class RewardNoShowOut(BaseModel):
    ok: bool = Field(..., description="是否处理成功(落库或幂等命中)")
    status: str = Field(
        ..., description="closed_early(已留痕) / granted(该次其实已发奖,跳过未写)"
    )
