"""排行榜奖励 — 俱乐部分榜结算、懒结算、领取入账（严格幂等）。"""
from __future__ import annotations

import logging
from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Optional

from django.db import IntegrityError, transaction
from django.db.models import F, Sum, Value
from django.db.models.functions import Coalesce
from django.utils import timezone

from backend.models import (
    LeaderDailyStats,
    ManagerDailyStats,
    MerchantDailyStats,
    PlayerDailyStats,
)
from jituan.services.club_user import get_user_club_id
from rank.reward_models import (
    RankRewardClaim,
    RankRewardClaimLog,
    RankRewardScheme,
    RankRewardSettlement,
    RankRewardTier,
)
from users.business_models import User
from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang

logger = logging.getLogger(__name__)

MAX_RANK = 50

RIQI_TO_PERIOD = {
    '昨日': RankRewardScheme.PERIOD_DAY,
    '上周': RankRewardScheme.PERIOD_WEEK,
    '上月': RankRewardScheme.PERIOD_MONTH,
}
OPEN_RIQI = frozenset({'今日', '本周', '本月', '总榜'})

OPEN_RIQI_PERIOD_MAP = {
    '今日': RankRewardScheme.PERIOD_DAY,
    '本周': RankRewardScheme.PERIOD_WEEK,
    '本月': RankRewardScheme.PERIOD_MONTH,
}

DEFAULT_SORT_FIELD = {
    'dashou': 'chengjiao_zongliang',
    'guanshi': 'chongzhi_dashou_shu',
    'zuzhang': 'shouru_zonge',
    'shangjia': 'jiesuan_jine',
}

# 总榜：管事=邀请打手总人数；组长=管事+打手身份邀请去重总人数（其它周期不变）
TOTAL_RANK_INVITE_SORT_FIELDS = {
    'guanshi': 'yaoqing_dashou_shu',
    'zuzhang': 'yaoqing_zongren_shu',
}
TOTAL_RANK_INVITE_SORT_LABEL = '邀请总人数'

# shenfen -> sort_field_key -> (model, id_field, db_field, is_int)
SORT_FIELD_REGISTRY = {
    'dashou': {
        'chengjiao_zongliang': (PlayerDailyStats, 'PlayerID', 'CompletedOrderTotal', True),
        'chengjiao_zonge': (PlayerDailyStats, 'PlayerID', 'CompletedAmount', False),
        'jiedan_zongliang': (PlayerDailyStats, 'PlayerID', 'AcceptedOrderTotal', True),
        'jiedan_zonge': (PlayerDailyStats, 'PlayerID', 'AcceptedAmount', False),
    },
    'guanshi': {
        'chongzhi_dashou_shu': (ManagerDailyStats, 'ManagerID', 'RechargedPlayerCount', True),
        'shouru_zonge': (ManagerDailyStats, 'ManagerID', 'TotalIncome', False),
        'yaoqing_dashou_shu': (ManagerDailyStats, 'ManagerID', 'InvitedPlayerCount', True),
    },
    'zuzhang': {
        'yaoqing_guanshi_shu': (LeaderDailyStats, 'LeaderID', 'InvitedManagerCount', True),
        'shouru_zonge': (LeaderDailyStats, 'LeaderID', 'TotalIncome', False),
        'fenyong_jine': (LeaderDailyStats, 'LeaderID', 'CommissionAmount', False),
    },
    'shangjia': {
        'jiesuan_dingdan_shu': (MerchantDailyStats, 'MerchantID', 'SettledOrderCount', True),
        'jiesuan_jine': (MerchantDailyStats, 'MerchantID', 'SettledAmount', False),
        'paifa_dingdan_shu': (MerchantDailyStats, 'MerchantID', 'AssignedOrderCount', True),
        'paifa_jine': (MerchantDailyStats, 'MerchantID', 'AssignedAmount', False),
    },
}

SORT_FIELD_LABELS = {
    'chengjiao_zongliang': '成交量',
    'chengjiao_zonge': '成交总额',
    'jiedan_zongliang': '接单量',
    'jiedan_zonge': '接单总额',
    'chongzhi_dashou_shu': '有效人数',
    'shouru_zonge': '收益总额',
    'yaoqing_dashou_shu': '邀请打手数',
    'yaoqing_guanshi_shu': '邀请管事数',
    'yaoqing_zongren_shu': '邀请总人数',
    'fenyong_jine': '分佣金额',
    'jiesuan_dingdan_shu': '结算单量',
    'jiesuan_jine': '结算金额',
    'paifa_dingdan_shu': '派单量',
    'paifa_jine': '派单流水',
}

