"""撤销指定用户「今天已签到」的状态,让今天可以重新签到,方便反复测试签到流程。

与 reset_signin.py 的区别:那个删**全部**签到历史(连续天数从头再来);本脚本只精确撤销
**今天**这一次,昨天及以前的记录原样保留 —— 所以重签后 cycle_day / streak 会接着昨天继续,
7 天循环的档位不会被打乱,可以连着好几天测「第 N 档」的奖励。

默认是**完整撤销**(等于今天这次签到从没发生过):
  1. 删 signin_record 今天这行 → 今天变回未签到
  2. 删今天的 signin 金币流水,并把金币从 coin_account 余额 / 累计收益里扣回

金币默认要退:签到流水**没有**唯一索引拦重复(ux_coin_transaction_task_ref 只覆盖
biz_type LIKE 'task%'),不退的话每测一轮余额就白涨一次奖励,coin_transaction 里还会堆出
同一 ref_id(日期)的重复流水,收益明细页会看到两条今天的签到。真想留着奖励用 --keep-coins。

例外:签到的金币若已被兑换成现金(余额已不够退),**自动跳过退款**并保留今天的签到流水。
因为 coin_balance 必须恒等于流水总和,硬退会把余额退成负数 —— 夹到 0 又会吃掉别处赚的金币,
两种做法都会让账对不上。这时重签会再发一次奖励,余额多涨一档,属可接受的测试噪音。

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

「今天」直接复用 app.core.rewards.cn_today(北京时间),与签到判重同源,不自己算时区。
走 SessionLocal 连 DATABASE_URL(SQLite / Postgres 都行),因此**只允许 APP_ENV=dev 时改库**
(--dry-run 只读,任何环境都能跑)。
"""
from __future__ import annotations

import argparse
import sys

from sqlalchemy import select

from app.core.config import settings
from app.core.rewards import cn_today
from app.db.session import SessionLocal, engine
from app.models.signin import SigninRecord
from app.models.user import User
from app.models.wallet import CoinAccount, CoinTransaction
from app.repositories import signin as crud_signin

# Windows 控制台默认 GBK,强制 UTF-8 否则中文输出乱码
if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.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 print_state(db, user: User, today, label: str) -> None:
    print(f"--- {label} ---")
    rec = db.execute(
        select(SigninRecord).where(
            SigninRecord.user_id == user.id, SigninRecord.signin_date == today
        )
    ).scalar_one_or_none()
    if rec is None:
        print(f"  signin_record {today}: (无)")
    else:
        print(f"  signin_record {today}: 第{rec.cycle_day}档 连续{rec.streak}天 +{rec.coin_awarded}金币")

    last = db.execute(
        select(SigninRecord.signin_date)
        .where(SigninRecord.user_id == user.id)
        .order_by(SigninRecord.signin_date.desc())
        .limit(1)
    ).scalar_one_or_none()
    print(f"  最近一次签到: {last or '(从未签到)'}")


    rows = db.execute(
        select(CoinTransaction).where(
            CoinTransaction.user_id == user.id,
            CoinTransaction.biz_type == "signin",
            CoinTransaction.ref_id == today.isoformat(),
        )
    ).scalars().all()
    print(f"  coin_transaction(signin, 今天): {len(rows)} 条 / {sum(r.amount for r in rows)} 金币")

    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}")

    # 用 App 自己的 get_status 复核,而不是脚本里重算一遍规则 —— 这行就是客户端会看到的
    st = crud_signin.get_status(db, user.id)
    print(f"  [签到接口] can_claim={st.can_claim} today_signed={st.today_signed} "
          f"今天第{st.today_cycle_day}档({st.today_coin}金币) 已连续{st.consecutive_days}天")


def refund_today(db, user_id: int, today) -> None:
    """退回今天签到(含膨胀)发的金币:删流水 + 扣余额。

    不变量:coin_balance 必须恒等于流水总和。所以余额不够退时**整笔跳过**,而不是硬退成
    负数、或夹到 0 —— 夹到 0 会吃掉用户在别处赚的金币,两种做法都会让余额和流水对不上。
    """
    rows = list(db.execute(
        select(CoinTransaction).where(
            CoinTransaction.user_id == user_id,
            CoinTransaction.biz_type == "signin",
            CoinTransaction.ref_id == today.isoformat(),
        )
    ).scalars().all())
    if not rows:
        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:
        # 典型场景:签完就把金币兑换成现金了(exchange_out),这笔奖励已经变成 cash_balance_cents,
        # 余额里已经没有它了。硬退会把余额退成负数 / 夹到 0 又会吃掉别处赚的金币,两者都会让账对不上。
        print(f"  ⚠️ 还有 {stuck} 笔今天的签到流水退不掉(金币已被兑换/花掉,余额 {acc.coin_balance} 兜不住),"
              f"原样保留 —— 硬退会让余额和流水总和对不上。")
        print("     → 收益明细今天会多出几条签到记录,不影响签到功能测试;想彻底清干净用 reset_signin.py --with-coins。")


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 可任意环境)")

    today = cn_today()
    db = SessionLocal()
    try:
        user = resolve_user(db, args.phone, args.user_id)
        print(f"DB: {settings.DATABASE_URL}")
        print(f"用户: id={user.id} phone={user.phone}  今天(北京): {today}  keep_coins: {args.keep_coins}")
        print_state(db, user, today, "before")

        # 今天的签到记录 —— 只删今天,昨天及以前保留,重签后 streak 接着涨
        rec = db.execute(
            select(SigninRecord).where(
                SigninRecord.user_id == user.id, SigninRecord.signin_date == today
            )
        ).scalar_one_or_none()
        if rec is not None:
            db.delete(rec)

        if rec is None:
            print("今天本来就没签到,无需处理。")
            db.rollback()
            return

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

        # SessionLocal 是 autoflush=False,不 flush 的话下面 print_state 的 select
        # 读到的还是删之前的旧行,"after" 会骗人
        db.flush()
        print_state(db, user, today, "after")

        if args.dry_run:
            db.rollback()
            print("(dry-run:以上 after 为预览,已回滚,库没动)")
            return
        db.commit()
        print(f"完成:{user.phone} 今天({today})可以重新签到了。"
              f"提醒:App 内存状态不会自动同步,杀掉重进福利页(当天未签到)会重新自动弹签到弹窗。")
    finally:
        db.close()


if __name__ == "__main__":
    main()
