"""普通会员推广路由。"""
import random
import string
from datetime import datetime, timezone
from typing import Literal

from pydantic import BaseModel, Field, field_validator
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.deps import get_current_user
from app.database import get_db
from app.models import AffiliateClick, AgentSettlement, AgentStatus, PromoCode, User, UserAgent
from app.models.coupon_mall_claim import CouponMallClaim, CouponMallClaimStatus
from app.models.coupon_mall_item import CouponMallItem, CouponMallItemScope, CouponMallItemStatus
from app.models.point_log import PointChangeType, PointLog
from app.models.promo_code import PromoCodeClaimMethod, PromoCodeDiscountLevel, PromoCodeScope, PromoCodeType
from app.schemas.common import ApiResponse
from app.schemas.user import GeetestPayload
from app.services import (
    agent_balance_service,
    agent_service,
    captcha_service,
    membership_service,
    notification_service,
    promotion_coupon_service,
    system_setting_service,
)

router = APIRouter()

ALLOWED_FIXED_COUPON_YUAN = (1, 2, 3, 5, 10)


class InviteCodeUpdateRequest(BaseModel):
    invite_code: str = Field(..., min_length=3, max_length=8, pattern=r"^[A-Za-z0-9]+$")

    @field_validator("invite_code", mode="before")
    @classmethod
    def normalize_invite_code(cls, value: str) -> str:
        return str(value).strip().upper()


class PromotionCouponCreateRequest(BaseModel):
    code: str | None = Field(None, min_length=4, max_length=16, pattern=r"^[A-Za-z0-9]+$")
    description: str | None = Field(None, max_length=255)
    type: Literal["fixed", "threshold_fixed", "percent"] = "fixed"
    value: int = Field(..., ge=1, description="fixed/threshold_fixed 为优惠金额（分），percent 为折扣 1-100")
    threshold_amount: int | None = Field(None, ge=0, description="threshold_fixed 的满减门槛（分）")
    max_discount_amount: int | None = Field(None, ge=0, description="percent 可选封顶金额（分）")
    max_uses: int = Field(1, ge=1, le=1000)
    max_uses_per_user: int = Field(1, ge=1, le=100)
    expires_at: datetime | None = None
    claim_method: Literal["code", "mall"] = "code"
    list_on_mall: bool = False
    points_price: int | None = Field(None, ge=0, description="商城领取方式时所需积分")
    stock_total: int | None = Field(None, ge=1, description="商城领取方式时总库存")
    mall_title: str | None = Field(None, max_length=200, description="商城展示标题")
    mall_subtitle: str | None = Field(None, max_length=500, description="商城展示副标题")
    mall_cover_image: str | None = Field(None, max_length=500, description="商城封面图 URL")
    mall_category: str | None = Field(None, max_length=50, description="商城分类")
    mall_scope: Literal["public", "agent_subordinates"] = "public"
    mall_sort_order: int = Field(0, ge=0)

    @field_validator("code", mode="before")
    @classmethod
    def normalize_coupon_code(cls, value: str | None) -> str | None:
        if value is None or str(value).strip() == "":
            return None
        return str(value).strip().upper()

    @field_validator("type", mode="before")
    @classmethod
    def normalize_type(cls, value: str | None) -> str:
        return str(value or "fixed").lower().strip()

    @field_validator("claim_method", mode="before")
    @classmethod
    def normalize_claim_method(cls, value: str | None) -> str:
        return str(value or "code").lower().strip()


def _promotion_link(base_url: str, code: str) -> str:
    return f"{base_url.rstrip('/')}/register?ref={code}"


def _enum_value(value) -> str:
    return value.value if hasattr(value, "value") else str(value)


def _settlement_status_label(status: str) -> str:
    return {
        "pending": "待结算",
        "frozen": "冻结中",
        "settled": "已到账",
        "reversed": "已扣回",
    }.get(status, status)


def _dump_empty_list() -> str | None:
    return None


def _coupon_refundable_points(coupon: PromoCode) -> int:
    return promotion_coupon_service.coupon_refundable_points(coupon)