BALANCE_FIELD_MAP = {
    'dashou': 'yue',
    'guanshi': 'yue',
    'zuzhang': 'ketixian_jine',
    'shangjia': 'yue',
}


def _scheme_activity_bounds(scheme) -> tuple[Optional[date], Optional[date]]:
    if not scheme:
        return None, None
    return scheme.activity_start, scheme.activity_end


def scheme_activity_open_today(scheme) -> tuple[bool, str]:
    """当前日期是否在活动开关窗口内（用于领取校验）。"""
    if not scheme or not scheme.enabled:
        return False, '奖励方案未启用'
    today = date.today()
    start, end = _scheme_activity_bounds(scheme)
    if start and today < start:
        return False, f'活动将于 {start.isoformat()} 开始'
    if end and today > end:
        return False, f'活动已于 {end.isoformat()} 结束'
    return True, ''


def period_overlaps_activity(scheme, period_start: date, period_end: date) -> bool:
    """统计周期与活动窗口有交集才允许结算/展示可领。"""
    if not scheme or not scheme.enabled:
        return False
    start, end = _scheme_activity_bounds(scheme)
    if not start and not end:
        return True
    if start and period_end < start:
        return False
    if end and period_start > end:
        return False
    return True


def serialize_scheme_activity(scheme) -> dict:
    if not scheme:
        return {
            'activity_start': '',
            'activity_end': '',
            'activity_open': False,
            'activity_reason': '未配置方案',
        }
    open_today, reason = scheme_activity_open_today(scheme)
    return {
        'activity_start': scheme.activity_start.isoformat() if scheme.activity_start else '',
        'activity_end': scheme.activity_end.isoformat() if scheme.activity_end else '',
        'activity_open': open_today,
        'activity_reason': reason,
    }


def _write_claim_log(claim, yonghuid: str, action: str, amount, detail: str = ''):
    RankRewardClaimLog.objects.create(
        claim_id=claim.id,
        yonghuid=yonghuid,
        action=action,
        amount=amount,
        detail=(detail or '')[:256],
    )


def sort_field_options_for_shenfen(shenfen: str) -> list:
    reg = SORT_FIELD_REGISTRY.get(shenfen) or {}
    return [
        {'key': k, 'label': SORT_FIELD_LABELS.get(k, k)}
        for k in reg.keys()
    ]


def resolve_sort_field_for_display(club_id: str, shenfen: str, riqi: str) -> tuple[str, str]:
    """按后台奖励方案决定展示/排序字段（方案存在即生效，与是否启用奖励无关）。"""
    if riqi == '总榜' and shenfen in TOTAL_RANK_INVITE_SORT_FIELDS:
        sf = TOTAL_RANK_INVITE_SORT_FIELDS[shenfen]
        return sf, TOTAL_RANK_INVITE_SORT_LABEL
    period_type = RIQI_TO_PERIOD.get(riqi) or OPEN_RIQI_PERIOD_MAP.get(riqi)
    sort_field = None
    if period_type:
        scheme = RankRewardScheme.objects.filter(
            club_id=club_id, shenfen=shenfen, period_type=period_type,
        ).first()
        if scheme:
            reg = SORT_FIELD_REGISTRY.get(shenfen) or {}
            if scheme.sort_field in reg:
                sort_field = scheme.sort_field
    if not sort_field:
        sort_field = DEFAULT_SORT_FIELD.get(shenfen, 'chengjiao_zongliang')
    return sort_field, SORT_FIELD_LABELS.get(sort_field, sort_field)


def club_member_uids(club_id: str) -> list:
    """仅统计归属该俱乐部的用户（未开业/无用户则空榜）。"""
    if not club_id:
        return []
    return list(
        User.objects.filter(ClubID=club_id).values_list('UserUID', flat=True)
    )


def filter_rank_rows_by_user_club(rows: list, club_id: str) -> list:
    if not rows or not club_id:
        return rows or []
    club_map = load_user_club_map([r['yonghuid'] for r in rows])
    return [r for r in rows if club_map.get(r['yonghuid']) == club_id]


def resolve_period_dates(riqi: str) -> Optional[tuple[date, date, str]]:
    today = date.today()
    if riqi == '昨日':
        d = today - timedelta(days=1)
        return d, d, f'day:{d.isoformat()}'
    if riqi == '上周':
        this_monday = today - timedelta(days=today.weekday())
        last_sunday = this_monday - timedelta(days=1)
        last_monday = last_sunday - timedelta(days=6)
        iso = last_monday.isocalendar()
        return last_monday, last_sunday, f'week:{iso[0]}-W{iso[1]:02d}'
    if riqi == '上月':
        first_this = today.replace(day=1)
        last_day = first_this - timedelta(days=1)
        first_prev = last_day.replace(day=1)
        return first_prev, last_day, f'month:{first_prev.strftime("%Y-%m")}'
    return None


