"""领券今日状态读写:弹窗频控(engagement)+ 领券记录(claim)。

写操作按唯一键幂等 upsert,自带 commit + 并发 IntegrityError 兜底(对齐 price_observation)。
日期口径 = Asia/Shanghai 的自然日。
"""
from __future__ import annotations

import logging
from datetime import date, datetime
from zoneinfo import ZoneInfo

from sqlalchemy import delete, func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.models.coupon_state import (
    CouponClaimRecord,
    CouponDailyCompletion,
    CouponPromptEngagement,
)

logger = logging.getLogger("shagua.coupon_state")

_CN_TZ = ZoneInfo("Asia/Shanghai")


def today_cn() -> date:
    """Asia/Shanghai 的自然日(领券判断的"今天")。"""
    return datetime.now(_CN_TZ).date()


# ===== 弹窗频控(coupon_prompt_engagement)=====

def has_engaged_today(db: Session, device_id: str, package: str) -> bool:
    """这台设备今天**这个 App** 是否已对领券引导窗表达过意向(领/拒/弹出)。有 = 该 App 不再弹。
    频控按 (device, package, 日):美团弹过不影响淘宝/京东今天各自仍弹一次。"""
    row = db.execute(
        select(CouponPromptEngagement.id).where(
            CouponPromptEngagement.device_id == device_id,
            CouponPromptEngagement.package == package,
            CouponPromptEngagement.engage_date == today_cn(),
        )
    ).first()
    return row is not None


def mark_engagement(
    db: Session, device_id: str, package: str, user_id: int | None, engage_type: str
) -> None:
    """记今日意向(shown / claim_started / dismissed)。(device, package, 今天) 唯一,幂等 upsert。

    engage_type 升级口径(同一 (device,package,日) 多次调,只覆盖 type,不新增行):
    shown(自动弹出)→ claim_started(点一键领取)/ dismissed(点关闭)。判断只看"有没有这条"。
    """
    today = today_cn()
    row = db.execute(
        select(CouponPromptEngagement).where(
            CouponPromptEngagement.device_id == device_id,
            CouponPromptEngagement.package == package,
            CouponPromptEngagement.engage_date == today,
        )
    ).scalar_one_or_none()
    if row is not None:
        row.engage_type = engage_type
        if user_id is not None:
            row.user_id = user_id
    else:
        db.add(CouponPromptEngagement(
            device_id=device_id, package=package, user_id=user_id,
            engage_date=today, engage_type=engage_type,
        ))
    try:
        db.commit()
    except IntegrityError:
        # 并发下另一请求刚插了同 (device, package, 日) → 唯一约束撞,回滚忽略(本就幂等)。
        db.rollback()


def reset_today_engagement(db: Session, device_id: str) -> int:
    """删这台设备今天**所有 App** 的 engagement(开发设置「重置今日领券弹窗状态」调,测频控用)。
    删后各 App has_engaged_today → false,今天又都能弹。返回删除行数。
    (不按 package 过滤:重置是"把今天清干净从头测",清全部 App 最符合预期。)"""
    result = db.execute(
        delete(CouponPromptEngagement).where(
            CouponPromptEngagement.device_id == device_id,
            CouponPromptEngagement.engage_date == today_cn(),
        )
    )
    db.commit()
    return result.rowcount or 0


# ===== 今日跑完整轮(coupon_daily_completion)=====

def has_completed_today(db: Session, device_id: str) -> bool:
    """这台设备今天是否已跑完整轮领券(到 done 帧)。有 = 首页置灰、不能再领。"""
    row = db.execute(
        select(CouponDailyCompletion.id).where(
            CouponDailyCompletion.device_id == device_id,
            CouponDailyCompletion.complete_date == today_cn(),
        )
    ).first()
    return row is not None


def mark_completed_today(
    db: Session, device_id: str, user_id: int | None, trace_id: str | None = None
) -> None:
    """记今日已跑完整轮。(device, 今天) 唯一,幂等 upsert。到 done 即记,不管单券成败。"""
    today = today_cn()
    row = db.execute(
        select(CouponDailyCompletion).where(
            CouponDailyCompletion.device_id == device_id,
            CouponDailyCompletion.complete_date == today,
        )
    ).scalar_one_or_none()
    if row is not None:
        if user_id is not None:
            row.user_id = user_id
        if trace_id is not None:
            row.trace_id = trace_id
    else:
        db.add(CouponDailyCompletion(
            device_id=device_id, user_id=user_id,
            complete_date=today, trace_id=trace_id,
        ))
    try:
        db.commit()
    except IntegrityError:
        # 并发下另一请求刚插了同 (device, 日) → 唯一约束撞,回滚忽略(本就幂等)。
        db.rollback()


