"""重置指定账号的「领券引导视频」已播次数,让领券等候浮层重新放引导视频,方便反复看这支片。

原理:浮层放不放引导视频只由两件事决定(见 app/repositories/guide_video.py `start_play`):
  1. 运营配置 app_config(key=coupon_guide_video):enabled / video_url / max_plays;
  2. 该账号 guide_video_play 的**行数**(开播即计次)—— 行数 >= max_plays 就不再下发,
     `/api/v1/guide-video/start` 返 should_play=false,客户端照旧放广告。
所以「想再看一遍」= 删掉这个账号的 guide_video_play 行。配置本脚本**只读不改**(要换片子 /
开关去运营后台「配置 - 领券引导视频」;次数 3 / 金币 120 是服务端默认值,后台不开放调整),
这样不会把别的账号的线上行为一起动了。

默认连**当时发的金币一起退**(每次 reward_coin,现配置 120):这些流水 biz_type='guide_video',
不退的话每重置一轮余额就白涨一轮,收益明细里还会堆出一串「新手引导视频奖励」。想留着用 --keep-coins。
例外:金币已被兑换成现金、余额兜不住时**整笔跳过不退** —— coin_balance 必须恒等于流水总和,
硬退会退成负数,夹到 0 又会吃掉别处赚的金币,两种做法都会让账对不上(同 reset_signin_today.py)。

用法(在项目根、已 pip install -e . 的环境里跑):
  python scripts/reset_guide_video.py                  # 默认测试号 11111111111
  python scripts/reset_guide_video.py 13800138000      # 指定手机号
  python scripts/reset_guide_video.py --user-id 5      # 直接指定 user_id
  python scripts/reset_guide_video.py --dry-run        # 预览(照常执行再回滚),不落库
  python scripts/reset_guide_video.py --keep-coins     # 只清次数,已发金币不退

走 SessionLocal 连 DATABASE_URL(SQLite / Postgres 都行),因此**只允许 APP_ENV=dev 改库**
(--dry-run 只读,任何环境都能跑)。
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

from sqlalchemy import select

from app.core.config import settings
from app.db.session import SessionLocal, engine
from app.models.guide_video import GuideVideoPlay
from app.models.user import User
from app.models.wallet import CoinAccount, CoinTransaction
from app.repositories import guide_video as crud_guide

# Windows 控制台默认 GBK,强制 UTF-8 否则中文输出乱码。stderr 也要设:
# SystemExit(如"用户不存在")的中文提示走的是 stderr。
for _stream in (sys.stdout, sys.stderr):
    if hasattr(_stream, "reconfigure"):
        _stream.reconfigure(encoding="utf-8")

# dev 下 engine 是 echo=True(APP_DEBUG),几十行 SQL 会把前后对比刷没。echo 走 SQLAlchemy 自己的
# InstanceLogger,不吃 logging.setLevel,只能改 engine.echo。
engine.echo = False

DEFAULT_PHONE = "11111111111"


def resolve_user(db, phone: str, user_id: int | None) -> User:
    if user_id is not None:
        user = db.get(User, user_id)
        if user is None:
            raise SystemExit(f"user_id={user_id} 不存在")
        return user
    user = db.execute(select(User).where(User.phone == phone)).scalar_one_or_none()
    if user is None:
        raise SystemExit(f"手机号 {phone} 没有对应用户(注意 phone 才是登录账号,username 是展示 ID)")
    return user


def load_plays(db, user_id: int) -> list[GuideVideoPlay]:
    return list(db.execute(
        select(GuideVideoPlay)
        .where(GuideVideoPlay.user_id == user_id)
        .order_by(GuideVideoPlay.id)
    ).scalars().all())


def print_config(db) -> dict:
    """打印运营配置 + 片子是否真在盘上(配了地址但文件丢了,客户端会黑屏/加载失败)。"""
    cfg = crud_guide.get_config(db)
    video_url = (cfg.get("video_url") or "").strip()
    print("--- 运营配置(app_config: coupon_guide_video,本脚本不改) ---")
    print(f"  enabled={cfg['enabled']}  max_plays={cfg['max_plays']}  reward_coin={cfg['reward_coin']}")
    print(f"  video_url={video_url or '(未配片)'}")
    if video_url.startswith(settings.MEDIA_URL_PREFIX + "/"):
        rel = video_url[len(settings.MEDIA_URL_PREFIX) + 1:]
        f = Path(settings.MEDIA_ROOT) / rel
        if f.is_file():
            print(f"  文件: {f} ({f.stat().st_size / 1024 / 1024:.1f} MB) ✓")
        else:
            print(f"  ⚠️ 文件不存在: {f} —— 客户端会加载失败,请去运营后台重新上传")
    return cfg


def print_state(db, user: User, cfg: dict, label: str) -> None:
    plays = load_plays(db, user.id)
    max_plays = int(cfg.get("max_plays") or 0)
    used = len(plays)
    print(f"--- {label} ---")
    print(f"  guide_video_play: {used} 行(已用次数,开播即计),上限 {max_plays}")
    for p in plays:
        print(f"    #{p.id} 第{p.seq}次 {p.status} +{p.coin}金币 "
              f"completed={p.completed} 开播于 {p.started_at} token={p.play_token[:12]}…")

    # 用仓储层原样复算一遍"下次会不会放",而不是脚本里自己判规则 —— 这就是客户端会拿到的结果
    enabled = bool(cfg.get("enabled"))
    has_video = bool((cfg.get("video_url") or "").strip())
    will_play = enabled and has_video and max_plays > 0 and used < max_plays
    reason = (
        "会放引导视频" if will_play
        else "开关关着" if not enabled
        else "没配片" if not has_video
        else "max_plays=0" if max_plays <= 0
        else f"次数已用完({used}/{max_plays})"
    )
    print(f"  [下次点一键领取] should_play={will_play} —— {reason}")

    acc = db.get(CoinAccount, user.id)
    if acc is None:
        print("  coin_account: (无)")
    else:
        print(f"  coin_account: coin={acc.coin_balance} earned={acc.total_coin_earned}")


def refund_plays(db, user_id: int, tokens: list[str]) -> None:
    """退回这些播放发的金币:删流水 + 扣余额。余额兜不住的整笔跳过(见模块 docstring)。"""
    if not tokens:
        return
    rows = list(db.execute(
        select(CoinTransaction).where(
            CoinTransaction.user_id == user_id,
            CoinTransaction.biz_type == crud_guide.BIZ_TYPE,
            CoinTransaction.ref_id.in_(tokens),
        )
    ).scalars().all())
    if not rows:
        print("  没有可退的金币流水(可能是 reward_coin=0,或本来就没发过)")
        return
    acc = db.get(CoinAccount, user_id)
    if acc is None:
        return

    # 从最近一笔往回退,退到余额兜不住为止 —— 保证"最新发的那笔"一定被退掉。
    rows.sort(key=lambda r: r.id, reverse=True)
    refundable: list[CoinTransaction] = []
    total = 0
    for r in rows:
        if total + r.amount > acc.coin_balance:
            break
        refundable.append(r)
        total += r.amount

    for r in refundable:
        db.delete(r)
    if total:
        acc.coin_balance -= total
        acc.total_coin_earned = max(0, acc.total_coin_earned - total)
        print(f"  已退回 {total} 金币({len(refundable)}/{len(rows)} 笔引导视频奖励)")

    stuck = len(rows) - len(refundable)
    if stuck:
        print(f"  ⚠️ 还有 {stuck} 笔退不掉(金币已被兑换/花掉,余额 {acc.coin_balance} 兜不住),原样保留 —— "
              "硬退会让余额和流水总和对不上。收益明细里会多留几条,不影响再看视频。")


def main() -> None:
    parser = argparse.ArgumentParser(description="重置账号的领券引导视频次数,让浮层重新放引导视频")
    parser.add_argument("phone", nargs="?", default=DEFAULT_PHONE,
                        help=f"手机号(默认 {DEFAULT_PHONE})")
    parser.add_argument("--user-id", type=int, default=None, help="直接按 user_id 定位,优先于 phone")
    parser.add_argument("--keep-coins", action="store_true",
                        help="不退已发金币(余额会越测越高,收益明细堆重复流水)")
    parser.add_argument("--dry-run", action="store_true", help="预览,最后回滚不落库")
    args = parser.parse_args()

    if not args.dry_run and settings.APP_ENV != "dev":
        raise SystemExit(f"APP_ENV={settings.APP_ENV},拒绝改库(只有 dev 能改;--dry-run 可任意环境)")

    db = SessionLocal()
    try:
        user = resolve_user(db, args.phone, args.user_id)
        print(f"DB: {settings.DATABASE_URL}  APP_ENV: {settings.APP_ENV}")
        print(f"用户: id={user.id} phone={user.phone} username={user.username}  keep_coins: {args.keep_coins}")
        cfg = print_config(db)
        print_state(db, user, cfg, "before")

        plays = load_plays(db, user.id)
        if not plays:
            print("该账号没有任何播放记录,次数本来就是满的,无需重置。")
            db.rollback()
            return

        tokens = [p.play_token for p in plays]
        for p in plays:
            db.delete(p)
        db.flush()

        if args.keep_coins:
            print("(--keep-coins:保留已发金币,流水和余额不动)")
        else:
            refund_plays(db, user.id, tokens)
            # 注:更早流水的 balance_after 是当时的快照,不回改 —— 收益明细里历史行的余额列
            # 会与现余额对不上,dev 测试库无妨。

        print_state(db, user, cfg, "after")

        if args.dry_run:
            db.rollback()
            print(f"(dry-run:以上 after 为预览,已回滚,库没动;真跑会删 {len(plays)} 条播放记录)")
            return
        db.commit()
        print(f"完成:删掉 {len(plays)} 条播放记录,{user.phone} 下次点「一键自动领取」浮层会重新放引导视频。")
        print("提醒:客户端是在浮层建窗时调 /guide-video/start 的,不用重装、不用重登,直接再点一次一键领取即可;"
              "但「广告显示」开关关掉时整块浮层不建窗(连引导视频也不放),测之前先确认它是开的。")
    finally:
        db.close()


if __name__ == "__main__":
    main()