def _resolve_date_range_for_display(riqi: str):
    """与 paihang_views 一致，用于集团/俱乐部展示榜。"""
    today = date.today()
    if riqi == '今日':
        return today, today
    if riqi == '昨日':
        d = today - timedelta(days=1)
        return d, d
    if riqi == '本周':
        monday = today - timedelta(days=today.weekday())
        return monday, today
    if riqi == '上周':
        this_monday = today - timedelta(days=today.weekday())
        last_sunday = this_monday - timedelta(days=1)
        last_monday = last_sunday - timedelta(days=6)
        return last_monday, last_sunday
    if riqi == '本月':
        return today.replace(day=1), today
    if riqi == '上月':
        first_this = today.replace(day=1)
        last_day = first_this - timedelta(days=1)
        return last_day.replace(day=1), last_day
    return None


def _is_total_rank_invite_sort(riqi: str, shenfen: str, sort_field: str) -> bool:
    return (
        riqi == '总榜'
        and shenfen in TOTAL_RANK_INVITE_SORT_FIELDS
        and sort_field == TOTAL_RANK_INVITE_SORT_FIELDS[shenfen]
    )


def _query_zuzhang_total_unique_invite_rows(club_id: str = None, limit: int = MAX_RANK) -> list:
    """
    组长总榜：组长身份邀请的管事 + 管事身份邀请的打手，按 UserUID 去重计数。
    """
    from users.models import UserDashou, UserGuanshi, UserZuzhang

    zuzhang_uids = set(
        UserZuzhang.objects.filter(zhuangtai=1).values_list('user__UserUID', flat=True)
    )
    if club_id:
        zuzhang_uids &= set(club_member_uids(club_id))
    if not zuzhang_uids:
        return []

    invite_sets = {}
    zuzhang_list = list(zuzhang_uids)

    def _add(inviter, invited_uid):
        if inviter not in zuzhang_uids or not invited_uid:
            return
        invite_sets.setdefault(inviter, set()).add(invited_uid)

    for inviter, invited_uid in UserGuanshi.objects.filter(
        yaoqingren__in=zuzhang_list,
    ).values_list('yaoqingren', 'user__UserUID'):
        _add(inviter, invited_uid)

    for inviter, invited_uid in UserDashou.objects.filter(
        yaoqingren__in=zuzhang_list,
    ).values_list('yaoqingren', 'user__UserUID'):
        _add(inviter, invited_uid)

    out = [
        {'yonghuid': uid, 'metric': len(invited), 'is_int': True}
        for uid, invited in invite_sets.items()
        if invited
    ]
    out.sort(key=lambda x: x['metric'], reverse=True)
    return out[:limit]


def query_total_rank_invite_rows(
    shenfen: str, club_id: str = None, limit: int = MAX_RANK,
) -> list:
    """总榜专用：管事/组长按邀请总人数排行。"""
    member_uids = club_member_uids(club_id) if club_id else None
    if club_id and not member_uids:
        return []
    fetch_limit = min(max(limit * 5, 100), 250)

    if shenfen == 'guanshi':
        model = ManagerDailyStats
        id_field = 'ManagerID'
        base_qs = model.objects.all()
        if club_id:
            base_qs = base_qs.filter(
                club_id=club_id, **{f'{id_field}__in': member_uids},
            )
        rows = (
            base_qs.values(id_field)
            .annotate(metric=Coalesce(Sum('InvitedPlayerCount'), Value(0)))
            .order_by('-metric')[:fetch_limit]
        )
        out = []
        for r in rows:
            uid = r[id_field]
            metric = int(r.get('metric') or 0)
            if metric <= 0:
                continue
            out.append({'yonghuid': uid, 'metric': metric, 'is_int': True})
        if club_id:
            out = filter_rank_rows_by_user_club(out, club_id)
        out.sort(key=lambda x: x['metric'], reverse=True)
        return out[:limit]

    if shenfen == 'zuzhang':
        return _query_zuzhang_total_unique_invite_rows(club_id=club_id, limit=limit)

    return []