def _coupon_to_dict(coupon: PromoCode) -> dict:
    point_cost = int(getattr(coupon, "point_cost", 0) or 0)
    point_refunded = int(getattr(coupon, "point_refunded", 0) or 0)
    return {
        "id": coupon.id,
        "code": coupon.code,
        "description": coupon.description,
        "issuer_type": getattr(coupon, "issuer_type", None),
        "issuer_user_id": getattr(coupon, "issuer_user_id", None),
        "point_cost": point_cost,
        "point_refunded": point_refunded,
        "refundable_points": _coupon_refundable_points(coupon),
        "type": _enum_value(coupon.type),
        "value": int(coupon.value or 0),
        "threshold_amount": int(coupon.min_order_amount or 0),
        "max_discount_amount": int(getattr(coupon, "max_discount_amount", 0) or 0),
        "discount_level": _enum_value(getattr(coupon, "discount_level", PromoCodeDiscountLevel.ORDER.value)),
        "scope": _enum_value(coupon.scope),
        "claim_method": _enum_value(getattr(coupon, "claim_method", PromoCodeClaimMethod.CODE.value)),
        "list_on_mall": bool(getattr(coupon, "list_on_mall", False)),
        "starts_at": coupon.starts_at.isoformat() if coupon.starts_at else None,
        "expires_at": coupon.expires_at.isoformat() if coupon.expires_at else None,
        "max_uses": int(coupon.max_uses or 0),
        "max_uses_per_user": int(coupon.max_uses_per_user or 0),
        "used_count": int(coupon.used_count or 0),
        "new_users_only": bool(coupon.new_users_only),
        "applies_to_renewals": bool(coupon.applies_to_renewals),
        "is_stackable": bool(coupon.is_stackable),
        "is_enabled": bool(coupon.is_enabled),
        "created_at": coupon.created_at.isoformat() if coupon.created_at else None,
    }


async def _ensure_coupon_code_available(db: AsyncSession, code: str) -> None:
    exists = await db.scalar(select(PromoCode.id).where(func.upper(PromoCode.code) == code.upper()).limit(1))
    if exists:
        raise HTTPException(status_code=400, detail="优惠券代码已存在")


async def _generate_coupon_code(db: AsyncSession, prefix: str) -> str:
    alphabet = string.ascii_uppercase + string.digits
    for _ in range(20):
        code = prefix + "".join(random.choices(alphabet, k=8))
        exists = await db.scalar(select(PromoCode.id).where(PromoCode.code == code).limit(1))
        if not exists:
            return code
    raise HTTPException(status_code=500, detail="优惠券代码生成失败")


async def _get_active_agent(db: AsyncSession, user_id: int) -> UserAgent:
    result = await db.execute(
        select(UserAgent).where(
            UserAgent.user_id == user_id,
            UserAgent.status.in_([AgentStatus.ACTIVE, AgentStatus.OBSERVING]),
        )
    )
    agent = result.scalar_one_or_none()
    if not agent:
        raise HTTPException(status_code=403, detail="您还不是已激活代理，不能创建代理券")
    if not agent.agent_tier:
        raise HTTPException(status_code=403, detail="代理档位未配置，不能创建代理券")
    return agent


async def _ensure_agent_coupon_quota(db: AsyncSession, agent: UserAgent) -> None:
    max_count = int(getattr(agent.agent_tier, "max_monthly_coupon_count", 0) or 0)
    if max_count <= 0:
        return
    now = datetime.now(timezone.utc)
    month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    count = await db.scalar(
        select(func.count(PromoCode.id)).where(
            PromoCode.issuer_user_id == agent.user_id,
            PromoCode.issuer_type == "agent",
            PromoCode.created_at >= month_start,
        )
    )
    if int(count or 0) >= max_count:
        raise HTTPException(status_code=400, detail=f"本月代理券创建数量已达上限 {max_count} 张")


def _validate_coupon_request(body: PromotionCouponCreateRequest, *, is_agent: bool) -> None:
    if body.expires_at:
        expires_at = body.expires_at
        if expires_at.tzinfo is None:
            expires_at = expires_at.replace(tzinfo=timezone.utc)
        if expires_at <= datetime.now(timezone.utc):
            raise HTTPException(status_code=400, detail="有效期必须晚于当前时间")

    if body.claim_method not in ("code", "mall"):
        raise HTTPException(status_code=400, detail="领取方式仅支持优惠码或商城领取")

    if body.claim_method == "mall":
        if body.points_price is None or body.points_price <= 0:
            raise HTTPException(status_code=400, detail="商城领取需要设置领取积分")
        if body.stock_total is None or body.stock_total <= 0:
            raise HTTPException(status_code=400, detail="商城领取需要设置库存")

    if body.type == "percent":
        if not is_agent:
            raise HTTPException(status_code=400, detail="会员券不支持百分比折扣")
        if body.value <= 0 or body.value > 100:
            raise HTTPException(status_code=400, detail="折扣百分比需在 1-100 之间")
    elif body.type == "threshold_fixed":
        if body.threshold_amount is None or body.threshold_amount <= 0:
            raise HTTPException(status_code=400, detail="满减券需要设置满减门槛")
        if body.value <= 0:
            raise HTTPException(status_code=400, detail="满减金额必须大于 0")
    elif body.type == "fixed":
        if body.value <= 0:
            raise HTTPException(status_code=400, detail="优惠金额必须大于 0")
    else:
        raise HTTPException(status_code=400, detail="不支持的优惠券类型")