def reset_today_completion(db: Session, device_id: str) -> int:
    """删这台设备今天的"已完成"记录(开发设置「重置今日领券弹窗状态」全重置时调)。
    删后 has_completed_today → false,首页「去领取」卡恢复可点。返回删除行数。"""
    result = db.execute(
        delete(CouponDailyCompletion).where(
            CouponDailyCompletion.device_id == device_id,
            CouponDailyCompletion.complete_date == today_cn(),
        )
    )
    db.commit()
    return result.rowcount or 0


# ===== 领券记录(coupon_claim_record)=====

def record_claims(
    db: Session,
    device_id: str,
    user_id: int | None,
    trace_id: str | None,
    results: list[dict],
) -> int:
    """一批券领取结果幂等写入,返回写入(新增 + 更新)条数。

    results 单项取自 pricebot 的 last_coupon_result / done.coupon_results,识别字段:
    coupon_id(必需)/ status(必需)/ name / vendor / reason /(display_count)。
    (device, coupon_id, 今天) 唯一:重复上报同张券走更新(status 以最后一次为准)。
    """
    today = today_cn()
    written = 0
    seen: set[str] = set()  # 同批去重防御:autoflush=False 下同 coupon_id 重复会两次 add → 撞唯一约束回滚整批
    for r in results:
        coupon_id = r.get("coupon_id")
        status = r.get("status")
        if not coupon_id or not status or coupon_id in seen:
            continue  # 脏数据 / 同批重复跳过
        seen.add(coupon_id)
        count = r.get("display_count")
        if count is None:
            count = r.get("claimed_count")
        row = db.execute(
            select(CouponClaimRecord).where(
                CouponClaimRecord.device_id == device_id,
                CouponClaimRecord.coupon_id == coupon_id,
                CouponClaimRecord.claim_date == today,
            )
        ).scalar_one_or_none()
        if row is not None:
            row.status = status
            row.reason = r.get("reason")
            if user_id is not None:
                row.user_id = user_id
            if count is not None:
                row.claimed_count = count
            row.extra = r
        else:
            db.add(CouponClaimRecord(
                device_id=device_id, user_id=user_id,
                coupon_id=coupon_id, claim_date=today,
                status=status, vendor=r.get("vendor"), coupon_name=r.get("name"),
                claimed_count=count, trace_id=trace_id, reason=r.get("reason"),
                extra=r,
            ))
        written += 1
    if written == 0:
        return 0
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        logger.warning(
            "coupon_claim 并发幂等冲突 device=%s trace=%s,回滚", device_id, trace_id
        )
        return 0
    return written


# ===== 累计领券数(「我的」页战绩卡「领取优惠券 X 张」)=====

def sum_claimed_count(db: Session, user_id: int) -> int:
    """该用户累计领到的优惠券张数。口径(2026-06-15 用户定):SUM(claimed_count) ——
    各成功领券记录的 pricebot 展示张数(claimed_count 列,存的是 display_count)之和,
    与领券完成时给用户看的「本次领了 N 张」同源。

    - 只算 status ∈ {success, already_claimed}:already_claimed=今日已领过,协议里算「已领到」;
      failed / skipped 不计。
    - claimed_count 为 0 的保持 0:那是同 count_group 合并去重项(pricebot 只让一条出数),不重复计;
      为 NULL 的兜底成 1(成功领到至少 1 张;实际 pricebot to_dict 恒下发 display_count,NULL 基本不出现)。
    - 维度 user_id:登录态领的券才归入。登录前匿名领的(user_id 为空)不算(产品可接受)。
    """
    total = db.execute(
        select(
            func.coalesce(
                func.sum(func.coalesce(CouponClaimRecord.claimed_count, 1)), 0
            )
        ).where(
            CouponClaimRecord.user_id == user_id,
            CouponClaimRecord.status.in_(("success", "already_claimed")),
        )
    ).scalar_one()
    return int(total or 0)