def query_club_rank_rows(club_id: str, shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK):
    """按 club 归属用户 + 日统计 club_id 双过滤（分奖与展示唯一依据）。"""
    if _is_total_rank_invite_sort(riqi, shenfen, sort_field):
        return query_total_rank_invite_rows(shenfen, club_id=club_id, limit=limit)
    reg = SORT_FIELD_REGISTRY.get(shenfen, {}).get(sort_field)
    if not reg:
        return []
    model, id_field, db_field, is_int = reg

    member_uids = club_member_uids(club_id)
    if not member_uids:
        return []

    fetch_limit = min(max(limit * 5, 100), 250)

    def _pack(qs_or_rows, is_values=False):
        out = []
        if is_values:
            for r in qs_or_rows:
                uid = r[id_field]
                metric = r.get('metric') or 0
                if metric and uid in member_uids:
                    out.append({'yonghuid': uid, 'metric': metric, 'is_int': is_int})
        else:
            for r in qs_or_rows:
                uid = getattr(r, id_field)
                metric = getattr(r, db_field) or 0
                if metric and uid in member_uids:
                    out.append({'yonghuid': uid, 'metric': metric, 'is_int': is_int})
        out = filter_rank_rows_by_user_club(out, club_id)
        out.sort(key=lambda x: x['metric'], reverse=True)
        return out[:limit]

    if riqi == '总榜':
        rows = (
            model.objects.filter(club_id=club_id, **{f'{id_field}__in': member_uids})
            .values(id_field)
            .annotate(metric=Sum(db_field))
            .order_by(f'-metric')[:fetch_limit]
        )
        return _pack(rows, is_values=True)

    date_range = _resolve_date_range_for_display(riqi)
    if not date_range:
        return []
    start_date, end_date = date_range

    if start_date == end_date:
        qs = (
            model.objects.filter(
                club_id=club_id, Date=start_date, **{f'{id_field}__in': member_uids},
            )
            .order_by(f'-{db_field}')[:fetch_limit]
        )
        return _pack(qs, is_values=False)

    rows = (
        model.objects.filter(
            club_id=club_id,
            Date__gte=start_date,
            Date__lte=end_date,
            **{f'{id_field}__in': member_uids},
        )
        .values(id_field)
        .annotate(metric=Sum(db_field))
        .order_by('-metric')[:fetch_limit]
    )
    return _pack(rows, is_values=True)


def query_group_rank_rows(shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK):
    """集团总榜（仅展示，不过滤 club）。"""
    return _query_group_rank_rows(shenfen, sort_field, riqi, limit)


def _query_group_rank_rows(shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK):
    if _is_total_rank_invite_sort(riqi, shenfen, sort_field):
        return query_total_rank_invite_rows(shenfen, club_id=None, limit=limit)
    reg = SORT_FIELD_REGISTRY.get(shenfen, {}).get(sort_field)
    if not reg:
        return []
    model, id_field, db_field, is_int = reg

    if riqi == '总榜':
        rows = (
            model.objects.values(id_field)
            .annotate(metric=Sum(db_field))
            .order_by('-metric')[:limit]
        )
        return [
            {'yonghuid': r[id_field], 'metric': r['metric'] or 0, 'is_int': is_int}
            for r in rows if r.get('metric')
        ]

    date_range = _resolve_date_range_for_display(riqi)
    if not date_range:
        return []
    start_date, end_date = date_range

    if start_date == end_date:
        qs = model.objects.filter(Date=start_date).order_by(f'-{db_field}')[:limit]
        return [
            {'yonghuid': getattr(r, id_field), 'metric': getattr(r, db_field) or 0, 'is_int': is_int}
            for r in qs if getattr(r, db_field)
        ]

    rows = (
        model.objects.filter(Date__gte=start_date, Date__lte=end_date)
        .values(id_field)
        .annotate(metric=Sum(db_field))
        .order_by('-metric')[:limit]
    )
    return [
        {'yonghuid': r[id_field], 'metric': r['metric'] or 0, 'is_int': is_int}
        for r in rows if r.get('metric')
    ]


def _fmt_stat_value(val, is_int: bool):
    if val is None:
        return 0 if is_int else 0.0
    if is_int:
        return int(val)
    if isinstance(val, Decimal):
        return float(val.quantize(Decimal('0.01')))
    return float(val)


