"""资金冻结入账网关与解冻骨架。

默认：无配置或 enabled=False → 全额进可提现，与现网一致，不写流水。
开启后：拆「可提现 + 冻结池」并写 FundFreezeLedger。

P1：打手订单结算入口已统一走 credit_dashou_order_settle（未开冻结时行为与改造前一致）。
"""
from __future__ import annotations

import logging
from datetime import timedelta
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Dict, Optional, Tuple

from django.core.cache import cache
from django.db.models import F
from django.utils import timezone

from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import ClubFundFreezeConfig, FundFreezeLedger

logger = logging.getLogger(__name__)

ROLE_DASHOU = 'dashou'
ROLE_GUANSHI = 'guanshi'
ROLE_ZUZHANG = 'zuzhang'
ROLE_SHENHEGUAN = 'shenheguan'

SOURCE_ORDER_SETTLE = 'order_settle'
SOURCE_FENHONG = 'fenhong'
SOURCE_KAOHE_FEE = 'kaohe_fee'
SOURCE_WITHDRAW_REJECT = 'withdraw_reject'
SOURCE_CLUB_MIGRATE = 'club_migrate'

_CFG_CACHE_TTL = 60


def _money(v) -> Decimal:
    return Decimal(str(v or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)


def _club(club_id: Optional[str]) -> str:
    return (club_id or '').strip() or CLUB_ID_DEFAULT


def get_freeze_config(club_id: Optional[str], role: str) -> Optional[ClubFundFreezeConfig]:
    cid = _club(club_id)
    role = (role or '').strip().lower()
    key = f'fund_freeze_cfg:{cid}:{role}'
    cached = cache.get(key)
    if cached is not None:
        return cached or None
    row = ClubFundFreezeConfig.query.filter(club_id=cid, role=role).first()
    cache.set(key, row or False, _CFG_CACHE_TTL)
    return row


def invalidate_freeze_config_cache(club_id: Optional[str] = None, role: Optional[str] = None):
    if club_id and role:
        cache.delete(f'fund_freeze_cfg:{_club(club_id)}:{(role or "").strip().lower()}')
        return
    # 粗粒度：调用方改配置后建议带 club+role 清；此处留空避免误扫


def calc_freeze_amount(credit: Decimal, cfg: ClubFundFreezeConfig) -> Decimal:
    credit = _money(credit)
    if credit <= 0:
        return Decimal('0.00')
    min_c = _money(cfg.min_credit_to_freeze)
    if min_c > 0 and credit < min_c:
        return Decimal('0.00')

    mode = int(cfg.freeze_mode or 1)
    frozen = Decimal('0.00')
    if mode == 2:
        frozen = _money(cfg.freeze_fixed)
    elif mode == 3:
        frozen = _money(credit * Decimal(str(cfg.freeze_rate or 0)))
        cap = _money(cfg.freeze_cap)
        if cap > 0:
            frozen = min(frozen, cap)
    else:
        frozen = _money(credit * Decimal(str(cfg.freeze_rate or 0)))
        cap = _money(cfg.freeze_cap)
        if cap > 0:
            frozen = min(frozen, cap)

    if frozen < 0:
        frozen = Decimal('0.00')
    if frozen > credit:
        frozen = credit
    return frozen


def _source_allowed(cfg: ClubFundFreezeConfig, source_type: str, role: str = '') -> bool:
    scope = cfg.source_scope
    if not scope:
        return True
    if isinstance(scope, str):
        items = [scope]
    else:
        try:
            items = list(scope)
        except TypeError:
            return True
    if source_type in items:
        return True
    # 管事/组长几乎没有 order_settle 入账；历史配置只勾了「订单结算」时仍允许分红/驳回退款冻结
    role = (role or '').strip().lower()
    if (
        role in (ROLE_GUANSHI, ROLE_ZUZHANG, ROLE_SHENHEGUAN)
        and source_type in (SOURCE_FENHONG, SOURCE_WITHDRAW_REJECT)
        and set(items) == {'order_settle'}
    ):
        return True
    return False


def _config_snapshot(cfg: ClubFundFreezeConfig) -> Dict[str, Any]:
    return {
        'club_id': cfg.club_id,
        'role': cfg.role,
        'enabled': cfg.enabled,
        'freeze_mode': cfg.freeze_mode,
        'freeze_rate': str(cfg.freeze_rate),
        'freeze_fixed': str(cfg.freeze_fixed),
        'freeze_cap': str(cfg.freeze_cap),
        'min_credit_to_freeze': str(cfg.min_credit_to_freeze),
        'source_scope': cfg.source_scope,
        'unfreeze_trigger': cfg.unfreeze_trigger,
        'schedule_mode': cfg.schedule_mode,
        'installment_json': cfg.installment_json,
        'condition_json': cfg.condition_json,
        'freeze_min_days': cfg.freeze_min_days,
        'display_reason': (cfg.remark or '')[:255],
        'remark': (cfg.remark or '')[:255],
    }


def _first_step_delay_days(cfg: ClubFundFreezeConfig) -> int:
    if (cfg.schedule_mode or '') == 'installment':
        steps = (cfg.installment_json or {}).get('steps') or []
        if steps:
            try:
                return max(0, int(steps[0].get('after_days') or 0))
            except (TypeError, ValueError):
                pass
    return max(0, int(cfg.freeze_min_days or 0))


def _add_withdrawable(role: str, profile, available: Decimal, also_zonge: bool = False):
    """原子增加可提现；组长字段为 ketixian_jine。"""
    available = _money(available)
    if available == 0:
        return
    if role == ROLE_ZUZHANG:
        type(profile).query.filter(pk=profile.pk).update(
            ketixian_jine=F('ketixian_jine') + available,
        )
    else:
        updates = {'yue': F('yue') + available}
        if also_zonge and hasattr(profile, 'zonge'):
            updates['zonge'] = F('zonge') + available
        type(profile).query.filter(pk=profile.pk).update(**updates)


def _add_dongjie(profile, frozen: Decimal):
    frozen = _money(frozen)
    if frozen == 0:
        return
    type(profile).query.filter(pk=profile.pk).update(
        dongjie_yue=F('dongjie_yue') + frozen,
    )


def credit_role_balance(
    *,
    club_id: Optional[str],
    user_id: str,
    role: str,
    amount,
    source_type: str,
    biz_id: str,
    profile=None,
    order_id: str = '',
    czjilu_id: str = '',
    freeze_reason: str = '',
    also_credit_zonge: bool = False,
    ignore_source_scope: bool = False,
) -> Dict[str, Any]:
    """
    统一入账。返回 {available, frozen, ledger_id, skipped_freeze}。

    调用方须在外层 transaction.atomic() 内，并已持有/即将更新的业务一致性。
    profile: 已加载的角色扩展实例（UserDashou 等）；若空则按 role+user_id 查。
    ignore_source_scope: True 时跳过 source_scope 检查（仍受 enabled / 金额规则约束）。
    """
    role = (role or '').strip().lower()
    cid = _club(club_id)
    credit = _money(amount)
    biz_id = (biz_id or '').strip()
    source_type = (source_type or '').strip() or 'unknown'

    if credit <= 0:
        return {'available': Decimal('0.00'), 'frozen': Decimal('0.00'), 'ledger_id': None, 'skipped_freeze': True}

    if profile is None:
        profile = _load_profile(role, user_id)
        if profile is None:
            raise ValueError(f'找不到角色资料 role={role} user_id={user_id}')

    cfg = get_freeze_config(cid, role)
    source_ok = True if ignore_source_scope else _source_allowed(cfg, source_type, role=role) if cfg else False
    freeze_on = bool(cfg and cfg.enabled and source_ok and biz_id)
    skip_reason = ''
    if not freeze_on:
        if not cfg:
            skip_reason = 'no_config'
        elif not cfg.enabled:
            skip_reason = 'disabled'
        elif not source_ok:
            skip_reason = f'source_not_in_scope:{source_type}'
        elif not biz_id:
            skip_reason = 'empty_biz_id'
        else:
            skip_reason = 'unknown'
        logger.info(
            'fund_freeze skip club=%s role=%s source=%s biz=%s user=%s amount=%s reason=%s',
            cid, role, source_type, biz_id, user_id, credit, skip_reason,
        )

    frozen = calc_freeze_amount(credit, cfg) if freeze_on else Decimal('0.00')
    available = _money(credit - frozen)
    if freeze_on and frozen <= 0:
        skip_reason = 'calc_zero'
        logger.info(
            'fund_freeze calc_zero club=%s role=%s source=%s biz=%s amount=%s mode=%s rate=%s',
            cid, role, source_type, biz_id, credit,
            getattr(cfg, 'freeze_mode', None), getattr(cfg, 'freeze_rate', None),
        )

    # 幂等：已存在同 biz 冻结单则只补可提现差额不重复冻（极端重入）
    if freeze_on and frozen > 0:
        existed = FundFreezeLedger.query.filter(
            club_id=cid, role=role, source_type=source_type, biz_id=biz_id,
        ).first()
        if existed:
            logger.warning(
                'fund_freeze idempotent hit club=%s role=%s source=%s biz=%s — 不再入账',
                cid, role, source_type, biz_id,
            )
            return {
                'available': Decimal('0.00'),
                'frozen': Decimal('0.00'),
                'ledger_id': existed.id,
                'skipped_freeze': True,
                'idempotent': True,
                'skip_reason': 'idempotent',
            }

    _add_withdrawable(role, profile, available, also_zonge=also_credit_zonge)
    ledger_id = None
    if frozen > 0:
        _add_dongjie(profile, frozen)
        delay = _first_step_delay_days(cfg)
        next_at = timezone.now() + timedelta(days=delay) if delay else timezone.now()
        ledger = FundFreezeLedger.query.create(
            club_id=cid,
            user_id=str(user_id),
            role=role,
            amount_total=frozen,
            amount_unfrozen=Decimal('0.00'),
            status=FundFreezeLedger.STATUS_FROZEN,
            source_type=source_type,
            order_id=(order_id or '')[:64],
            czjilu_id=(czjilu_id or '')[:64],
            biz_id=biz_id[:64],
            freeze_reason=(
                freeze_reason
                or (getattr(cfg, 'remark', None) if cfg else None)
                or f'{source_type}冻结'
            )[:255],
            config_snapshot=_config_snapshot(cfg),
            current_step=1,
            next_eligible_at=next_at,
        )
        ledger_id = ledger.id
        logger.info(
            'fund_freeze applied club=%s role=%s source=%s biz=%s user=%s '
            'credit=%s available=%s frozen=%s ledger=%s',
            cid, role, source_type, biz_id, user_id, credit, available, frozen, ledger_id,
        )

    return {
        'available': available,
        'frozen': frozen,
        'ledger_id': ledger_id,
        'skipped_freeze': frozen <= 0,
        'skip_reason': skip_reason or None,
        'club_id': cid,
    }


def _load_profile(role: str, user_id: str):
    from users.business_models import User
    from users.models import UserDashou, UserGuanshi, UserZuzhang, UserShenheguan
    user = User.query.filter(UserUID=user_id).first()
    if not user:
        return None
    mapping = {
        ROLE_DASHOU: UserDashou,
        ROLE_GUANSHI: UserGuanshi,
        ROLE_ZUZHANG: UserZuzhang,
        ROLE_SHENHEGUAN: UserShenheguan,
    }
    model = mapping.get(role)
    if not model:
        return None
    return model.query.filter(user=user).first()


def split_preview(club_id: Optional[str], role: str, amount, source_type: str = SOURCE_ORDER_SETTLE) -> Tuple[Decimal, Decimal]:
    """只算不写库：后台试算用。"""
    credit = _money(amount)
    role = (role or '').strip().lower()
    cfg = get_freeze_config(club_id, role)
    if not cfg or not cfg.enabled or not _source_allowed(cfg, source_type, role=role):
        return credit, Decimal('0.00')
    frozen = calc_freeze_amount(credit, cfg)
    return _money(credit - frozen), frozen


def credit_guanshi_fenhong(
    *,
    user_id: str,
    amount,
    biz_id: str,
    profile=None,
    order_id: str = '',
    czjilu_id: str = '',
    club_id: Optional[str] = None,
    freeze_reason: str = '',
) -> Dict[str, Any]:
    """管事分红入账：未开冻结=全额进 yue；开启则按配置拆冻结池。调用方须在 atomic 内。"""
    cid = settle_freeze_club_id(user_id=str(user_id or '')) if not club_id else _club(club_id)
    return credit_role_balance(
        club_id=cid,
        user_id=str(user_id),
        role=ROLE_GUANSHI,
        amount=amount,
        source_type=SOURCE_FENHONG,
        biz_id=biz_id,
        profile=profile,
        order_id=order_id or '',
        czjilu_id=czjilu_id or '',
        freeze_reason=freeze_reason or f'分红 {biz_id}',
    )


def credit_zuzhang_fenhong(
    *,
    user_id: str,
    amount,
    biz_id: str,
    profile=None,
    order_id: str = '',
    czjilu_id: str = '',
    club_id: Optional[str] = None,
    freeze_reason: str = '',
) -> Dict[str, Any]:
    """组长分红入账：未开冻结=全额进 ketixian_jine。调用方须在 atomic 内。"""
    cid = settle_freeze_club_id(user_id=str(user_id or '')) if not club_id else _club(club_id)
    return credit_role_balance(
        club_id=cid,
        user_id=str(user_id),
        role=ROLE_ZUZHANG,
        amount=amount,
        source_type=SOURCE_FENHONG,
        biz_id=biz_id,
        profile=profile,
        order_id=order_id or '',
        czjilu_id=czjilu_id or '',
        freeze_reason=freeze_reason or f'分红 {biz_id}',
    )


def settle_freeze_club_id(order=None, user_id: str = '') -> str:
    """
    结算冻结规则按「入账人所属俱乐部」，不按订单俱乐部。

    跨店抢单、跨平台商家发单时，订单 ClubID 可能是对方店；
    分成进的是接单打手钱包，必须用打手 User.ClubID 取冻结配置。
    仅当打手无归属时，才回退订单 ClubID / 默认俱乐部。
    """
    uid = (user_id or '').strip()
    if uid:
        try:
            from users.business_models import User
            u = User.query.filter(UserUID=uid).only('ClubID').first()
            if u and getattr(u, 'ClubID', None) and str(u.ClubID).strip():
                return _club(u.ClubID)
        except Exception as e:
            logger.warning('settle_freeze_club_id user ClubID failed uid=%s: %s', uid, e)
    if order is not None:
        cid = getattr(order, 'ClubID', None) or getattr(order, 'club_id', None)
        if cid and str(cid).strip():
            return _club(cid)
    return _club(None)


def order_club_id(order, user_id: str = '') -> str:
    """兼容旧名：实际按打手所属俱乐部解析冻结配置俱乐部。"""
    return settle_freeze_club_id(order, user_id=user_id)


def credit_dashou_order_settle(
    *,
    order,
    dashou_profile,
    user_id: str,
    amount,
    set_idle_status: bool = False,
    increment_chengjiao: bool = True,
    settle_biz_id: str = '',
) -> Dict[str, Any]:
    """
    打手订单结算专用入账（P1）。

    - 可提现走 credit_role_balance（未开冻结=全额进 yue，与改造前一致）
    - 冻结配置按打手所属俱乐部（跨店单不按订单 ClubID）
    - zonge / 今日今月收益仍按「全额」累加（统计口径不变）
    - 成交单量 +1、可选置空闲状态
    - settle_biz_id：组队多人分账时传入 `{order_id}:team:{uid}`，避免同单幂等撞键
    调用方须已在 transaction.atomic() 内；本函数用 F() 更新，勿再对同一 profile save 覆盖余额字段。
    """
    from users.models import UserDashou

    credit = _money(amount)
    order_id = str(getattr(order, 'OrderID', None) or getattr(order, 'order_id', '') or '')
    cid = settle_freeze_club_id(order, user_id=str(user_id or ''))
    biz_id = (settle_biz_id or order_id or '').strip()

    if credit <= 0:
        updates = {}
        if increment_chengjiao:
            updates['chengjiaozongliang'] = F('chengjiaozongliang') + 1
        if set_idle_status:
            updates['zhuangtai'] = 1
        if updates:
            UserDashou.query.filter(pk=dashou_profile.pk).update(**updates)
        return {
            'available': Decimal('0.00'),
            'frozen': Decimal('0.00'),
            'ledger_id': None,
            'skipped_freeze': True,
        }

    result = credit_role_balance(
        club_id=cid,
        user_id=str(user_id),
        role=ROLE_DASHOU,
        amount=credit,
        source_type=SOURCE_ORDER_SETTLE,
        biz_id=biz_id,
        profile=dashou_profile,
        order_id=order_id,
        freeze_reason=f'订单结算 {order_id}',
        also_credit_zonge=False,
    )
    if result.get('idempotent'):
        logger.warning('打手订单结算幂等跳过 order=%s user=%s', order_id, user_id)
        return result

    updates = {
        'zonge': F('zonge') + credit,
        'jinrishouyi': F('jinrishouyi') + credit,
        'jinyueshouyi': F('jinyueshouyi') + credit,
    }
    if increment_chengjiao:
        updates['chengjiaozongliang'] = F('chengjiaozongliang') + 1
    if set_idle_status:
        updates['zhuangtai'] = 1
    UserDashou.query.filter(pk=dashou_profile.pk).update(**updates)
    return result


def _step_unfreeze_amount(ledger: FundFreezeLedger, step: dict) -> Decimal:
    remain = _money(ledger.amount_total - ledger.amount_unfrozen)
    if remain <= 0:
        return Decimal('0.00')
    typ = (step.get('type') or 'remain').strip().lower()
    if typ == 'remain':
        return remain
    if typ == 'fixed':
        return min(remain, _money(step.get('value') or 0))
    # ratio
    ratio = Decimal(str(step.get('value') or 0))
    if ratio <= 0:
        return Decimal('0.00')
    return min(remain, _money(ledger.amount_total * ratio))


def _plan_steps(snapshot: dict) -> list:
    schedule = (snapshot or {}).get('schedule_mode') or 'all_at_once'
    if schedule == 'installment':
        steps = ((snapshot or {}).get('installment_json') or {}).get('steps') or []
        if steps:
            return sorted(steps, key=lambda s: int(s.get('seq') or 0))
    # 一次全解：单步 remain，天数用 freeze_min_days（已在 next_eligible_at）
    return [{'seq': 1, 'after_days': 0, 'type': 'remain', 'value': 1, 'need_condition': False}]


def apply_unfreeze_step(
    ledger_id: int,
    *,
    trigger: str = 'schedule',
    operator_id: str = '',
    reason: str = '',
    force: bool = False,
) -> Dict[str, Any]:
    """
    执行一笔冻结单的当前分期步。
    force=True：后台手动，跳过时间/条件门槛。
    条件未达标时不推进步数、不改 next_eligible_at，供定时任务下次重试。
    """
    from django.db import transaction
    from users.models import UserDashou, UserGuanshi, UserZuzhang, UserShenheguan
    from jituan.models import FundUnfreezeEvent
    from jituan.services.fund_freeze_conditions import evaluate_unfreeze_conditions

    with transaction.atomic():
        ledger = (
            FundFreezeLedger.objects.select_for_update()
            .filter(pk=ledger_id)
            .first()
        )
        if not ledger:
            return {'ok': False, 'msg': '冻结单不存在'}
        if ledger.status in (FundFreezeLedger.STATUS_DONE, FundFreezeLedger.STATUS_VOID):
            return {'ok': False, 'msg': '已完结或已作废'}

        now = timezone.now()
        snap = ledger.config_snapshot or {}
        trigger_mode = (snap.get('unfreeze_trigger') or 'time_only')
        min_days = max(0, int(snap.get('freeze_min_days') or 0))
        frozen_at = ledger.frozen_at or now
        min_days_ok = force or (now >= frozen_at + timedelta(days=min_days))
        time_ok = force or (
            not ledger.next_eligible_at or ledger.next_eligible_at <= now
        )

        if trigger_mode == 'manual_only' and not force:
            return {'ok': False, 'msg': '仅允许手动解冻'}

        steps = _plan_steps(snap)
        step_idx = max(0, int(ledger.current_step or 1) - 1)
        if step_idx >= len(steps):
            step = {'seq': ledger.current_step, 'type': 'remain', 'value': 1, 'need_condition': False}
        else:
            step = steps[step_idx]
        need_step_cond = bool(step.get('need_condition'))

        cond_ok = True
        cond_snap: Dict[str, Any] = {}
        need_eval = (
            not force
            and (
                need_step_cond
                or trigger_mode in (
                    'condition_only', 'time_and_condition', 'time_or_condition',
                )
            )
        )

        profile_map = {
            ROLE_DASHOU: UserDashou,
            ROLE_GUANSHI: UserGuanshi,
            ROLE_ZUZHANG: UserZuzhang,
            ROLE_SHENHEGUAN: UserShenheguan,
        }
        model = profile_map.get(ledger.role)
        if not model:
            return {'ok': False, 'msg': f'未知角色 {ledger.role}'}

        from users.business_models import User
        user = User.query.filter(UserUID=ledger.user_id).first()
        if not user:
            return {'ok': False, 'msg': '用户不存在'}
        profile = model.objects.select_for_update().filter(user=user).first()
        if not profile:
            return {'ok': False, 'msg': '角色扩展表不存在'}

        if need_eval:
            cond_ok, cond_snap = evaluate_unfreeze_conditions(
                ledger.club_id,
                ledger.user_id,
                ledger.role,
                snap.get('condition_json') or {},
                profile=profile,
            )

        if not force:
            if trigger_mode == 'time_only':
                if not time_ok:
                    return {'ok': False, 'msg': '未到解冻时间', 'next_eligible_at': ledger.next_eligible_at}
            elif trigger_mode == 'condition_only':
                if not min_days_ok:
                    return {'ok': False, 'msg': f'未满最短冻结天数({min_days}天)'}
                if not cond_ok:
                    return {
                        'ok': False,
                        'msg': '条件未达标',
                        'condition': cond_snap,
                        'retry': True,
                    }
            elif trigger_mode == 'time_and_condition':
                if not time_ok:
                    return {'ok': False, 'msg': '未到解冻时间', 'next_eligible_at': ledger.next_eligible_at}
                if not cond_ok:
                    return {
                        'ok': False,
                        'msg': '已到期但条件未达标，等待达标后自动解',
                        'condition': cond_snap,
                        'retry': True,
                    }
            elif trigger_mode == 'time_or_condition':
                if not (time_ok or cond_ok):
                    return {
                        'ok': False,
                        'msg': '未到期且条件未达标',
                        'condition': cond_snap,
                        'retry': True,
                    }

            # 分期单步要求达标时，即使全局是 time_only / time_or 已到期，也仍需条件
            if need_step_cond and not cond_ok:
                return {
                    'ok': False,
                    'msg': '本分期步要求条件达标',
                    'condition': cond_snap,
                    'retry': True,
                }

        amount = _step_unfreeze_amount(ledger, step)
        if amount <= 0:
            ledger.status = FundFreezeLedger.STATUS_DONE
            ledger.save(update_fields=['status', 'UpdateTime'])
            return {'ok': True, 'amount': Decimal('0.00'), 'done': True}

        # 冻结池不足则按实际可解
        cur_frozen = _money(getattr(profile, 'dongjie_yue', 0) or 0)
        if cur_frozen < amount:
            amount = cur_frozen
        if amount <= 0:
            return {'ok': False, 'msg': '冻结池余额不足'}

        if ledger.role == ROLE_ZUZHANG:
            model.query.filter(pk=profile.pk).update(
                dongjie_yue=F('dongjie_yue') - amount,
                ketixian_jine=F('ketixian_jine') + amount,
            )
        else:
            model.query.filter(pk=profile.pk).update(
                dongjie_yue=F('dongjie_yue') - amount,
                yue=F('yue') + amount,
            )

        new_unfrozen = _money(ledger.amount_unfrozen + amount)
        remain = _money(ledger.amount_total - new_unfrozen)
        ledger.amount_unfrozen = new_unfrozen
        next_step = int(ledger.current_step or 1) + 1

        if remain <= 0:
            ledger.status = FundFreezeLedger.STATUS_DONE
            ledger.current_step = next_step
            ledger.next_eligible_at = None
        else:
            ledger.status = FundFreezeLedger.STATUS_PARTIAL
            ledger.current_step = next_step
            if next_step - 1 < len(steps):
                nxt = steps[next_step - 1]
                days = int(nxt.get('after_days') or 0)
                ledger.next_eligible_at = frozen_at + timedelta(days=days)
            else:
                # 无更多配置步：剩余立即进入可扫状态，下一步按 remain 吃完
                ledger.next_eligible_at = now

        ledger.save(update_fields=[
            'amount_unfrozen', 'status', 'current_step', 'next_eligible_at', 'UpdateTime',
        ])

        event_trigger = trigger
        if not force and trigger_mode in ('condition_only', 'time_or_condition') and cond_ok and not time_ok:
            event_trigger = 'condition'

        FundUnfreezeEvent.query.create(
            ledger_id=ledger.id,
            club_id=ledger.club_id,
            user_id=ledger.user_id,
            role=ledger.role,
            step_seq=int(step.get('seq') or ledger.current_step),
            amount=amount,
            trigger=event_trigger,
            reason=(reason or f'{event_trigger}解冻')[:255],
            operator_id=(operator_id or '')[:32],
            condition_snapshot=cond_snap or {},
        )

        return {
            'ok': True,
            'amount': amount,
            'remain': max(remain, Decimal('0.00')),
            'done': remain <= 0,
            'ledger_id': ledger.id,
            'condition': cond_snap,
        }


def scan_due_freeze_ledgers(limit: int = 200) -> Dict[str, Any]:
    """
    定时任务入口。
    1) 到期（next_eligible_at<=now）的冻结中/部分解冻单
    2) 条件触发类：condition_only / time_or_condition —— 即使未到期也扫描，避免「条件先达标却要干等到期」
    临界：条件未达标只记 fail+retry，不改账本；下次 beat 再试。
    """
    from django.db.models import Q

    now = timezone.now()
    base = FundFreezeLedger.query.filter(
        status__in=[FundFreezeLedger.STATUS_FROZEN, FundFreezeLedger.STATUS_PARTIAL],
    )
    due_ids = list(
        base.filter(next_eligible_at__lte=now)
        .order_by('next_eligible_at', 'id')
        .values_list('id', flat=True)[:limit]
    )
    # JSON 字段路径：MySQL/Postgres Django JSONField 支持 key 查询
    cond_ids = []
    remain = max(0, limit - len(due_ids))
    if remain > 0:
        try:
            cond_ids = list(
                base.filter(
                    Q(config_snapshot__unfreeze_trigger='condition_only')
                    | Q(config_snapshot__unfreeze_trigger='time_or_condition')
                )
                .exclude(id__in=due_ids)
                .order_by('id')
                .values_list('id', flat=True)[:remain]
            )
        except Exception as e:
            logger.warning('scan condition branch skipped: %s', e)

    ids = due_ids + cond_ids
    ok_n = fail_n = retry_n = 0
    for lid in ids:
        try:
            r = apply_unfreeze_step(lid, trigger='schedule')
            if r.get('ok'):
                ok_n += 1
            else:
                fail_n += 1
                if r.get('retry'):
                    retry_n += 1
                logger.info('unfreeze skip id=%s msg=%s', lid, r.get('msg'))
        except Exception as e:
            fail_n += 1
            logger.exception('unfreeze error id=%s: %s', lid, e)
    return {
        'ok': ok_n,
        'fail': fail_n,
        'retry': retry_n,
        'scanned': len(ids),
        'due': len(due_ids),
        'cond_scan': len(cond_ids),
    }