def _settlement_reward_to_dict(item: AgentSettlement) -> dict:
    return {
        "id": f"settlement-{item.id}",
        "source": "settlement",
        "order_id": item.order_id,
        "ref_no": item.order_no,
        "remark": item.remark,
        "points": item.points_amount,
        "status": item.status,
        "status_label": _settlement_status_label(item.status),
        "frozen_until": item.frozen_until.isoformat() if item.frozen_until else None,
        "settled_at": item.settled_at.isoformat() if item.settled_at else None,
        "reversed_at": item.reversed_at.isoformat() if item.reversed_at else None,
        "created_at": item.created_at.isoformat() if item.created_at else None,
    }


def _legacy_reward_log_to_dict(log: PointLog) -> dict:
    return {
        "id": f"log-{log.id}",
        "source": "point_log",
        "order_id": log.order_id,
        "ref_no": log.ref_no,
        "remark": log.remark,
        "points": max(0, int(log.delta or 0)),
        "status": "settled",
        "status_label": "已到账",
        "frozen_until": None,
        "settled_at": log.created_at.isoformat() if log.created_at else None,
        "reversed_at": None,
        "created_at": log.created_at.isoformat() if log.created_at else None,
    }


@router.get("/me", response_model=ApiResponse)
async def get_promotion_overview(
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    code = await agent_service.generate_invite_code(db, current_user)
    business_settings = await system_setting_service.get_business_settings(db)
    link = _promotion_link(business_settings.promotion_base_url, code)

    click_count_res = await db.execute(
        select(func.count()).select_from(AffiliateClick).where(AffiliateClick.agent_id == current_user.id)
    )
    registered_count_res = await db.execute(
        select(func.count()).select_from(User).where(User.bound_agent_id == current_user.id)
    )
    settlement_log_ids_stmt = select(AgentSettlement.point_log_id).where(
        AgentSettlement.user_id == current_user.id,
        AgentSettlement.settlement_type == "member_commission",
        AgentSettlement.point_log_id.is_not(None),
    )
    legacy_reward_points_res = await db.execute(
        select(func.coalesce(func.sum(PointLog.delta), 0)).where(
            PointLog.user_id == current_user.id,
            PointLog.change_type == PointChangeType.REFERRAL_REWARD,
            PointLog.id.not_in(settlement_log_ids_stmt),
        )
    )
    settled_points_res = await db.execute(
        select(func.coalesce(func.sum(AgentSettlement.points_amount), 0)).where(
            AgentSettlement.user_id == current_user.id,
            AgentSettlement.settlement_type == "member_commission",
            AgentSettlement.status == "settled",
        )
    )
    frozen_points_res = await db.execute(
        select(func.coalesce(func.sum(AgentSettlement.points_amount), 0)).where(
            AgentSettlement.user_id == current_user.id,
            AgentSettlement.settlement_type == "member_commission",
            AgentSettlement.status.in_(["pending", "frozen"]),
        )
    )
    reversed_points_res = await db.execute(
        select(func.coalesce(func.sum(AgentSettlement.points_amount), 0)).where(
            AgentSettlement.user_id == current_user.id,
            AgentSettlement.settlement_type == "member_commission",
            AgentSettlement.status == "reversed",
        )
    )

    reward_points = int(legacy_reward_points_res.scalar() or 0) + int(settled_points_res.scalar() or 0)
    frozen_reward_points = int(frozen_points_res.scalar() or 0)
    reversed_reward_points = int(reversed_points_res.scalar() or 0)
    membership = await membership_service.get_or_create_membership(db, current_user.id)
    reward_rate = membership.member_level.commission_rate if membership.member_level else 0

    return ApiResponse(data={
        "invite_code": code,
        "invite_link": link,
        "register_link": link,
        "invite_code_edit_enabled": business_settings.invite_code_edit_enabled,
        "invite_code_change_count": int(current_user.invite_code_change_count or 0),
        "invite_code_change_limit": agent_service.invite_code_change_limit(current_user),
        "invite_code_change_remaining": agent_service.invite_code_change_remaining(current_user),
        "click_count": click_count_res.scalar() or 0,
        "registered_count": registered_count_res.scalar() or 0,
        "reward_points": reward_points,
        "settled_reward_points": reward_points,
        "frozen_reward_points": frozen_reward_points,
        "reversed_reward_points": reversed_reward_points,
        "reward_rate": reward_rate,
        "available_points": membership.available_points,
        "member_level": membership.member_level.level if membership.member_level else None,
        "member_level_name": membership.member_level.name if membership.member_level else None,
    })


@router.patch("/invite-code", response_model=ApiResponse)
async def update_invite_code(
    body: InviteCodeUpdateRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    code = body.invite_code
    business_settings = await system_setting_service.get_business_settings(db)
    if not business_settings.invite_code_edit_enabled:
        raise HTTPException(status_code=403, detail="当前暂停自定义推广码")

    code, change_count, change_limit = await agent_service.change_user_invite_code(db, current_user, code)

    link = _promotion_link(business_settings.promotion_base_url, code)
    return ApiResponse(
        data={
            "invite_code": code,
            "invite_link": link,
            "register_link": link,
            "invite_code_edit_enabled": business_settings.invite_code_edit_enabled,
            "invite_code_change_count": change_count,
            "invite_code_change_limit": change_limit,
            "invite_code_change_remaining": max(0, change_limit - change_count),
        },
        message="推广码已更新",
    )


@router.get("/coupons", response_model=ApiResponse)
async def list_promotion_coupons(
    limit: int = Query(50, ge=1, le=100),
    offset: int = Query(0, ge=0),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    await promotion_coupon_service.refund_expired_member_coupons(db, user_id=current_user.id, limit=100)
    total = await db.scalar(
        select(func.count(PromoCode.id)).where(PromoCode.issuer_user_id == current_user.id)
    )
    result = await db.execute(
        select(PromoCode)
        .where(PromoCode.issuer_user_id == current_user.id)
        .order_by(PromoCode.id.desc())
        .limit(limit)
        .offset(offset)
    )
    items = [_coupon_to_dict(coupon) for coupon in result.scalars().all()]
    return ApiResponse(data={"items": items, "total": int(total or 0)})


@router.post("/coupons", response_model=ApiResponse)
async def create_promotion_coupon(
    body: PromotionCouponCreateRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    # 自动判断券归属：已激活代理 -> 代理券，否则 -> 会员券
    agent = await db.execute(
        select(UserAgent).where(
            UserAgent.user_id == current_user.id,
            UserAgent.status.in_([AgentStatus.ACTIVE, AgentStatus.OBSERVING]),
        )
    )
    active_agent = agent.scalar_one_or_none()
    is_agent = active_agent is not None and active_agent.agent_tier_id is not None

    _validate_coupon_request(body, is_agent=is_agent)

    issuer_type = "agent" if is_agent else "member"
    point_cost = 0

    code = body.code or await _generate_coupon_code(db, "AC" if is_agent else "MC")
    await _ensure_coupon_code_available(db, code)

    if is_agent:
        await _ensure_agent_coupon_quota(db, active_agent)
        # 如果创建的是可能低于成本的百分比券，校验最低余额门槛
        if body.type == "percent":
            # 折扣力度低于成本折扣时可能产生补贴
            cost_rate_bps = active_agent.agent_tier.cost_rate if active_agent.agent_tier else 10000
            # value 是 percent，例如 30 表示 3 折；cost_rate_bps 是万分比，例如 5000 表示 5 折
            # 当 percent < cost_rate_bps/100 时，可能补贴
            if body.value * 100 < cost_rate_bps:
                business_settings = await system_setting_service.get_business_settings(db)
                min_balance = business_settings.min_agent_balance_for_subsidy_coupon_cents
                if active_agent.agent_balance < min_balance:
                    raise HTTPException(
                        status_code=400,
                        detail=f"创建低于成本折扣的代理券需要代理余额不低于 ¥{min_balance / 100:.2f}",
                    )
    else:
        # 会员券预扣积分
        max_discount_per_use = body.value
        if body.type == "percent":
            max_discount_per_use = body.max_discount_amount or 0
        point_cost = max_discount_per_use * body.max_uses * membership_service.POINTS_TO_CENTS_RATE // 100
        try:
            await membership_service.add_points(
                db=db,
                user_id=current_user.id,
                delta=-point_cost,
                change_type=PointChangeType.ACTIVITY_GIFT,
                ref_no=code,
                remark=f"创建会员优惠券扣除积分：{body.value / 100:.2f}元 x {body.max_uses}次",
            )
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc

    # 映射优惠券类型
    if body.type == "percent":
        promo_type = PromoCodeType.PERCENT.value
    elif body.type == "threshold_fixed":
        promo_type = PromoCodeType.THRESHOLD_FIXED.value
    else:
        promo_type = PromoCodeType.FIXED.value

    coupon = PromoCode(
        code=code,
        description=body.description,
        issuer_user_id=current_user.id,
        issuer_type=issuer_type,
        point_cost=point_cost,
        point_refunded=0,
        type=promo_type,
        value=body.value,
        discount_level=PromoCodeDiscountLevel.ORDER.value,
        scope=PromoCodeScope.ALL.value,
        scope_ids=_dump_empty_list(),
        claim_method=body.claim_method,
        list_on_mall=body.list_on_mall,
        starts_at=datetime.now(timezone.utc),
        expires_at=body.expires_at,
        max_uses=body.max_uses,
        max_uses_per_user=body.max_uses_per_user,
        used_count=0,
        applicable_cycles=None,
        min_order_amount=body.threshold_amount or 0,
        min_quantity=0,
        max_discount_amount=body.max_discount_amount or 0,
        required_product_ids=None,
        required_group_ids=None,
        required_category_ids=None,
        allowed_user_ids=None,
        new_users_only=False,
        applies_to_renewals=True,
        is_stackable=False,
        is_enabled=True,
    )
    db.add(coupon)
    await db.flush()
    await db.refresh(coupon)

    mall_item = None
    if body.claim_method == "mall" or body.list_on_mall:
        mall_scope = CouponMallItemScope.PUBLIC
        if body.mall_scope == "agent_subordinates":
            mall_scope = CouponMallItemScope.AGENT_SUBORDINATES
        mall_item = CouponMallItem(
            promo_code_id=coupon.id,
            issuer_user_id=current_user.id,
            title=body.mall_title or body.description or coupon.code,
            subtitle=body.mall_subtitle,
            cover_image=body.mall_cover_image,
            description=body.description,
            points_price=body.points_price or 0,
            stock_total=body.stock_total or body.max_uses or 0,
            stock_remaining=body.stock_total or body.max_uses or 0,
            status=CouponMallItemStatus.ACTIVE,
            scope=mall_scope,
            sort_order=body.mall_sort_order,
            category=body.mall_category,
        )
        db.add(mall_item)
        await db.flush()

    title = "代理优惠券创建成功" if is_agent else "会员优惠券创建成功"
    content = f"优惠券 {coupon.code} 已创建"
    if issuer_type == "member":
        content += f"，已扣除 {point_cost} 积分"
    if body.claim_method == "mall" or body.list_on_mall:
        content += f"，已上架积分商城（库存 {body.stock_total or body.max_uses}，{body.points_price or 0} 积分领取）"
    await notification_service.safe_notify(
        db,
        user_id=current_user.id,
        category="promotion",
        title=title,
        content=content + "。",
        link="/promotion",
    )

    response_data = {"coupon": _coupon_to_dict(coupon)}
    if mall_item:
        response_data["mall_item"] = {
            "id": mall_item.id,
            "title": mall_item.title,
            "subtitle": mall_item.subtitle,
            "cover_image": mall_item.cover_image,
            "points_price": mall_item.points_price,
            "stock_total": mall_item.stock_total,
            "stock_remaining": mall_item.stock_remaining,
            "status": mall_item.status.value,
            "scope": mall_item.scope.value,
            "category": mall_item.category,
        }

    if issuer_type == "member":
        membership = await membership_service.get_or_create_membership(db, current_user.id)
        response_data["available_points"] = membership.available_points
    else:
        response_data["agent_balance"] = active_agent.agent_balance if active_agent else 0

    return ApiResponse(data=response_data)


@router.get("/mall/items", response_model=ApiResponse)
async def list_mall_items(
    limit: int = Query(50, ge=1, le=100),
    offset: int = Query(0, ge=0),
    scope: str | None = Query(None, description="public 或 agent_subordinates"),
    q: str | None = Query(None, description="标题/副标题/描述/分类/发行者搜索"),
    type: str | None = Query(None, description="fixed / threshold_fixed / percent"),
    min_points: int | None = Query(None, ge=0),
    max_points: int | None = Query(None, ge=0),
    is_hot: bool | None = Query(None),
    sort_by: str | None = Query(None, description="points / expires / sort_order"),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """列出积分商城中可领取的优惠券。

    公开商品：scope=public 或未指定时返回。
    代理专区：scope=agent_subordinates 时返回当前用户绑定代理上架的商品。
    """
    now = datetime.now(timezone.utc)

    # 基础可见条件
    base_filters = [
        CouponMallItem.status == CouponMallItemStatus.ACTIVE,
        CouponMallItem.stock_remaining > 0,
        PromoCode.is_enabled.is_(True),
        PromoCode.expires_at.is_(None) | (PromoCode.expires_at > now),
    ]

    if scope == "agent_subordinates":
        # 仅显示当前用户绑定代理上架的商品
        if not current_user.bound_agent_id:
            return ApiResponse(data={"items": [], "total": 0})
        base_filters.extend([
            CouponMallItem.scope == CouponMallItemScope.AGENT_SUBORDINATES,
            CouponMallItem.issuer_user_id == current_user.bound_agent_id,
        ])
    elif scope == "public":
        base_filters.append(CouponMallItem.scope == CouponMallItemScope.PUBLIC)
    else:
        # 公开商品 + 代理专区商品也对普通用户可见（若用户未绑定代理则只看公开）
        public_filter = CouponMallItem.scope == CouponMallItemScope.PUBLIC
        if current_user.bound_agent_id:
            public_filter = public_filter | (
                (CouponMallItem.scope == CouponMallItemScope.AGENT_SUBORDINATES) &
                (CouponMallItem.issuer_user_id == current_user.bound_agent_id)
            )
        base_filters.append(public_filter)

    if q:
        keyword = f"%{q}%"
        base_filters.append(
            (CouponMallItem.title.ilike(keyword)) |
            (CouponMallItem.subtitle.ilike(keyword)) |
            (CouponMallItem.category.ilike(keyword)) |
            (PromoCode.description.ilike(keyword)) |
            (User.nickname.ilike(keyword)) |
            (User.username.ilike(keyword))
        )
    if type:
        base_filters.append(PromoCode.type == type)
    if min_points is not None:
        base_filters.append(CouponMallItem.points_price >= min_points)
    if max_points is not None:
        base_filters.append(CouponMallItem.points_price <= max_points)
    if is_hot is not None:
        base_filters.append(CouponMallItem.is_hot == is_hot)

    order_clause = [CouponMallItem.sort_order.desc(), CouponMallItem.id.desc()]
    if sort_by == "points":
        order_clause = [CouponMallItem.points_price.asc(), CouponMallItem.id.desc()]
    elif sort_by == "expires":
        order_clause = [PromoCode.expires_at.asc().nullslast(), CouponMallItem.id.desc()]
    elif sort_by == "sort_order":
        order_clause = [CouponMallItem.sort_order.desc(), CouponMallItem.id.desc()]

    total = await db.scalar(
        select(func.count(CouponMallItem.id))
        .join(PromoCode, PromoCode.id == CouponMallItem.promo_code_id)
        .outerjoin(User, User.id == CouponMallItem.issuer_user_id)
        .where(*base_filters)
    )
    result = await db.execute(
        select(CouponMallItem, PromoCode)
        .join(PromoCode, PromoCode.id == CouponMallItem.promo_code_id)
        .outerjoin(User, User.id == CouponMallItem.issuer_user_id)
        .where(*base_filters)
        .order_by(*order_clause)
        .limit(limit)
        .offset(offset)
    )

    items = []
    for mall_item, coupon in result.all():
        claimed = await db.scalar(
            select(func.count(CouponMallClaim.id)).where(
                CouponMallClaim.mall_item_id == mall_item.id,
                CouponMallClaim.user_id == current_user.id,
                CouponMallClaim.status == CouponMallClaimStatus.CLAIMED,
            )
        )
        issuer = await db.get(User, coupon.issuer_user_id) if coupon.issuer_user_id else None
        items.append({
            "id": mall_item.id,
            "promo_code_id": coupon.id,
            "title": mall_item.title,
            "subtitle": mall_item.subtitle,
            "cover_image": mall_item.cover_image,
            "description": coupon.description,
            "issuer_user_id": coupon.issuer_user_id,
            "issuer_name": issuer.nickname or issuer.username if issuer else "平台",
            "issuer_type": coupon.issuer_type,
            "points_price": mall_item.points_price,
            "stock_remaining": mall_item.stock_remaining,
            "scope": _enum_value(mall_item.scope),
            "category": mall_item.category,
            "max_claims_per_user": mall_item.max_claims_per_user,
            "max_claims_per_user_per_day": mall_item.max_claims_per_user_per_day,
            "required_member_level": mall_item.required_member_level,
            "required_member_level_name": mall_item.required_member_level_name,
            "is_hot": mall_item.is_hot,
            "type": _enum_value(coupon.type),
            "value": coupon.value,
            "threshold_amount": coupon.min_order_amount,
            "max_discount_amount": coupon.max_discount_amount,
            "expires_at": coupon.expires_at.isoformat() if coupon.expires_at else None,
            "claimed": (claimed or 0) > 0,
        })

    return ApiResponse(data={"items": items, "total": int(total or 0)})


class MallClaimRequest(BaseModel):
    mall_item_id: int
    captcha: GeetestPayload | None = None


@router.post("/mall/claim", response_model=ApiResponse)
async def claim_mall_coupon(
    body: MallClaimRequest,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    await captcha_service.verify(body.captcha.model_dump() if body.captcha else None)
    """用户从积分商城领取优惠券。"""
    result = await db.execute(
        select(CouponMallItem, PromoCode)
        .join(PromoCode, PromoCode.id == CouponMallItem.promo_code_id)
        .where(
            CouponMallItem.id == body.mall_item_id,
            CouponMallItem.status == CouponMallItemStatus.ACTIVE,
        )
        .with_for_update(of=(CouponMallItem,))
    )
    row = result.first()
    if not row:
        raise HTTPException(status_code=404, detail="商城商品不存在")
    mall_item, coupon = row

    if mall_item.stock_remaining <= 0:
        raise HTTPException(status_code=400, detail="该优惠券已领完")
    if coupon.expires_at and coupon.expires_at <= datetime.now(timezone.utc):
        raise HTTPException(status_code=400, detail="该优惠券已过期")
    if not coupon.is_enabled:
        raise HTTPException(status_code=400, detail="该优惠券已停用")

    # 检查是否已领取未使用
    existing = await db.scalar(
        select(CouponMallClaim.id).where(
            CouponMallClaim.mall_item_id == mall_item.id,
            CouponMallClaim.user_id == current_user.id,
            CouponMallClaim.status == CouponMallClaimStatus.CLAIMED,
        )
    )
    if existing:
        raise HTTPException(status_code=400, detail="您已领取过该优惠券，请先使用")

    # 限购校验：每用户总次数
    if mall_item.max_claims_per_user > 0:
        total_claimed = await db.scalar(
            select(func.count(CouponMallClaim.id)).where(
                CouponMallClaim.mall_item_id == mall_item.id,
                CouponMallClaim.user_id == current_user.id,
                CouponMallClaim.status == CouponMallClaimStatus.CLAIMED,
            )
        )
        if total_claimed and total_claimed >= mall_item.max_claims_per_user:
            raise HTTPException(status_code=400, detail="您已达到该优惠券的领取上限")

    # 限购校验：每用户每日次数
    if mall_item.max_claims_per_user_per_day > 0:
        today = datetime.now(timezone.utc).date()
        today_claimed = await db.scalar(
            select(func.count(CouponMallClaim.id)).where(
                CouponMallClaim.mall_item_id == mall_item.id,
                CouponMallClaim.user_id == current_user.id,
                CouponMallClaim.status == CouponMallClaimStatus.CLAIMED,
                func.date(CouponMallClaim.created_at) == today,
            )
        )
        if today_claimed and today_claimed >= mall_item.max_claims_per_user_per_day:
            raise HTTPException(status_code=400, detail="您今日已领取过该优惠券，明天再来吧")

    # 会员等级门槛
    if mall_item.required_member_level > 0:
        membership = await membership_service.get_or_create_membership(db, current_user.id)
        if (membership.level or 0) < mall_item.required_member_level:
            level_name = mall_item.required_member_level_name or f"LV{mall_item.required_member_level}"
            raise HTTPException(status_code=403, detail=f"该优惠券仅限 {level_name} 及以上会员领取")

    # 扣积分
    points_price = mall_item.points_price
    if points_price > 0:
        try:
            await membership_service.add_points(
                db=db,
                user_id=current_user.id,
                delta=-points_price,
                change_type=PointChangeType.ACTIVITY_GIFT,
                ref_no=coupon.code,
                remark=f"积分商城领取优惠券 {coupon.code} 扣除 {points_price} 积分",
            )
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc)) from exc

    mall_item.stock_remaining -= 1

    claim = CouponMallClaim(
        mall_item_id=mall_item.id,
        promo_code_id=coupon.id,
        user_id=current_user.id,
        points_paid=points_price,
        code=coupon.code,
        status=CouponMallClaimStatus.CLAIMED,
    )
    db.add(claim)
    await db.flush()
    await db.refresh(claim)

    membership = await membership_service.get_or_create_membership(db, current_user.id)
    return ApiResponse(data={
        "claim_id": claim.id,
        "code": coupon.code,
        "points_paid": points_price,
        "available_points": membership.available_points,
    })


@router.get("/mall/my-claims", response_model=ApiResponse)
async def list_my_mall_claims(
    status: str | None = Query(None),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """列出当前用户从商城领取的优惠券。"""
    stmt = (
        select(CouponMallClaim, PromoCode, CouponMallItem)
        .join(PromoCode, PromoCode.id == CouponMallClaim.promo_code_id)
        .join(CouponMallItem, CouponMallItem.promo_code_id == PromoCode.id)
        .where(CouponMallClaim.user_id == current_user.id)
        .order_by(CouponMallClaim.id.desc())
    )
    if status:
        stmt = stmt.where(CouponMallClaim.status == status)
    result = await db.execute(stmt)
    items = []
    for claim, coupon, mall_item in result.all():
        title = mall_item.title or coupon.description or None
        issuer = await db.get(User, coupon.issuer_user_id) if coupon.issuer_user_id else None
        items.append({
            "id": claim.id,
            "mall_item_id": claim.mall_item_id,
            "promo_code_id": claim.promo_code_id,
            "title": title,
            "subtitle": mall_item.subtitle,
            "cover_image": mall_item.cover_image,
            "description": coupon.description,
            "category": mall_item.category,
            "points_price": mall_item.points_price,
            "stock_remaining": mall_item.stock_remaining,
            "scope": _enum_value(mall_item.scope),
            "max_claims_per_user": mall_item.max_claims_per_user,
            "max_claims_per_user_per_day": mall_item.max_claims_per_user_per_day,
            "required_member_level": mall_item.required_member_level,
            "required_member_level_name": mall_item.required_member_level_name,
            "is_hot": mall_item.is_hot,
            "issuer_name": issuer.nickname or issuer.username if issuer else "平台",
            "points_paid": claim.points_paid,
            "status": claim.status.value,
            "used_at": claim.used_at.isoformat() if claim.used_at else None,
            "order_id": claim.order_id,
            "type": _enum_value(coupon.type),
            "value": coupon.value,
            "threshold_amount": coupon.min_order_amount,
            "max_discount_amount": coupon.max_discount_amount,
            "expires_at": coupon.expires_at.isoformat() if coupon.expires_at else None,
        })
    return ApiResponse(data={"items": items})


@router.post("/coupons/{coupon_id}/disable", response_model=ApiResponse)
async def disable_promotion_coupon(
    coupon_id: int,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(PromoCode)
        .where(PromoCode.id == coupon_id, PromoCode.issuer_user_id == current_user.id)
        .with_for_update()
    )
    coupon = result.scalar_one_or_none()
    if not coupon:
        raise HTTPException(status_code=404, detail="优惠券不存在")

    if getattr(coupon, "issuer_type", None) == "member":
        refund_points = await promotion_coupon_service.refund_unused_member_coupon(db, coupon, reason="停用")
    else:
        coupon.is_enabled = False
        refund_points = 0

    # 如果该券在积分商城上架，同步停用
    mall_result = await db.execute(
        select(CouponMallItem).where(
            CouponMallItem.promo_code_id == coupon.id,
            CouponMallItem.status == CouponMallItemStatus.ACTIVE,
        )
    )
    mall_item = mall_result.scalar_one_or_none()
    if mall_item:
        mall_item.status = CouponMallItemStatus.PAUSED

    await db.flush()
    await db.refresh(coupon)

    response_data = {
        "coupon": _coupon_to_dict(coupon),
        "refunded_points": refund_points,
    }
    if coupon.issuer_type == "member":
        membership = await membership_service.get_or_create_membership(db, current_user.id)
        response_data["available_points"] = membership.available_points
    else:
        agent = await agent_service.get_user_agent(db, current_user.id)
        response_data["agent_balance"] = agent.agent_balance if agent else 0

    return ApiResponse(data=response_data)


@router.get("/rewards", response_model=ApiResponse)
async def get_promotion_rewards(
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    settlement_limit = limit
    settlement_res = await db.execute(
        select(AgentSettlement)
        .where(
            AgentSettlement.user_id == current_user.id,
            AgentSettlement.settlement_type == "member_commission",
        )
        .order_by(AgentSettlement.created_at.desc())
        .limit(settlement_limit)
        .offset(offset)
    )
    settlements = list(settlement_res.scalars().all())
    settlement_log_ids_res = await db.execute(
        select(AgentSettlement.point_log_id).where(
            AgentSettlement.user_id == current_user.id,
            AgentSettlement.settlement_type == "member_commission",
            AgentSettlement.point_log_id.is_not(None),
        )
    )
    settlement_log_ids = {int(log_id) for log_id in settlement_log_ids_res.scalars().all() if log_id}

    legacy_stmt = (
        select(PointLog)
        .where(
            PointLog.user_id == current_user.id,
            PointLog.change_type == PointChangeType.REFERRAL_REWARD,
        )
        .order_by(PointLog.created_at.desc())
        .limit(limit)
        .offset(offset)
    )
    if settlement_log_ids:
        legacy_stmt = legacy_stmt.where(PointLog.id.not_in(settlement_log_ids))
    legacy_res = await db.execute(legacy_stmt)
    data = [_settlement_reward_to_dict(item) for item in settlements]
    data.extend(_legacy_reward_log_to_dict(log) for log in legacy_res.scalars().all())
    data.sort(key=lambda item: item.get("created_at") or "", reverse=True)
    return ApiResponse(data=data[:limit])