def _enrich_today_profiles(shenfen: str, yonghuids: list) -> dict:
    """今日日统计为空时，从 Profile 补全展示字段（与 phbhqsj 回退一致）。"""
    from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang
    from users.paihang_views import ROLE_CONFIG

    cfg = ROLE_CONFIG[shenfen]
    response_field_map = cfg['response_field_map']
    int_fields = cfg['int_fields']
    id_field = cfg['id_field']

    profile_cfg = {
        'dashou': (UserDashou, {
            'AcceptedOrderTotal': 'jinrijiedan',
            'CompletedOrderTotal': 'jinrijiedan',
            'AcceptedAmount': 'jinrishouyi',
            'CompletedAmount': 'jinrishouyi',
        }),
        'guanshi': (UserGuanshi, {
            'InvitedPlayerCount': 'yaogingshuliang',
            'RechargedPlayerCount': 'jinrichongzhi',
            'TotalIncome': 'chongzhifenrun',
        }),
        'zuzhang': (UserZuzhang, {
            'InvitedManagerCount': 'yaoqing_zongshu',
            'CommissionAmount': 'jinri_fenyong',
            'TotalIncome': 'fenyong_zonge',
        }),
        'shangjia': (UserShangjia, {
            'AssignedOrderCount': 'jinridingdan',
            'AssignedAmount': 'jinriliushui',
            'SettledOrderCount': None,
            'SettledAmount': None,
        }),
    }
    model_cls, field_map = profile_cfg.get(shenfen, (None, {}))
    if not model_cls:
        return {}

    out = {}
    qs = model_cls.objects.filter(user__UserUID__in=yonghuids).select_related('user')
    for p in qs:
        uid = p.user.UserUID
        row = {}
        for db_field, profile_field in field_map.items():
            key = response_field_map.get(db_field, db_field)
            if profile_field:
                val = getattr(p, profile_field, 0) or 0
            else:
                val = 0
            row[key] = _fmt_stat_value(val, db_field in int_fields)
        out[uid] = row
    return out


def enrich_rank_stats(shenfen: str, yonghuids: list, riqi: str, club_id: str = '') -> dict:
    """加载榜单行完整统计（排序指标 + 其它单量/金额），避免前端只看到一个数。"""
    if not yonghuids:
        return {}
    from users.paihang_views import ROLE_CONFIG

    cfg = ROLE_CONFIG[shenfen]
    model = cfg['model']
    id_field = cfg['id_field']
    fields = cfg['fields']
    int_fields = cfg['int_fields']
    response_field_map = cfg['response_field_map']

    base_filter = {f'{id_field}__in': yonghuids}
    if club_id and hasattr(model, 'club_id'):
        base_filter['club_id'] = club_id

    result = {uid: {} for uid in yonghuids}

    def _finish():
        from rank.display_utils import enrich_extra_rank_fields
        enrich_extra_rank_fields(shenfen, result, riqi)
        return result

    def _fill_from_row(uid, row_data):
        for f in fields:
            key = response_field_map.get(f, f)
            result[uid][key] = _fmt_stat_value(row_data.get(f), f in int_fields)

    if riqi == '总榜':
        rows = (
            model.objects.filter(**base_filter)
            .values(id_field)
            .annotate(**{f: Sum(f) for f in fields})
        )
        for r in rows:
            uid = r[id_field]
            if uid in result:
                _fill_from_row(uid, r)
        return _finish()

    date_range = _resolve_date_range_for_display(riqi)
    if not date_range:
        return _finish()
    start_date, end_date = date_range

    if start_date == end_date:
        qs = model.objects.filter(Date=start_date, **base_filter)
        if qs.exists():
            for r in qs:
                uid = getattr(r, id_field)
                if uid not in result:
                    continue
                row_data = {f: getattr(r, f) for f in fields}
                _fill_from_row(uid, row_data)
            return _finish()
        if riqi == '今日':
            today = _enrich_today_profiles(shenfen, yonghuids)
            for uid, row in today.items():
                result[uid] = {**result.get(uid, {}), **row}
            return _finish()
        return _finish()

    rows = (
        model.objects.filter(Date__gte=start_date, Date__lte=end_date, **base_filter)
        .values(id_field)
        .annotate(**{f: Sum(f) for f in fields})
    )
    for r in rows:
        uid = r[id_field]
        if uid in result:
            _fill_from_row(uid, r)
    return _finish()


def load_user_club_map(yonghuids: list) -> dict:
    if not yonghuids:
        return {}
    users = User.objects.filter(UserUID__in=yonghuids)
    return {u.UserUID: get_user_club_id(u) or getattr(u, 'ClubID', None) or '' for u in users}


def user_has_active_shenfen(yonghuid: str, shenfen: str) -> bool:
    if shenfen == 'dashou':
        return UserDashou.objects.filter(user__UserUID=yonghuid, zhanghaozhuangtai=1).exists()
    if shenfen == 'guanshi':
        return UserGuanshi.objects.filter(user__UserUID=yonghuid, zhuangtai=1).exists()
    if shenfen == 'zuzhang':
        return UserZuzhang.objects.filter(user__UserUID=yonghuid, zhuangtai=1).exists()
    if shenfen == 'shangjia':
        return UserShangjia.objects.filter(user__UserUID=yonghuid, zhuangtai=1).exists()
    return False


def tier_amount_for_rank(tiers, mingci: int) -> Optional[Decimal]:
    for t in tiers:
        if t.rank_from <= mingci <= t.rank_to:
            return t.reward_amount
    return None


def build_tier_rewards_preview(scheme) -> dict:
    """按方案档位生成各名次预计奖金（进行中周期展示用）。"""
    if not scheme:
        return {}
    tiers = list(RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from'))
    if not tiers:
        return {}
    preview = {}
    for mingci in range(1, MAX_RANK + 1):
        amt = tier_amount_for_rank(tiers, mingci)
        if amt and amt > 0:
            preview[mingci] = float(amt)
    return preview


def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional[RankRewardSettlement]:
    """懒结算：仅已结束周期；幂等。"""
    if riqi in OPEN_RIQI:
        return None
    period_type = RIQI_TO_PERIOD.get(riqi)
    if not period_type:
        return None

    period_info = resolve_period_dates(riqi)
    if not period_info:
        return None
    period_start, period_end, period_key = period_info

    scheme = RankRewardScheme.query.filter(
        club_id=club_id, shenfen=shenfen, period_type=period_type, enabled=True,
    ).first()
    if not scheme:
        return None

    if not period_overlaps_activity(scheme, period_start, period_end):
        logger.info(
            'skip settle: period %s-%s outside activity window scheme=%s',
            period_start, period_end, scheme.id,
        )
        return None

    if scheme.sort_field not in (SORT_FIELD_REGISTRY.get(shenfen) or {}):
        logger.warning('invalid sort_field %s for %s', scheme.sort_field, shenfen)
        return None

    existing = RankRewardSettlement.query.filter(
        club_id=club_id, scheme_id=scheme.id, period_key=period_key,
    ).first()
    if existing:
        return existing

    rows = query_club_rank_rows(club_id, shenfen, scheme.sort_field, riqi)
    tiers = list(RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from'))
    if not tiers:
        return None

    min_metric = scheme.min_metric or Decimal('0')
    _all_uids = [r['yonghuid'] for r in rows]
    club_map = load_user_club_map(_all_uids)
    expire_at = timezone.now() + timedelta(days=scheme.claim_days or 30)
    balance_field = BALANCE_FIELD_MAP[shenfen]
    claims_to_create = []

    # 批量预取活跃用户集合，避免循环内 exists 查询
    if shenfen == 'dashou':
        _active_uids = set(
            UserDashou.objects.filter(user__UserUID__in=_all_uids, zhanghaozhuangtai=1)
            .values_list('user__UserUID', flat=True)
        )
    elif shenfen == 'guanshi':
        _active_uids = set(
            UserGuanshi.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
            .values_list('user__UserUID', flat=True)
        )
    elif shenfen == 'zuzhang':
        _active_uids = set(
            UserZuzhang.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
            .values_list('user__UserUID', flat=True)
        )
    elif shenfen == 'shangjia':
        _active_uids = set(
            UserShangjia.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
            .values_list('user__UserUID', flat=True)
        )
    else:
        _active_uids = set()

    for idx, row in enumerate(rows):
        mingci = idx + 1
        metric = Decimal(str(row['metric'] or 0))
        if metric < min_metric:
            continue
        amount = tier_amount_for_rank(tiers, mingci)
        if not amount or amount <= 0:
            continue
        uid = row['yonghuid']
        if club_map.get(uid) != club_id:
            continue
        if uid not in _active_uids:
            continue
        idem = f'{club_id}|{scheme.id}|{period_key}|{uid}'
        claims_to_create.append(RankRewardClaim(
            club_id=club_id,
            yonghuid=uid,
            shenfen=shenfen,
            mingci=mingci,
            metric_value=metric,
            reward_amount=amount,
            status=RankRewardClaim.STATUS_PENDING,
            expire_at=expire_at,
            balance_field=balance_field,
            idempotent_key=idem,
        ))

    try:
        with transaction.atomic():
            settlement = RankRewardSettlement.query.create(
                scheme_id=scheme.id,
                club_id=club_id,
                period_key=period_key,
                period_start=period_start,
                period_end=period_end,
                riqi_label=riqi,
                sort_field=scheme.sort_field,
                claim_count=0,
            )
            # 批量创建 claims，用 ignore_conflicts 处理幂等冲突
            if claims_to_create:
                for claim in claims_to_create:
                    claim.settlement_id = settlement.id
                RankRewardClaim.objects.bulk_create(claims_to_create, ignore_conflicts=True)
                created = RankRewardClaim.objects.filter(settlement_id=settlement.id).count()
            else:
                created = 0
            settlement.claim_count = created
            settlement.save(update_fields=['claim_count'])
            return settlement
    except IntegrityError:
        return RankRewardSettlement.query.filter(
            club_id=club_id, scheme_id=scheme.id, period_key=period_key,
        ).first()


def build_reward_payload(club_id: str, shenfen: str, riqi: str, yonghuid: str = '') -> dict:
    """奖励规则 + 我的待领取/已领取状态。"""
    period_type = RIQI_TO_PERIOD.get(riqi) or OPEN_RIQI_PERIOD_MAP.get(riqi)
    display_sort, display_label = resolve_sort_field_for_display(club_id, shenfen, riqi)
    scheme = None
    if period_type:
        scheme = RankRewardScheme.query.filter(
            club_id=club_id, shenfen=shenfen, period_type=period_type,
        ).first()

    settlement = None
    if scheme and riqi not in OPEN_RIQI:
        maybe_settle_club_rewards(club_id, shenfen, riqi)
        period_info = resolve_period_dates(riqi)
        if period_info:
            _, _, period_key = period_info
            settlement = RankRewardSettlement.query.filter(
                club_id=club_id, scheme_id=scheme.id, period_key=period_key,
            ).first()

    tiers = []
    if scheme:
        tiers = [
            {
                'rank_from': t.rank_from,
                'rank_to': t.rank_to,
                'amount': float(t.reward_amount),
                'label': t.label or '',
            }
            for t in RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')
        ]

    rank_rewards = {}
    if scheme and settlement:
        for c in RankRewardClaim.query.filter(settlement_id=settlement.id):
            rank_rewards[c.mingci] = float(c.reward_amount)

    tier_rewards = build_tier_rewards_preview(scheme) if scheme else {}

    my_claim = None
    my_claimed = None
    if yonghuid and settlement:
        pending = RankRewardClaim.query.filter(
            settlement_id=settlement.id, yonghuid=yonghuid, status=RankRewardClaim.STATUS_PENDING,
        ).first()
        if pending:
            if pending.expire_at < timezone.now():
                pending.status = RankRewardClaim.STATUS_EXPIRED
                pending.save(update_fields=['status', 'UpdateTime'])
            else:
                my_claim = {
                    'claim_id': pending.id,
                    'amount': float(pending.reward_amount),
                    'mingci': pending.mingci,
                    'status': 'pending',
                    'status_text': '待领取',
                    'expire_at': pending.expire_at.isoformat(),
                    'club_id': club_id,
                }
        claimed = RankRewardClaim.query.filter(
            settlement_id=settlement.id, yonghuid=yonghuid, status=RankRewardClaim.STATUS_CLAIMED,
        ).first()
        if claimed:
            my_claimed = {
                'claim_id': claimed.id,
                'amount': float(claimed.reward_amount),
                'mingci': claimed.mingci,
                'status': 'claimed',
                'status_text': '已领取',
                'claimed_at': claimed.claimed_at.isoformat() if claimed.claimed_at else '',
            }

    activity_meta = serialize_scheme_activity(scheme if scheme and scheme.enabled else None)
    if scheme and scheme.enabled and not activity_meta['activity_open']:
        # 活动窗口外：不展示待领取（已结算的 pending 也视为不可领）
        my_claim = None

    return {
        'club_id': club_id,
        'shenfen': shenfen,
        'riqi': riqi,
        'scheme_enabled': bool(scheme and scheme.enabled and activity_meta['activity_open']),
        'period_status': 'open' if riqi in OPEN_RIQI else 'closed',
        'period_label': riqi,
        'sort_field': (scheme.sort_field if scheme and scheme.enabled else display_sort),
        'sort_label': (
            SORT_FIELD_LABELS.get(scheme.sort_field, display_label)
            if scheme and scheme.enabled else display_label
        ),
        'title': scheme.title if scheme else '',
        'description': scheme.description if scheme else '',
        'tiers': tiers,
        'tier_rewards': tier_rewards,
        'rank_rewards': rank_rewards,
        'my_claim': my_claim,
        'my_claimed': my_claimed,
        **activity_meta,
    }


def list_my_pending_claims(yonghuid: str, club_id: str = '') -> list:
    qs = RankRewardClaim.query.filter(
        yonghuid=yonghuid, status=RankRewardClaim.STATUS_PENDING,
    ).order_by('-CreateTime')
    if club_id:
        qs = qs.filter(club_id=club_id)
    now = timezone.now()
    result = []
    for c in qs[:20]:
        if c.expire_at < now:
            c.status = RankRewardClaim.STATUS_EXPIRED
            c.save(update_fields=['status', 'UpdateTime'])
            continue
        result.append({
            'claim_id': c.id,
            'club_id': c.club_id,
            'shenfen': c.shenfen,
            'amount': float(c.reward_amount),
            'mingci': c.mingci,
            'status': 'pending',
            'status_text': '待领取',
            'expire_at': c.expire_at.isoformat(),
        })
    return result


def claim_reward(claim_id: int, user, request_shenfen: str = '') -> dict:
    uid = user.UserUID
    user_club = get_user_club_id(user) or getattr(user, 'ClubID', None) or ''
    req_sf = (request_shenfen or '').strip()

    with transaction.atomic():
        claim = RankRewardClaim.objects.select_for_update().filter(id=claim_id).first()
        if not claim:
            return {'ok': False, 'msg': '奖励记录不存在'}
        if claim.yonghuid != uid:
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, '非本人')
            return {'ok': False, 'msg': '只能领取本人的奖励'}
        if req_sf and req_sf != claim.shenfen:
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, '身份不匹配')
            return {'ok': False, 'msg': '请使用对应身份领取'}
        if claim.club_id != user_club:
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, '俱乐部不匹配')
            return {'ok': False, 'msg': '俱乐部不匹配，无法领取'}
        if claim.status == RankRewardClaim.STATUS_CLAIMED:
            return {'ok': True, 'msg': '已领取', 'amount': float(claim.reward_amount), 'duplicate': True}
        if claim.status != RankRewardClaim.STATUS_PENDING:
            return {'ok': False, 'msg': '奖励不可领取'}
        if claim.expire_at < timezone.now():
            claim.status = RankRewardClaim.STATUS_EXPIRED
            claim.save(update_fields=['status', 'UpdateTime'])
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, '已过期')
            return {'ok': False, 'msg': '奖励已过期'}

        if not user_has_active_shenfen(uid, claim.shenfen):
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, '身份未激活')
            return {'ok': False, 'msg': '身份状态异常，无法领取'}

        settlement = RankRewardSettlement.query.select_related('scheme').filter(id=claim.settlement_id).first()
        scheme = settlement.scheme if settlement else None
        if not scheme or not scheme.enabled:
            return {'ok': False, 'msg': '奖励方案未启用'}
        open_today, act_reason = scheme_activity_open_today(scheme)
        if not open_today:
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, act_reason)
            return {'ok': False, 'msg': act_reason or '不在活动有效期内'}
        if settlement and not period_overlaps_activity(scheme, settlement.period_start, settlement.period_end):
            _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_REJECT, claim.reward_amount, '周期不在活动内')
            return {'ok': False, 'msg': '该周期不在活动有效期内'}

        amount = claim.reward_amount
        _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_START, amount, '开始入账')

        _credit_balance(claim.shenfen, uid, amount, biz_id=f'rank_claim:{claim.id}')

        claim.status = RankRewardClaim.STATUS_CLAIMED
        claim.claimed_at = timezone.now()
        claim.save(update_fields=['status', 'claimed_at', 'UpdateTime'])
        _write_claim_log(claim, uid, RankRewardClaimLog.ACTION_SUCCESS, amount, claim.balance_field)

    return {
        'ok': True,
        'msg': '领取成功',
        'amount': float(amount),
        'shenfen': claim.shenfen,
        'balance_field': claim.balance_field,
    }


def _credit_balance(shenfen: str, yonghuid: str, amount: Decimal, biz_id: str = ''):
    amount = Decimal(str(amount))
    biz = (biz_id or '').strip() or f'rank:{shenfen}:{yonghuid}:{amount}'
    if shenfen == 'dashou':
        from jituan.services.fund_freeze import (
            credit_role_balance, settle_freeze_club_id, ROLE_DASHOU, SOURCE_FENHONG,
        )
        p = UserDashou.objects.select_for_update().get(user__UserUID=yonghuid)
        credit_role_balance(
            club_id=settle_freeze_club_id(user_id=str(yonghuid)),
            user_id=str(yonghuid),
            role=ROLE_DASHOU,
            amount=amount,
            source_type=SOURCE_FENHONG,
            biz_id=biz,
            profile=p,
            freeze_reason='排行榜奖励',
        )
    elif shenfen == 'guanshi':
        from jituan.services.fund_freeze import credit_guanshi_fenhong
        p = UserGuanshi.objects.select_for_update().get(user__UserUID=yonghuid)
        credit_guanshi_fenhong(
            user_id=str(yonghuid),
            amount=amount,
            biz_id=biz,
            profile=p,
            freeze_reason='排行榜奖励',
        )
    elif shenfen == 'zuzhang':
        from jituan.services.fund_freeze import credit_zuzhang_fenhong
        p = UserZuzhang.objects.select_for_update().get(user__UserUID=yonghuid)
        credit_zuzhang_fenhong(
            user_id=str(yonghuid),
            amount=amount,
            biz_id=biz,
            profile=p,
            freeze_reason='排行榜奖励',
        )
    elif shenfen == 'shangjia':
        p = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid)
        UserShangjia.objects.filter(pk=p.pk).update(yue=F('yue') + amount)
    else:
        raise ValueError(f'unsupported shenfen: {shenfen}')
