package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
)

type activeVipBenefitResource struct {
	BenefitCode     string
	AutoEquip       bool
	CurrentIdentity bool
	Resource        resourcedomain.Resource
}

// VIP 称号、长徽章和短徽章表达的是同一时刻的当前身份，不能像座驾、头像框等
// 收藏型装扮一样向下累计。三个稳定 code 必须在发放、装备门禁和展示投影中共用此判断。
func isCurrentLevelOnlyVipBenefit(benefitCode string) bool {
	switch strings.ToLower(strings.TrimSpace(benefitCode)) {
	case ledger.VipBenefitCodeTitle, ledger.VipBenefitCodeBadgeIdentity, ledger.VipBenefitCodeMedal:
		return true
	default:
		return false
	}
}

// listActiveVipBenefitResourcesTx 只返回当前最终等级的自动佩戴候选。用户可以手动选择低等级
// 装扮，但 VIP 升级时不能依次自动佩戴所有历史样式覆盖当前等级的默认展示。
func (r *Repository) listActiveVipBenefitResourcesTx(ctx context.Context, tx *sql.Tx, level int32, effectiveSource string) ([]activeVipBenefitResource, error) {
	return r.listVipBenefitResourcesTx(ctx, tx, level, effectiveSource, false)
}

// listPaidVipRewardResourcesTx 收集目标等级及其以下每一级显式绑定的收藏型物品；VIP 称号、
// 长徽章和短徽章只取目标等级，避免直购高等级时把历级身份同时发入用户背包。
func (r *Repository) listPaidVipRewardResourcesTx(ctx context.Context, tx *sql.Tx, level int32) ([]activeVipBenefitResource, error) {
	return r.listVipBenefitResourcesTx(ctx, tx, level, ledger.VipEffectiveSourcePaid, true)
}

// listVipBenefitResourcesTx 先走 vip_level_benefits 的 (app_code, level, benefit_code) 主键范围，
// 查询上界受 program.level_count 和购买校验约束；再按 resource 主键加锁，保证 admin 改绑与发放处于同一事务边界。
func (r *Repository) listVipBenefitResourcesTx(ctx context.Context, tx *sql.Tx, level int32, effectiveSource string, includeLowerLevels bool) ([]activeVipBenefitResource, error) {
	if level <= 0 || (effectiveSource != ledger.VipEffectiveSourcePaid && effectiveSource != ledger.VipEffectiveSourceTrial) {
		return nil, nil
	}
	trialOnly := effectiveSource == ledger.VipEffectiveSourceTrial
	levelPredicate := `b.level = ?`
	levelArgs := []any{level}
	if includeLowerLevels {
		// 收藏型资源继续向下累计；三类 VIP 当前身份资源只允许命中最终等级。
		levelPredicate = `b.level <= ? AND (b.level = ? OR b.benefit_code NOT IN (?, ?, ?))`
		levelArgs = []any{
			level,
			level,
			ledger.VipBenefitCodeTitle,
			ledger.VipBenefitCodeBadgeIdentity,
			ledger.VipBenefitCodeMedal,
		}
	}
	args := []any{
		ledger.VipBenefitCodeTitle,
		ledger.VipBenefitCodeBadgeIdentity,
		ledger.VipBenefitCodeMedal,
		appcode.FromContext(ctx),
	}
	args = append(args, levelArgs...)
	args = append(args, trialOnly, ledger.VipBenefitCodeDailyCoinRebate)
	rows, err := tx.QueryContext(ctx, `
			SELECT MIN(b.benefit_code), MAX(b.auto_equip),
			       MAX(b.benefit_code IN (?, ?, ?)), b.resource_id
			FROM vip_level_benefits AS b
			JOIN resources AS resource ON resource.app_code = b.app_code
			 AND resource.resource_id = b.resource_id
			 AND resource.status = 'active'
			 AND resource.resource_type = b.resource_type
			WHERE b.app_code = ? AND `+levelPredicate+` AND b.status = 'active' AND b.resource_id > 0
			  AND (? = FALSE OR (b.trial_enabled = TRUE AND b.benefit_code <> ?))
			GROUP BY b.resource_id
			ORDER BY b.resource_id ASC`, args...)
	if err != nil {
		return nil, err
	}
	type binding struct {
		benefitCode     string
		autoEquip       bool
		currentIdentity bool
		resourceID      int64
	}
	bindings := make([]binding, 0, 4)
	for rows.Next() {
		var item binding
		if err := rows.Scan(&item.benefitCode, &item.autoEquip, &item.currentIdentity, &item.resourceID); err != nil {
			_ = rows.Close()
			return nil, err
		}
		bindings = append(bindings, item)
	}
	if err := rows.Err(); err != nil {
		_ = rows.Close()
		return nil, err
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}

	items := make([]activeVipBenefitResource, 0, len(bindings))
	seen := make(map[int64]struct{}, len(bindings))
	for _, binding := range bindings {
		if _, exists := seen[binding.resourceID]; exists {
			continue
		}
		resource, err := r.getResourceForUpdate(ctx, tx, binding.resourceID)
		if err != nil {
			return nil, err
		}
		seen[binding.resourceID] = struct{}{}
		items = append(items, activeVipBenefitResource{
			BenefitCode:     binding.benefitCode,
			AutoEquip:       binding.autoEquip,
			CurrentIdentity: binding.currentIdentity,
			Resource:        resource,
		})
	}
	return items, nil
}

// grantVipTrialBenefitResourcesTx 按 config_version 为一张体验卡创建来源独立的装扮 entitlement。
// 收藏型资源向下累计，三类当前身份资源只发体验等级；改绑会生成新版本 grant，未变化资源
// 复用同一卡已有 entitlement，既拿到新资源又不会重复旧资源。
func (r *Repository) grantVipTrialBenefitResourcesTx(ctx context.Context, tx *sql.Tx, card ledger.VipTrialCard, configVersion int64, nowMS int64) error {
	items, err := r.listVipBenefitResourcesTx(ctx, tx, card.Level, ledger.VipEffectiveSourceTrial, true)
	if err != nil || len(items) == 0 {
		return err
	}
	resourceIDs := make([]int64, 0, len(items))
	for _, item := range items {
		resourceIDs = append(resourceIDs, item.Resource.ResourceID)
	}
	sort.Slice(resourceIDs, func(left, right int) bool { return resourceIDs[left] < resourceIDs[right] })
	snapshot := struct {
		TrialCardID string  `json:"trial_card_id"`
		Level       int32   `json:"level"`
		ExpiresAtMS int64   `json:"expires_at_ms"`
		ResourceIDs []int64 `json:"resource_ids"`
	}{TrialCardID: card.TrialCardID, Level: card.Level, ExpiresAtMS: card.ExpiresAtMS, ResourceIDs: resourceIDs}
	snapshotJSON, err := json.Marshal(snapshot)
	if err != nil {
		return err
	}
	// v3 固化“三类身份资源仅当前等级”的新请求语义，避免与 v2 的历级资源快照发生幂等冲突。
	grantCommandID := "vip_trial_benefit_v3:" + stableHash(fmt.Sprintf("%s|%s|%d", appcode.FromContext(ctx), card.TrialCardID, configVersion))
	requestHash := stableHash(fmt.Sprintf("%s|%d|%s", appcode.FromContext(ctx), card.UserID, snapshotJSON))
	if _, exists, err := r.lookupResourceGrant(ctx, tx, grantCommandID, requestHash); err != nil || exists {
		return err
	}
	grantID := resourceGrantID(appcode.FromContext(ctx), grantCommandID)
	originSource := strings.TrimSpace(card.GrantSource)
	if originSource == "" {
		originSource = ledger.VipGrantSourceActivity
	}
	subjectID := vipTrialBenefitGrantSubjectID(appcode.FromContext(ctx), card.TrialCardID)
	if err := r.insertResourceGrant(ctx, tx, grantID, grantCommandID, card.UserID, resourcedomain.GrantSourceVIPTrial,
		resourcedomain.GrantSubjectGroup, subjectID, requestHash,
		string(snapshotJSON), "vip trial effective benefit resources", card.UserID, nowMS); err != nil {
		return err
	}
	durationMS := card.ExpiresAtMS - nowMS
	if durationMS <= 0 {
		return xerr.New(xerr.VIPTrialCardExpired, "vip trial card expired")
	}
	for _, item := range items {
		if entitlementID, exists, err := r.findExistingVipBenefitEntitlementTx(ctx, tx, card.UserID,
			item.Resource.ResourceID, resourcedomain.GrantSourceVIPTrial, subjectID,
			"vip_trial_benefit:"+card.TrialCardID, nowMS); err != nil {
			return err
		} else if exists {
			if err := r.alignEntitlementExpiresAt(ctx, tx, entitlementID, card.ExpiresAtMS, nowMS); err != nil {
				return err
			}
			continue
		}
		inserted, err := r.applyVipGrantItemWithoutAutoEquipTx(ctx, tx, grantID, grantCommandID,
			card.UserID, item.Resource, 1, durationMS, nowMS)
		if err != nil {
			return err
		}
		if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, card.ExpiresAtMS, nowMS); err != nil {
			return err
		}
	}
	return r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupGranted", grantCommandID, card.UserID, 0, map[string]any{
			"grant_id": grantID, "source": resourcedomain.GrantSourceVIPTrial,
			"origin_source": originSource, "vip_level": card.Level, "config_version": configVersion,
			"trial_card_id": card.TrialCardID, "expires_at_ms": card.ExpiresAtMS,
			"resource_ids": resourceIDs,
		}, nowMS),
	})
}

// grantPaidVipBenefitResourcesTx 直接从 active benefit bindings 发付费 entitlement，彻底去掉
// “运营还要同步 reward group”的隐含前置条件。直购高等级时累计 1..level 的收藏型物品，
// 但称号、长徽章和短徽章只发当前等级；功能权限仍不做向下合并。
func (r *Repository) grantPaidVipBenefitResourcesTx(ctx context.Context, tx *sql.Tx, vip ledger.UserVip, includeCurrentIdentity bool, nowMS int64) ([]ledger.VipRewardItem, string, error) {
	if !vip.Active || vip.Level <= 0 || vip.ExpiresAtMS <= nowMS {
		return nil, "", nil
	}
	items, err := r.listPaidVipRewardResourcesTx(ctx, tx, vip.Level)
	if err != nil || len(items) == 0 {
		return nil, "", err
	}
	if !includeCurrentIdentity {
		// 更高 trial 是最终身份时，paid 仍正常累计座驾、头像框等收藏品，但三类身份素材
		// 不得先写 entitlement/outbox 再回收，否则异步徽章投影仍可能短暂收到低等级事实。
		filtered := items[:0]
		for _, item := range items {
			if !item.CurrentIdentity {
				filtered = append(filtered, item)
			}
		}
		items = filtered
		if len(items) == 0 {
			return nil, "", nil
		}
	}
	resourceIDs := make([]int64, 0, len(items))
	for _, item := range items {
		resourceIDs = append(resourceIDs, item.Resource.ResourceID)
	}
	sort.Slice(resourceIDs, func(left, right int) bool { return resourceIDs[left] < resourceIDs[right] })
	snapshot := struct {
		UserID        int64   `json:"user_id"`
		Level         int32   `json:"level"`
		ExpiresAtMS   int64   `json:"expires_at_ms"`
		ConfigVersion int64   `json:"config_version"`
		WithIdentity  bool    `json:"with_identity"`
		ResourceIDs   []int64 `json:"resource_ids"`
	}{
		UserID: vip.UserID, Level: vip.Level, ExpiresAtMS: vip.ExpiresAtMS,
		ConfigVersion: vip.ConfigVersion, WithIdentity: includeCurrentIdentity,
		ResourceIDs: resourceIDs,
	}
	snapshotJSON, err := json.Marshal(snapshot)
	if err != nil {
		return nil, "", err
	}
	// v3 同时隔离旧版历级身份快照与 trial 生效期间的 collectible-only 发放；trial
	// 回落时 with_identity=true 使用独立幂等命令，不能与先前无身份素材的 grant 冲突。
	grantCommandID := "vip_paid_benefit_v3:" + stableHash(fmt.Sprintf(
		"%s|%d|%d|%d|%d|%t",
		appcode.FromContext(ctx), vip.UserID, vip.Level, vip.ExpiresAtMS, vip.ConfigVersion, includeCurrentIdentity,
	))
	requestHash := stableHash(fmt.Sprintf("%s|%d|%s", appcode.FromContext(ctx), vip.UserID, snapshotJSON))
	if existingGrantID, exists, err := r.lookupResourceGrant(ctx, tx, grantCommandID, requestHash); err != nil {
		return nil, "", err
	} else if exists {
		rewards, replayErr := r.rewardItemsByGrant(ctx, tx, existingGrantID, vip.ExpiresAtMS)
		return rewards, existingGrantID, replayErr
	}
	grantID := resourceGrantID(appcode.FromContext(ctx), grantCommandID)
	subjectID := "vip_paid_membership:" + stableHash(fmt.Sprintf("%s|%d|%d|%d", appcode.FromContext(ctx), vip.UserID, vip.Level, vip.ExpiresAtMS))
	if err := r.insertResourceGrant(ctx, tx, grantID, grantCommandID, vip.UserID, resourcedomain.GrantSourceVIPPaid,
		resourcedomain.GrantSubjectGroup, subjectID, requestHash, string(snapshotJSON),
		"paid vip effective benefit resources", vip.UserID, nowMS); err != nil {
		return nil, "", err
	}
	durationMS := vip.ExpiresAtMS - nowMS
	rewards := make([]ledger.VipRewardItem, 0, len(items))
	for _, item := range items {
		if entitlementID, exists, err := r.findExistingVipBenefitEntitlementTx(ctx, tx, vip.UserID,
			item.Resource.ResourceID, resourcedomain.GrantSourceVIPPaid, subjectID, "", nowMS); err != nil {
			return nil, "", err
		} else if exists {
			if err := r.alignEntitlementExpiresAt(ctx, tx, entitlementID, vip.ExpiresAtMS, nowMS); err != nil {
				return nil, "", err
			}
		} else {
			inserted, err := r.applyVipGrantItemWithoutAutoEquipTx(ctx, tx, grantID, grantCommandID,
				vip.UserID, item.Resource, 1, durationMS, nowMS)
			if err != nil {
				return nil, "", err
			}
			if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, vip.ExpiresAtMS, nowMS); err != nil {
				return nil, "", err
			}
		}
		rewards = append(rewards, vipRewardItemFromBenefitResource(item.Resource, vip.ExpiresAtMS))
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupGranted", grantCommandID, vip.UserID, 0, map[string]any{
			"grant_id": grantID, "source": resourcedomain.GrantSourceVIPPaid,
			"vip_level": vip.Level, "config_version": vip.ConfigVersion,
			"expires_at_ms": vip.ExpiresAtMS, "resource_ids": resourceIDs,
		}, nowMS),
	}); err != nil {
		return nil, "", err
	}
	return rewards, grantID, nil
}

func vipRewardItemFromBenefitResource(resource resourcedomain.Resource, expiresAtMS int64) ledger.VipRewardItem {
	return ledger.VipRewardItem{
		ResourceID: resource.ResourceID, ResourceCode: resource.ResourceCode,
		ResourceType: resource.ResourceType, Name: resource.Name, Quantity: 1,
		ExpiresAtMS: expiresAtMS, AssetURL: resource.AssetURL,
		PreviewURL: resource.PreviewURL, AnimationURL: resource.AnimationURL,
	}
}

func vipTrialBenefitGrantSubjectID(appCode string, trialCardID string) string {
	return "vip_trial_card:" + stableHash(appcode.Normalize(appCode)+"|"+strings.TrimSpace(trialCardID))
}

// findExistingVipBenefitEntitlementTx 只在同一 VIP 来源和同一业务 subject 内复用资源，
// paid/trial 永不互相合并。legacyTrialCommand 只兼容升级前已经由同一体验卡创建的固定命令。
func (r *Repository) findExistingVipBenefitEntitlementTx(ctx context.Context, tx *sql.Tx, userID int64, resourceID int64, grantSource string, subjectID string, legacyTrialCommand string, nowMS int64) (string, bool, error) {
	row := tx.QueryRowContext(ctx, `
		SELECT e.entitlement_id
		FROM user_resource_entitlements AS e
		JOIN resource_grants AS g ON g.app_code = e.app_code AND g.grant_id = e.source_grant_id
		WHERE e.app_code = ? AND e.user_id = ? AND e.resource_id = ?
		  AND e.status = 'active' AND e.effective_at_ms <= ?
		  AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?) AND e.remaining_quantity > 0
		  AND g.status = ?
		  AND ((g.grant_source = ? AND g.grant_subject_id = ?)
		       OR (? <> '' AND g.command_id = ?))
		ORDER BY e.expires_at_ms DESC, e.created_at_ms DESC
		LIMIT 1 FOR UPDATE`,
		appcode.FromContext(ctx), userID, resourceID, nowMS, nowMS,
		resourcedomain.GrantStatusDone, grantSource, subjectID, legacyTrialCommand, legacyTrialCommand,
	)
	var entitlementID string
	if err := row.Scan(&entitlementID); errors.Is(err, sql.ErrNoRows) {
		return "", false, nil
	} else if err != nil {
		return "", false, err
	}
	return entitlementID, true, nil
}

type vipIdentityGrantContext struct {
	EffectiveSource string
	GrantSource     string
	GrantSubjectID  string
	LegacyCommandID string
	ExpiresAtMS     int64
}

type activeVipIdentityEntitlement struct {
	EntitlementID  string
	ResourceID     int64
	SourceGrantID  string
	EffectiveAtMS  int64
	ExpiresAtMS    int64
	Remaining      int64
	GrantSource    string
	GrantSubjectID string
	GrantCommandID string
	GrantStatus    string
}

// currentVipIdentityGrantContextTx 把当前 paid membership 或已佩戴 trial card 固化为三类
// 身份 entitlement 的唯一来源和绝对有效期。调用方已按 program -> membership 加锁；
// 这里继续锁定具体来源事实，避免等级切换与回收在两个事务中交错。
func (r *Repository) currentVipIdentityGrantContextTx(ctx context.Context, tx *sql.Tx, userID int64, level int32, effectiveSource string, nowMS int64) (vipIdentityGrantContext, error) {
	switch effectiveSource {
	case ledger.VipEffectiveSourcePaid:
		var expiresAtMS int64
		if err := tx.QueryRowContext(ctx, `
			SELECT expires_at_ms
			FROM user_vip_memberships FORCE INDEX (PRIMARY)
			WHERE app_code = ? AND user_id = ? AND level = ? AND status = ?
			  AND expires_at_ms > ?
			FOR UPDATE`,
			appcode.FromContext(ctx), userID, level, ledger.VipStatusActive, nowMS,
		).Scan(&expiresAtMS); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return vipIdentityGrantContext{}, xerr.New(xerr.NotFound, "active paid vip identity source not found")
			}
			return vipIdentityGrantContext{}, err
		}
		return vipIdentityGrantContext{
			EffectiveSource: effectiveSource,
			GrantSource:     resourcedomain.GrantSourceVIPPaid,
			// 与正常 paid benefit grant 使用同一 membership subject；reconcile 只能
			// 保留当前有效窗创建的实例，不能误选旧会员周期的同 resource entitlement。
			GrantSubjectID: "vip_paid_membership:" + stableHash(fmt.Sprintf(
				"%s|%d|%d|%d",
				appcode.FromContext(ctx), userID, level, expiresAtMS,
			)),
			ExpiresAtMS: expiresAtMS,
		}, nil
	case ledger.VipEffectiveSourceTrial:
		var trialCardID string
		var expiresAtMS int64
		if err := tx.QueryRowContext(ctx, `
			SELECT card.trial_card_id, card.expires_at_ms
			FROM user_resource_equipment AS equipment FORCE INDEX (PRIMARY)
			JOIN user_vip_trial_cards AS card
			  ON card.app_code = equipment.app_code
			 AND card.user_id = equipment.user_id
			 AND card.entitlement_id = equipment.entitlement_id
			JOIN user_resource_entitlements AS entitlement
			  ON entitlement.app_code = equipment.app_code
			 AND entitlement.user_id = equipment.user_id
			 AND entitlement.entitlement_id = equipment.entitlement_id
			WHERE equipment.app_code = ? AND equipment.user_id = ?
			  AND equipment.resource_type = ?
			  AND card.level = ? AND card.status = ?
			  AND card.effective_at_ms <= ? AND card.expires_at_ms > ?
			  AND entitlement.status = 'active'
			  AND entitlement.effective_at_ms <= ?
			  AND (entitlement.expires_at_ms = 0 OR entitlement.expires_at_ms > ?)
			  AND entitlement.remaining_quantity > 0
			LIMIT 1 FOR UPDATE`,
			appcode.FromContext(ctx), userID, resourcedomain.TypeVIPTrialCard,
			level, ledger.VipTrialCardStatusActive, nowMS, nowMS, nowMS, nowMS,
		).Scan(&trialCardID, &expiresAtMS); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return vipIdentityGrantContext{}, xerr.New(xerr.NotFound, "equipped vip trial identity source not found")
			}
			return vipIdentityGrantContext{}, err
		}
		return vipIdentityGrantContext{
			EffectiveSource: effectiveSource,
			GrantSource:     resourcedomain.GrantSourceVIPTrial,
			GrantSubjectID:  vipTrialBenefitGrantSubjectID(appcode.FromContext(ctx), trialCardID),
			LegacyCommandID: "vip_trial_benefit:" + trialCardID,
			ExpiresAtMS:     expiresAtMS,
		}, nil
	default:
		return vipIdentityGrantContext{EffectiveSource: ledger.VipEffectiveSourceNone}, nil
	}
}

// listActiveVipIdentityEntitlementsForUpdateTx 只扫描单用户 active entitlement 索引范围，
// 再通过 resource_id 的 current/history 小表索引识别三类 VIP 身份素材。旧 grant 保持
// 审计不可变；这里只锁将要保留或回收的 entitlement，绝不整笔撤销含座驾等收藏品的 grant。
func (r *Repository) listActiveVipIdentityEntitlementsForUpdateTx(ctx context.Context, tx *sql.Tx, userID int64) ([]activeVipIdentityEntitlement, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT entitlement.entitlement_id, entitlement.resource_id,
		       entitlement.source_grant_id,
		       entitlement.effective_at_ms, entitlement.expires_at_ms,
		       entitlement.remaining_quantity
		FROM user_resource_entitlements AS entitlement FORCE INDEX (idx_user_resource_user_type)
		WHERE entitlement.app_code = ? AND entitlement.user_id = ?
		  AND entitlement.status = 'active'
		  AND (
		    EXISTS (
		      SELECT 1
		      FROM vip_level_benefits AS current_benefit
		      JOIN vip_program_configs AS current_program
		        ON current_program.app_code = current_benefit.app_code
		       AND current_program.program_type = ?
		      WHERE current_benefit.app_code = entitlement.app_code
		        AND current_benefit.resource_id = entitlement.resource_id
		        AND current_benefit.benefit_code IN (?, ?, ?)
		    )
		    OR EXISTS (
		      SELECT 1
		      FROM vip_benefit_resource_binding_history AS history
		      WHERE history.app_code = entitlement.app_code
		        AND history.resource_id = entitlement.resource_id
		        AND history.program_type = ?
		        AND history.benefit_code IN (?, ?, ?)
		    )
		  )
		ORDER BY entitlement.resource_id ASC, entitlement.expires_at_ms DESC,
		         entitlement.created_at_ms DESC, entitlement.entitlement_id ASC
		FOR UPDATE`,
		appcode.FromContext(ctx), userID,
		ledger.VipProgramTypeTieredPrivilegeV1,
		ledger.VipBenefitCodeTitle,
		ledger.VipBenefitCodeBadgeIdentity,
		ledger.VipBenefitCodeMedal,
		ledger.VipProgramTypeTieredPrivilegeV1,
		ledger.VipBenefitCodeTitle,
		ledger.VipBenefitCodeBadgeIdentity,
		ledger.VipBenefitCodeMedal,
	)
	if err != nil {
		return nil, err
	}
	items := make([]activeVipIdentityEntitlement, 0, 6)
	grantIDSet := make(map[string]struct{}, 6)
	for rows.Next() {
		var item activeVipIdentityEntitlement
		if err := rows.Scan(
			&item.EntitlementID,
			&item.ResourceID,
			&item.SourceGrantID,
			&item.EffectiveAtMS,
			&item.ExpiresAtMS,
			&item.Remaining,
		); err != nil {
			_ = rows.Close()
			return nil, err
		}
		items = append(items, item)
		grantIDSet[item.SourceGrantID] = struct{}{}
	}
	if err := rows.Err(); err != nil {
		_ = rows.Close()
		return nil, err
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if len(items) == 0 {
		return nil, nil
	}

	// grant 事实只用于判断来源，不参与写入。先锁 entitlement、再做普通一致性读取，
	// 可与后台 RevokeResourceGrant 的 grant -> entitlement 锁序共存，避免反向锁死。
	grantIDs := make([]string, 0, len(grantIDSet))
	for grantID := range grantIDSet {
		grantIDs = append(grantIDs, grantID)
	}
	sort.Strings(grantIDs)
	grantArgs := []any{appcode.FromContext(ctx)}
	grantArgs = append(grantArgs, stringAnyArgs(grantIDs)...)
	grantRows, err := tx.QueryContext(ctx, `
		SELECT grant_id, grant_source, grant_subject_id, command_id, status
		FROM resource_grants
		WHERE app_code = ? AND grant_id IN (`+placeholders(len(grantIDs))+`)`, grantArgs...)
	if err != nil {
		return nil, err
	}
	type grantMetadata struct {
		source    string
		subjectID string
		commandID string
		status    string
	}
	grants := make(map[string]grantMetadata, len(grantIDs))
	for grantRows.Next() {
		var grantID string
		var metadata grantMetadata
		if err := grantRows.Scan(
			&grantID,
			&metadata.source,
			&metadata.subjectID,
			&metadata.commandID,
			&metadata.status,
		); err != nil {
			_ = grantRows.Close()
			return nil, err
		}
		grants[grantID] = metadata
	}
	if err := grantRows.Err(); err != nil {
		_ = grantRows.Close()
		return nil, err
	}
	if err := grantRows.Close(); err != nil {
		return nil, err
	}

	filtered := items[:0]
	for _, item := range items {
		metadata, exists := grants[item.SourceGrantID]
		if !exists || !isVipIdentityGrantSource(metadata.source) {
			continue
		}
		item.GrantSource = metadata.source
		item.GrantSubjectID = metadata.subjectID
		item.GrantCommandID = metadata.commandID
		item.GrantStatus = metadata.status
		filtered = append(filtered, item)
	}
	return filtered, nil
}

func isVipIdentityGrantSource(source string) bool {
	switch strings.ToLower(strings.TrimSpace(source)) {
	case resourcedomain.GrantSourceVIPPaid,
		resourcedomain.GrantSourceVIPTrial,
		ledger.VipGrantSourcePurchase,
		ledger.VipGrantSourceActivity,
		ledger.VipGrantSourceAdmin,
		ledger.VipGrantSourceManagerCenter:
		return true
	default:
		return false
	}
}

func vipIdentityEntitlementMatchesCurrentSource(item activeVipIdentityEntitlement, grantContext vipIdentityGrantContext, nowMS int64) bool {
	if item.GrantStatus != resourcedomain.GrantStatusDone ||
		item.EffectiveAtMS > nowMS ||
		(item.ExpiresAtMS != 0 && item.ExpiresAtMS <= nowMS) ||
		item.Remaining <= 0 {
		return false
	}
	switch grantContext.EffectiveSource {
	case ledger.VipEffectiveSourcePaid:
		return item.GrantSource == resourcedomain.GrantSourceVIPPaid &&
			item.GrantSubjectID == grantContext.GrantSubjectID
	case ledger.VipEffectiveSourceTrial:
		return item.GrantSource == resourcedomain.GrantSourceVIPTrial &&
			(item.GrantSubjectID == grantContext.GrantSubjectID ||
				item.GrantCommandID == grantContext.LegacyCommandID)
	default:
		return false
	}
}

// grantMissingVipIdentityResourcesTx 只在当前来源的某个身份素材确实不存在时创建新 grant。
// 重新生成 entitlement 而不是把已回收行改回 active，可保留每次 VIP 来源切换的完整审计，
// 并重新发布 ResourceGroupGranted 让徽章投影在体验卡到期回落时恢复当前 paid 素材。
func (r *Repository) grantMissingVipIdentityResourcesTx(
	ctx context.Context,
	tx *sql.Tx,
	userID int64,
	level int32,
	grantContext vipIdentityGrantContext,
	commandID string,
	items []activeVipBenefitResource,
	nowMS int64,
) ([]string, walletOutboxEvent, error) {
	if len(items) == 0 {
		return nil, walletOutboxEvent{}, nil
	}
	durationMS := grantContext.ExpiresAtMS - nowMS
	if durationMS <= 0 {
		return nil, walletOutboxEvent{}, xerr.New(xerr.Conflict, "vip identity source already expired")
	}
	resourceIDs := make([]int64, 0, len(items))
	benefitCodes := make([]string, 0, len(items))
	for _, item := range items {
		resourceIDs = append(resourceIDs, item.Resource.ResourceID)
		benefitCodes = append(benefitCodes, item.BenefitCode)
	}
	snapshot := struct {
		UserID          int64    `json:"user_id"`
		Level           int32    `json:"level"`
		EffectiveSource string   `json:"effective_source"`
		ExpiresAtMS     int64    `json:"expires_at_ms"`
		ResourceIDs     []int64  `json:"resource_ids"`
		BenefitCodes    []string `json:"benefit_codes"`
	}{
		UserID: userID, Level: level, EffectiveSource: grantContext.EffectiveSource,
		ExpiresAtMS: grantContext.ExpiresAtMS, ResourceIDs: resourceIDs, BenefitCodes: benefitCodes,
	}
	snapshotJSON, err := json.Marshal(snapshot)
	if err != nil {
		return nil, walletOutboxEvent{}, err
	}
	// nowMS 只参与内部恢复实例的唯一性；membership/card 锁保证同一时刻只有一个事务
	// 判断“缺失”，重试提交后会先看到新 active entitlement，不会重复发放。
	grantCommandID := "vip_identity_restore_v1:" + stableHash(fmt.Sprintf(
		"%s|%d|%d|%s|%d|%s|%s|%d",
		appcode.FromContext(ctx), userID, level, grantContext.EffectiveSource,
		grantContext.ExpiresAtMS, grantContext.GrantSubjectID, commandID, nowMS,
	))
	grantID := resourceGrantID(appcode.FromContext(ctx), grantCommandID)
	requestHash := stableHash(fmt.Sprintf("%s|%s", appcode.FromContext(ctx), snapshotJSON))
	if err := r.insertResourceGrant(
		ctx,
		tx,
		grantID,
		grantCommandID,
		userID,
		grantContext.GrantSource,
		resourcedomain.GrantSubjectGroup,
		grantContext.GrantSubjectID,
		requestHash,
		string(snapshotJSON),
		"restore current vip identity resources",
		userID,
		nowMS,
	); err != nil {
		return nil, walletOutboxEvent{}, err
	}
	entitlementIDs := make([]string, 0, len(items))
	for _, item := range items {
		inserted, err := r.applyVipGrantItemWithoutAutoEquipTx(
			ctx,
			tx,
			grantID,
			grantCommandID,
			userID,
			item.Resource,
			1,
			durationMS,
			nowMS,
		)
		if err != nil {
			return nil, walletOutboxEvent{}, err
		}
		if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, grantContext.ExpiresAtMS, nowMS); err != nil {
			return nil, walletOutboxEvent{}, err
		}
		entitlementIDs = append(entitlementIDs, inserted.EntitlementID)
	}
	event := resourceOutboxEvent("ResourceGroupGranted", grantCommandID, userID, 0, map[string]any{
		"grant_id": grantID, "source": grantContext.GrantSource,
		"vip_level": level, "effective_source": grantContext.EffectiveSource,
		"expires_at_ms": grantContext.ExpiresAtMS, "resource_ids": resourceIDs,
		"identity_only": true,
	}, nowMS)
	return entitlementIDs, event, nil
}

// reconcileCurrentVipIdentityEntitlementsTx 先补齐最终 effective level 的三类身份素材，
// 再把其它等级、其它来源和重复实例全部标记 revoked。两步与 equipment reconcile 共用
// 一个事务，所以任意时刻数据库中最多只存在当前等级的一套 active 身份 entitlement。
func (r *Repository) reconcileCurrentVipIdentityEntitlementsTx(
	ctx context.Context,
	tx *sql.Tx,
	userID int64,
	level int32,
	effectiveSource string,
	commandID string,
	nowMS int64,
	items []activeVipBenefitResource,
) ([]walletOutboxEvent, error) {
	identityItems := make([]activeVipBenefitResource, 0, 3)
	for _, item := range items {
		if item.CurrentIdentity {
			identityItems = append(identityItems, item)
		}
	}
	grantContext, err := r.currentVipIdentityGrantContextTx(ctx, tx, userID, level, effectiveSource, nowMS)
	if err != nil {
		return nil, err
	}
	activeItems, err := r.listActiveVipIdentityEntitlementsForUpdateTx(ctx, tx, userID)
	if err != nil {
		return nil, err
	}

	keep := make(map[string]struct{}, len(identityItems))
	missing := make([]activeVipBenefitResource, 0, len(identityItems))
	for _, desired := range identityItems {
		found := false
		for _, entitlement := range activeItems {
			if entitlement.ResourceID != desired.Resource.ResourceID ||
				!vipIdentityEntitlementMatchesCurrentSource(entitlement, grantContext, nowMS) {
				continue
			}
			keep[entitlement.EntitlementID] = struct{}{}
			if err := r.alignEntitlementExpiresAt(ctx, tx, entitlement.EntitlementID, grantContext.ExpiresAtMS, nowMS); err != nil {
				return nil, err
			}
			found = true
			break
		}
		if !found {
			missing = append(missing, desired)
		}
	}

	events := make([]walletOutboxEvent, 0, 2)
	if len(missing) > 0 {
		insertedIDs, grantEvent, err := r.grantMissingVipIdentityResourcesTx(
			ctx, tx, userID, level, grantContext, commandID, missing, nowMS,
		)
		if err != nil {
			return nil, err
		}
		for _, entitlementID := range insertedIDs {
			keep[entitlementID] = struct{}{}
		}
		events = append(events, grantEvent)
	}

	revokeIDs := make([]string, 0, len(activeItems))
	reclaimedResourceIDs := make([]int64, 0, len(activeItems))
	for _, entitlement := range activeItems {
		if _, exists := keep[entitlement.EntitlementID]; exists {
			continue
		}
		revokeIDs = append(revokeIDs, entitlement.EntitlementID)
		reclaimedResourceIDs = append(reclaimedResourceIDs, entitlement.ResourceID)
	}
	if len(revokeIDs) == 0 {
		return events, nil
	}
	args := []any{appcode.FromContext(ctx), userID}
	args = append(args, stringAnyArgs(revokeIDs)...)
	if _, err := tx.ExecContext(ctx, `
		DELETE FROM user_resource_equipment
		WHERE app_code = ? AND user_id = ?
		  AND entitlement_id IN (`+placeholders(len(revokeIDs))+`)`, args...); err != nil {
		return nil, err
	}
	updateArgs := []any{
		resourcedomain.GrantStatusRevoked,
		nowMS,
		appcode.FromContext(ctx),
		userID,
		resourcedomain.StatusActive,
	}
	updateArgs = append(updateArgs, stringAnyArgs(revokeIDs)...)
	result, err := tx.ExecContext(ctx, `
		UPDATE user_resource_entitlements
		SET status = ?, remaining_quantity = 0, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ? AND status = ?
		  AND entitlement_id IN (`+placeholders(len(revokeIDs))+`)`, updateArgs...)
	if err != nil {
		return nil, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return nil, err
	}
	if affected != int64(len(revokeIDs)) {
		return nil, xerr.New(xerr.LedgerConflict, "vip identity entitlement reclaim state conflict")
	}
	reclaimCommandID := "vip_identity_reclaim_v1:" + stableHash(fmt.Sprintf(
		"%s|%d|%d|%s|%s|%d",
		appcode.FromContext(ctx), userID, level, effectiveSource, strings.Join(revokeIDs, ","), nowMS,
	))
	events = append(events, resourceOutboxEvent("UserResourceChanged", reclaimCommandID, userID, 0, map[string]any{
		"action": "vip_identity_reclaim", "vip_level": level, "effective_source": effectiveSource,
		"benefit_codes": []string{
			ledger.VipBenefitCodeTitle,
			ledger.VipBenefitCodeBadgeIdentity,
			ledger.VipBenefitCodeMedal,
		},
		"reclaimed_count": len(revokeIDs), "resource_ids": reclaimedResourceIDs,
		"updated_at_ms": nowMS,
	}, nowMS))
	return events, nil
}

// reconcileTieredVipResourceEquipmentTx 先把三类当前身份素材实际收敛到最终 effective level，
// 再删除目录失效或 entitlement 已失效的装备，并按当前等级 auto_equip 恢复空槽位。
// preserveValidSelection 只适用于座驾、头像框等收藏型装扮；身份素材永远不能保留低等级实例。
func (r *Repository) reconcileTieredVipResourceEquipmentTx(ctx context.Context, tx *sql.Tx, userID int64, level int32, effectiveSource string, commandID string, nowMS int64, preserveValidSelection bool) error {
	trialOnly := effectiveSource == ledger.VipEffectiveSourceTrial
	if effectiveSource != ledger.VipEffectiveSourcePaid && effectiveSource != ledger.VipEffectiveSourceTrial {
		level = 0
		effectiveSource = ledger.VipEffectiveSourceNone
	}
	if strings.TrimSpace(commandID) == "" {
		commandID = "vip_reconcile:" + stableHash(fmt.Sprintf("%s|%d|%d|%s", appcode.FromContext(ctx), userID, level, effectiveSource))
	}
	result, err := tx.ExecContext(ctx, `
			DELETE equipment
			FROM user_resource_equipment AS equipment
			LEFT JOIN user_resource_entitlements AS entitlement
			  ON entitlement.app_code = equipment.app_code
			 AND entitlement.user_id = equipment.user_id
			 AND entitlement.entitlement_id = equipment.entitlement_id
			 AND entitlement.resource_id = equipment.resource_id
			WHERE equipment.app_code = ? AND equipment.user_id = ?
			  AND (
			    EXISTS (
			      SELECT 1
			      FROM vip_level_benefits AS mapped
			      JOIN vip_program_configs AS mapped_program ON mapped_program.app_code = mapped.app_code
			       AND mapped_program.program_type = ?
			      WHERE mapped.app_code = equipment.app_code
			        AND mapped.resource_id = equipment.resource_id
			    )
			    OR EXISTS (
			      SELECT 1 FROM vip_benefit_resource_binding_history AS historical
			      WHERE historical.app_code = equipment.app_code
			        AND historical.resource_id = equipment.resource_id
			        AND historical.program_type = ?
			    )
			  )
			  AND (
			    entitlement.entitlement_id IS NULL
			    OR entitlement.status <> 'active'
			    OR entitlement.effective_at_ms > ?
			    OR (entitlement.expires_at_ms <> 0 AND entitlement.expires_at_ms <= ?)
			    OR entitlement.remaining_quantity <= 0
			    OR NOT EXISTS (
			      SELECT 1
			      FROM vip_level_benefits AS current_benefit
			      JOIN resources AS current_resource ON current_resource.app_code = current_benefit.app_code
			        AND current_resource.resource_id = current_benefit.resource_id
			        AND current_resource.status = 'active'
			        AND current_resource.resource_type = current_benefit.resource_type
			      WHERE current_benefit.app_code = equipment.app_code
			        AND current_benefit.level <= ?
			        AND (
			          current_benefit.level = ?
			          OR current_benefit.benefit_code NOT IN (?, ?, ?)
			        )
			        AND current_benefit.status = 'active'
			        AND current_benefit.resource_id = equipment.resource_id
			        AND (? = FALSE OR (current_benefit.trial_enabled = TRUE AND current_benefit.benefit_code <> ?))
			    )
			  )`, appcode.FromContext(ctx), userID,
		ledger.VipProgramTypeTieredPrivilegeV1, ledger.VipProgramTypeTieredPrivilegeV1,
		nowMS, nowMS,
		level, level,
		ledger.VipBenefitCodeTitle,
		ledger.VipBenefitCodeBadgeIdentity,
		ledger.VipBenefitCodeMedal,
		trialOnly, ledger.VipBenefitCodeDailyCoinRebate)
	if err != nil {
		return err
	}
	removed, err := result.RowsAffected()
	if err != nil {
		return err
	}
	items, err := r.listActiveVipBenefitResourcesTx(ctx, tx, level, effectiveSource)
	if err != nil {
		return err
	}
	identityEvents, err := r.reconcileCurrentVipIdentityEntitlementsTx(
		ctx, tx, userID, level, effectiveSource, commandID, nowMS, items,
	)
	if err != nil {
		return err
	}
	validEquippedTypes := map[string]struct{}{}
	if preserveValidSelection && len(items) > 0 {
		validEquippedTypes, err = r.listValidEquippedResourceTypesTx(ctx, tx, userID, nowMS)
		if err != nil {
			return err
		}
	}
	events := make([]walletOutboxEvent, 0, len(items)+len(identityEvents)+1)
	events = append(events, identityEvents...)
	if removed > 0 {
		events = append(events, resourceOutboxEvent("UserResourceChanged", boundedDerivedCommandID("vip_reconcile_remove:", commandID), userID, 0, map[string]any{
			"action": "vip_reconcile", "vip_level": level, "effective_source": effectiveSource,
			"removed_count": removed, "updated_at_ms": nowMS,
		}, nowMS))
	}
	for _, item := range items {
		if !item.AutoEquip || !isEquipableResourceType(item.Resource.ResourceType) {
			continue
		}
		resourceType := resourcedomain.NormalizeResourceType(item.Resource.ResourceType)
		if preserveValidSelection && !allowsMultipleEquippedResources(resourceType) {
			if _, exists := validEquippedTypes[resourceType]; exists {
				// 单选槽已有仍有效且当前 VIP 等级允许的素材时，读取接口只补 entitlement，
				// 不能把用户主动选择的低等级素材覆盖为当前等级默认值。
				continue
			}
		}
		entitlement, err := r.queryVipBenefitEntitlementForUpdateTx(ctx, tx, userID, item.Resource.ResourceID, effectiveSource, nowMS)
		if xerr.IsCode(err, xerr.NotFound) {
			// 调用方会先做 paid/trial 懒补齐；仍缺失说明目录或历史数据不完整，失败关闭展示但不阻塞状态读取。
			continue
		}
		if err != nil {
			return err
		}
		alreadyEquipped, err := r.isUserResourceEntitlementEquippedTx(ctx, tx, userID, entitlement)
		if err != nil {
			return err
		}
		if alreadyEquipped {
			continue
		}
		if err := r.equipUserResourceEntitlementTx(ctx, tx, userID, entitlement, nowMS); err != nil {
			return err
		}
		events = append(events, resourceOutboxEvent("UserResourceChanged", boundedDerivedCommandID("vip_reconcile_equip:", commandID), userID, item.Resource.ResourceID, map[string]any{
			"action": "equip", "vip_level": level, "effective_source": effectiveSource,
			"benefit_code": item.BenefitCode,
			"resource_id":  item.Resource.ResourceID, "resource_type": item.Resource.ResourceType,
			"entitlement_id": entitlement.EntitlementID, "updated_at_ms": nowMS,
		}, nowMS))
	}
	if len(events) == 0 {
		return nil
	}
	return r.insertWalletOutbox(ctx, tx, events)
}

// listValidEquippedResourceTypesTx 一次索引范围查询收集仍可保留的装备槽位。VIP 装备已由
// reconcile 的删除语句完成等级/目录门禁；这里仍校验 entitlement 和普通资源目录状态，
// 避免一个过期或已下架的非 VIP 指针阻止默认素材恢复。
func (r *Repository) listValidEquippedResourceTypesTx(ctx context.Context, tx *sql.Tx, userID int64, nowMS int64) (map[string]struct{}, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT DISTINCT equipment.resource_type
		FROM user_resource_equipment AS equipment
		JOIN user_resource_entitlements AS entitlement
		  ON entitlement.app_code = equipment.app_code
		 AND entitlement.user_id = equipment.user_id
		 AND entitlement.entitlement_id = equipment.entitlement_id
		 AND entitlement.resource_id = equipment.resource_id
		LEFT JOIN resources AS resource
		  ON resource.app_code = equipment.app_code
		 AND resource.resource_id = equipment.resource_id
		WHERE equipment.app_code = ? AND equipment.user_id = ?
		  AND entitlement.status = 'active'
		  AND entitlement.effective_at_ms <= ?
		  AND (entitlement.expires_at_ms = 0 OR entitlement.expires_at_ms > ?)
		  AND entitlement.remaining_quantity > 0
		  AND (
		    entitlement.source_snapshot_id <> ''
		    OR (resource.resource_id IS NOT NULL AND resource.status = 'active')
		  )`,
		appcode.FromContext(ctx), userID, nowMS, nowMS,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	resourceTypes := make(map[string]struct{}, 4)
	for rows.Next() {
		var resourceType string
		if err := rows.Scan(&resourceType); err != nil {
			return nil, err
		}
		resourceTypes[resourcedomain.NormalizeResourceType(resourceType)] = struct{}{}
	}
	return resourceTypes, rows.Err()
}

func (r *Repository) isUserResourceEntitlementEquippedTx(ctx context.Context, tx *sql.Tx, userID int64, entitlement resourcedomain.UserResourceEntitlement) (bool, error) {
	var equipped bool
	err := tx.QueryRowContext(ctx, `
		SELECT EXISTS(
		  SELECT 1 FROM user_resource_equipment
		  WHERE app_code = ? AND user_id = ? AND resource_type = ? AND entitlement_id = ?
		)`, appcode.FromContext(ctx), userID,
		resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType), entitlement.EntitlementID,
	).Scan(&equipped)
	return equipped, err
}

// queryVipBenefitEntitlementForUpdateTx 强制 reconcile 选中最终来源自己的 entitlement。
// trial 还绑定当前佩戴 card subject，避免两张同等级体验卡共享剩余时长不同的资源实例。
func (r *Repository) queryVipBenefitEntitlementForUpdateTx(ctx context.Context, tx *sql.Tx, userID int64, resourceID int64, effectiveSource string, nowMS int64) (resourcedomain.UserResourceEntitlement, error) {
	grantWhere := `g.grant_source = ?`
	args := []any{appcode.FromContext(ctx), userID, resourceID, nowMS, nowMS}
	if effectiveSource == ledger.VipEffectiveSourcePaid {
		args = append(args, resourcedomain.GrantSourceVIPPaid)
	} else if effectiveSource == ledger.VipEffectiveSourceTrial {
		var trialCardID string
		if err := tx.QueryRowContext(ctx, `
			SELECT card.trial_card_id
			FROM user_resource_equipment AS equipment
			JOIN user_vip_trial_cards AS card ON card.app_code = equipment.app_code
			 AND card.user_id = equipment.user_id AND card.entitlement_id = equipment.entitlement_id
			WHERE equipment.app_code = ? AND equipment.user_id = ? AND equipment.resource_type = ?
			LIMIT 1`, appcode.FromContext(ctx), userID, resourcedomain.TypeVIPTrialCard).Scan(&trialCardID); err != nil {
			if errors.Is(err, sql.ErrNoRows) {
				return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "equipped vip trial card not found")
			}
			return resourcedomain.UserResourceEntitlement{}, err
		}
		grantWhere = `((g.grant_source = ? AND g.grant_subject_id = ?) OR g.command_id = ?)`
		args = append(args, resourcedomain.GrantSourceVIPTrial,
			vipTrialBenefitGrantSubjectID(appcode.FromContext(ctx), trialCardID), "vip_trial_benefit:"+trialCardID)
	} else {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "effective vip resource entitlement not found")
	}
	query := userResourceSelectSQL() + `
		JOIN resource_grants AS g ON g.app_code = e.app_code AND g.grant_id = e.source_grant_id
		WHERE e.app_code = ? AND e.user_id = ? AND e.resource_id = ?
		  AND e.status = 'active' AND e.effective_at_ms <= ?
		  AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?) AND e.remaining_quantity > 0
		  AND g.status = 'succeeded' AND ` + grantWhere + `
		ORDER BY e.expires_at_ms DESC, e.created_at_ms DESC
		LIMIT 1 FOR UPDATE`
	item, err := scanUserResourceEntitlement(tx.QueryRowContext(ctx, query, args...))
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "effective vip resource entitlement not found")
	}
	return item, err
}

// isTieredVipBenefitResourceTx 识别当前或历史任一 tiered VIP 绑定；program 状态不会参与，
// 因为关闭 program 时旧 entitlement 也必须失败关闭，而不是变成普通可装备资源。
func (r *Repository) isTieredVipBenefitResourceTx(ctx context.Context, tx *sql.Tx, resourceID int64) (bool, error) {
	var bound bool
	err := tx.QueryRowContext(ctx, `
		SELECT (
		  EXISTS(
		    SELECT 1 FROM vip_level_benefits AS benefit
		    JOIN vip_program_configs AS program ON program.app_code = benefit.app_code
		     AND program.program_type = ?
		    WHERE benefit.app_code = ? AND benefit.resource_id = ?
		  )
		  OR EXISTS(
		    SELECT 1 FROM vip_benefit_resource_binding_history AS history
		    WHERE history.app_code = ? AND history.resource_id = ? AND history.program_type = ?
		  )
		)`, ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx), resourceID,
		appcode.FromContext(ctx), resourceID, ledger.VipProgramTypeTieredPrivilegeV1,
	).Scan(&bound)
	return bound, err
}

// requireCurrentVipResourceBindingForUserTx 是装备提交事务内的最后门禁。它先共享锁 program，
// 再通过 idx_vip_benefit_resource 对单 resource_id 做有界检查；收藏型装扮允许命中低等级，
// 三类当前身份资源必须精确命中 effective level。admin 改绑不能夹在校验与装备写入之间。
func (r *Repository) requireCurrentVipResourceBindingForUserTx(ctx context.Context, tx *sql.Tx, userID int64, resourceID int64, nowMS int64) error {
	bound, err := r.isTieredVipBenefitResourceTx(ctx, tx, resourceID)
	if err != nil || !bound {
		return err
	}
	requirement, err := r.firstTieredVipBenefitRequirementTx(ctx, tx, resourceID)
	if err != nil {
		return err
	}
	program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx)))
	if errors.Is(err, sql.ErrNoRows) {
		return vipBenefitRequiredForResourceEquip(requirement, resourceID, 0)
	}
	if err != nil {
		return err
	}
	if program.ProgramType != ledger.VipProgramTypeTieredPrivilegeV1 || program.Status != ledger.VipStatusActive {
		return vipBenefitRequiredForResourceEquip(requirement, resourceID, 0)
	}
	state, err := r.getVipStateFromSnapshot(ctx, tx, userID, nowMS, program)
	if err != nil {
		return err
	}
	if !state.EffectiveVip.Active || state.EffectiveVip.Level <= 0 {
		return vipBenefitRequiredForResourceEquip(requirement, resourceID, state.EffectiveVip.Level)
	}
	trialOnly := state.EffectiveSource == ledger.VipEffectiveSourceTrial
	if state.EffectiveSource != ledger.VipEffectiveSourcePaid && !trialOnly {
		return vipBenefitRequiredForResourceEquip(requirement, resourceID, state.EffectiveVip.Level)
	}
	var allowed bool
	if err := tx.QueryRowContext(ctx, `
		SELECT EXISTS(
		  SELECT 1
		  FROM vip_level_benefits AS benefit
		  JOIN resources AS resource ON resource.app_code = benefit.app_code
		   AND resource.resource_id = benefit.resource_id
		   AND resource.status = 'active'
		   AND resource.resource_type = benefit.resource_type
		  WHERE benefit.app_code = ? AND benefit.resource_id = ?
		    AND benefit.status = 'active' AND benefit.level <= ?
		    AND (
		      benefit.level = ?
		      OR benefit.benefit_code NOT IN (?, ?, ?)
		    )
		    AND (? = FALSE OR (benefit.trial_enabled = TRUE AND benefit.benefit_code <> ?))
		)`, appcode.FromContext(ctx), resourceID,
		state.EffectiveVip.Level, state.EffectiveVip.Level,
		ledger.VipBenefitCodeTitle,
		ledger.VipBenefitCodeBadgeIdentity,
		ledger.VipBenefitCodeMedal,
		trialOnly, ledger.VipBenefitCodeDailyCoinRebate,
	).Scan(&allowed); err != nil {
		return err
	}
	if allowed {
		return nil
	}
	return vipBenefitRequiredForResourceEquip(requirement, resourceID, state.EffectiveVip.Level)
}

func (r *Repository) firstTieredVipBenefitRequirementTx(ctx context.Context, tx *sql.Tx, resourceID int64) (resourcedomain.VipBenefitRequirement, error) {
	var requirement resourcedomain.VipBenefitRequirement
	err := tx.QueryRowContext(ctx, `
		SELECT binding.benefit_code, MIN(binding.level), binding.resource_type
		FROM (
		  SELECT benefit.benefit_code, benefit.level, benefit.resource_type
		  FROM vip_level_benefits AS benefit
		  JOIN vip_program_configs AS program ON program.app_code = benefit.app_code
		   AND program.program_type = ?
		  WHERE benefit.app_code = ? AND benefit.resource_id = ?
		  UNION ALL
		  SELECT history.benefit_code, history.level, history.resource_type
		  FROM vip_benefit_resource_binding_history AS history
		  WHERE history.app_code = ? AND history.resource_id = ? AND history.program_type = ?
		) AS binding
		GROUP BY binding.benefit_code, binding.resource_type
		ORDER BY MIN(binding.level) ASC, binding.benefit_code ASC
		LIMIT 1`, ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx), resourceID,
		appcode.FromContext(ctx), resourceID, ledger.VipProgramTypeTieredPrivilegeV1,
	).Scan(&requirement.BenefitCode, &requirement.RequiredLevel, &requirement.ResourceType)
	if errors.Is(err, sql.ErrNoRows) {
		return resourcedomain.VipBenefitRequirement{}, xerr.New(xerr.Internal, "vip resource binding history is missing")
	}
	return requirement, err
}

func vipBenefitRequiredForResourceEquip(requirement resourcedomain.VipBenefitRequirement, resourceID int64, currentLevel int32) error {
	return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required for resource equip", map[string]string{
		"benefit_code":            requirement.BenefitCode,
		"required_level":          fmt.Sprintf("%d", requirement.RequiredLevel),
		"current_effective_level": fmt.Sprintf("%d", currentLevel),
		"resource_id":             fmt.Sprintf("%d", resourceID),
		"action":                  "equip_resource",
	})
}

// ensureTieredVipResourceState 是状态读取时的安全懒补齐：老会员无需全表迁移，首次读取时会
// 在单用户事务内补发当前 paid/trial resources，并按最终来源恢复 auto_equip。自然过期的 trial
// 装备指针在调用前已清理，因此这里会自动回落仍有效 paid。
func (r *Repository) ensureTieredVipResourceState(ctx context.Context, userID int64, nowMS int64) error {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()
	program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx)))
	if errors.Is(err, sql.ErrNoRows) {
		return nil
	}
	if err != nil {
		return err
	}
	if program.ProgramType != ledger.VipProgramTypeTieredPrivilegeV1 {
		return tx.Commit()
	}
	// 与购买保持 program -> membership 的统一锁序；无 membership 行时只读取 trial/none。
	var lockedUserID int64
	lockErr := tx.QueryRowContext(ctx, `
		SELECT user_id FROM user_vip_memberships
		WHERE app_code = ? AND user_id = ? FOR UPDATE`, appcode.FromContext(ctx), userID).Scan(&lockedUserID)
	if lockErr != nil && !errors.Is(lockErr, sql.ErrNoRows) {
		return lockErr
	}
	state, err := r.getVipStateFromSnapshot(ctx, tx, userID, nowMS, program)
	if err != nil {
		return err
	}
	if program.Status == ledger.VipStatusActive && program.TrialCardEnabled &&
		state.PaidVip.Active && state.EquippedTrialCard != nil &&
		state.EquippedTrialCard.Level < state.PaidVip.Level {
		// 写规则上线前可能已留下“真实 VIP3 仍佩戴 VIP2 体验卡”的历史事实。
		// 状态读取按单用户懒修复完整回收卡片及其体验资源，不能只隐藏 trial 身份、
		// 继续把卡和资源留在背包中。
		cleanupCommandID := "vip_trial_paid_cleanup:" + stableHash(fmt.Sprintf(
			"%s|%d|%s|%d",
			appcode.FromContext(ctx), userID, state.EquippedTrialCard.EntitlementID, state.PaidVip.Level,
		))
		if err := r.replaceVipTrialCardWithPaidTx(
			ctx,
			tx,
			*state.EquippedTrialCard,
			state.PaidVip.Level,
			cleanupCommandID,
			nowMS,
		); err != nil {
			return err
		}
		state.EquippedTrialCard = nil
	}
	if program.Status == ledger.VipStatusActive && state.PaidVip.Active {
		paid := state.PaidVip
		paid.ProgramType = program.ProgramType
		paid.ConfigVersion = program.ConfigVersion
		if _, _, err := r.grantPaidVipBenefitResourcesTx(
			ctx, tx, paid, state.EffectiveSource == ledger.VipEffectiveSourcePaid, nowMS,
		); err != nil {
			return err
		}
	}
	if program.Status == ledger.VipStatusActive && state.EffectiveSource == ledger.VipEffectiveSourceTrial && state.EquippedTrialCard != nil {
		if err := r.grantVipTrialBenefitResourcesTx(ctx, tx, *state.EquippedTrialCard, program.ConfigVersion, nowMS); err != nil {
			return err
		}
	}
	effectiveLevel := int32(0)
	effectiveSource := ledger.VipEffectiveSourceNone
	if state.EffectiveVip.Active {
		effectiveLevel = state.EffectiveVip.Level
		effectiveSource = state.EffectiveSource
	}
	commandID := "vip_state_sync:" + stableHash(fmt.Sprintf("%s|%d|%d|%s|%d", appcode.FromContext(ctx), userID, effectiveLevel, effectiveSource, program.ConfigVersion))
	if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, userID, effectiveLevel, effectiveSource, commandID, nowMS, true); err != nil {
		return err
	}
	return tx.Commit()
}

type vipResourceBindingKey struct {
	Level      int32
	ResourceID int64
}

type currentVipResourceBinding struct {
	ResourceType      string
	PaidBenefitCodes  []string
	TrialBenefitCodes []string
}

type vipBatchEffectiveLevels struct {
	Paid             int32
	PaidExpiresAtMS  int64
	Trial            int32
	TrialExpiresAtMS int64
}

// filterCurrentVipEquipmentBatch 对整个 appearance/背包批次只执行三次配置/用户事实查询：
// 1) 共享锁 program；2) 一次 current+history resource mapping；3) 一次 paid+trial effective level。
// 查询数量不随 user 数增长，且所有条件都以 app_code 开头，避免高频房间列表退化成 N+1 或全表扫描。
// preserveNonExclusiveInventory=true 时，非身份型 VIP 收藏仍留在背包，只把三类身份资源收敛到最高有效等级。
func (r *Repository) filterCurrentVipEquipmentBatch(ctx context.Context, userIDs []int64, grouped map[int64][]resourcedomain.UserResourceEntitlement, nowMS int64, preserveNonExclusiveInventory bool) error {
	resourceIDSet := make(map[int64]struct{}, 8)
	for _, resources := range grouped {
		for _, entitlement := range resources {
			if entitlement.ResourceID > 0 {
				resourceIDSet[entitlement.ResourceID] = struct{}{}
			}
		}
	}
	if len(resourceIDSet) == 0 {
		return nil
	}
	resourceIDs := make([]int64, 0, len(resourceIDSet))
	for resourceID := range resourceIDSet {
		resourceIDs = append(resourceIDs, resourceID)
	}
	sort.Slice(resourceIDs, func(left, right int) bool { return resourceIDs[left] < resourceIDs[right] })

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()
	program, programErr := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx)))
	if programErr != nil && !errors.Is(programErr, sql.ErrNoRows) {
		return programErr
	}

	mappingArgs := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(resourceIDs)...)
	mappingArgs = append(mappingArgs, ledger.VipProgramTypeTieredPrivilegeV1,
		ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx))
	mappingArgs = append(mappingArgs, int64AnyArgs(resourceIDs)...)
	rows, err := tx.QueryContext(ctx, `
		SELECT history.resource_id, history.level, history.resource_type, history.benefit_code,
		       history.trial_enabled, FALSE AS current_allowed
		FROM vip_benefit_resource_binding_history AS history
		WHERE history.app_code = ? AND history.resource_id IN (`+placeholders(len(resourceIDs))+`)
		  AND history.program_type = ?
		UNION ALL
		SELECT benefit.resource_id, benefit.level, benefit.resource_type, benefit.benefit_code,
		       (benefit.trial_enabled AND benefit.benefit_code <> 'daily_coin_rebate') AS trial_enabled,
		       (benefit.status = 'active' AND resource.resource_id IS NOT NULL) AS current_allowed
		FROM vip_level_benefits AS benefit
		JOIN vip_program_configs AS current_program ON current_program.app_code = benefit.app_code
		 AND current_program.program_type = ?
		LEFT JOIN resources AS resource ON resource.app_code = benefit.app_code
		 AND resource.resource_id = benefit.resource_id
		 AND resource.status = 'active' AND resource.resource_type = benefit.resource_type
		WHERE benefit.app_code = ? AND benefit.resource_id IN (`+placeholders(len(resourceIDs))+`)`, mappingArgs...,
	)
	if err != nil {
		return err
	}
	historical := make(map[int64]struct{}, len(resourceIDs))
	currentLevelOnly := make(map[int64]struct{}, len(resourceIDs))
	bindings := make(map[vipResourceBindingKey]currentVipResourceBinding, len(resourceIDs))
	for rows.Next() {
		var resourceID int64
		var level int32
		var resourceType string
		var benefitCode string
		var trialEnabled bool
		var currentAllowed bool
		if err := rows.Scan(&resourceID, &level, &resourceType, &benefitCode, &trialEnabled, &currentAllowed); err != nil {
			_ = rows.Close()
			return err
		}
		historical[resourceID] = struct{}{}
		if isCurrentLevelOnlyVipBenefit(benefitCode) {
			currentLevelOnly[resourceID] = struct{}{}
		}
		if !currentAllowed || level <= 0 {
			continue
		}
		key := vipResourceBindingKey{Level: level, ResourceID: resourceID}
		binding := bindings[key]
		binding.ResourceType = resourcedomain.NormalizeResourceType(resourceType)
		binding.PaidBenefitCodes = appendUniqueNormalizedVIPCode(binding.PaidBenefitCodes, benefitCode)
		if trialEnabled {
			binding.TrialBenefitCodes = appendUniqueNormalizedVIPCode(binding.TrialBenefitCodes, benefitCode)
		}
		bindings[key] = binding
	}
	if err := rows.Err(); err != nil {
		_ = rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	if len(historical) == 0 {
		return tx.Commit()
	}
	if programErr != nil || program.ProgramType != ledger.VipProgramTypeTieredPrivilegeV1 || program.Status != ledger.VipStatusActive {
		filterVipEquipmentByAccess(grouped, historical, currentLevelOnly, bindings, nil, false, preserveNonExclusiveInventory, nowMS)
		return tx.Commit()
	}

	levels, err := r.batchVipEffectiveLevelsFromSnapshot(ctx, tx, userIDs, nowMS)
	if err != nil {
		return err
	}
	filterVipEquipmentByAccess(grouped, historical, currentLevelOnly, bindings, levels, program.TrialCardEnabled, preserveNonExclusiveInventory, nowMS)
	return tx.Commit()
}

func filterVipEquipmentByAccess(grouped map[int64][]resourcedomain.UserResourceEntitlement, historical map[int64]struct{}, currentLevelOnly map[int64]struct{}, bindings map[vipResourceBindingKey]currentVipResourceBinding, levels map[int64]vipBatchEffectiveLevels, trialCardEnabled bool, preserveNonExclusiveInventory bool, nowMS int64) {
	for userID, resources := range grouped {
		filtered := resources[:0]
		access := levels[userID]
		effectiveLevel := access.Paid
		effectiveSource := ledger.VipEffectiveSourcePaid
		if trialCardEnabled && access.Trial > 0 && access.Trial >= access.Paid {
			effectiveLevel = access.Trial
			effectiveSource = ledger.VipEffectiveSourceTrial
		}
		for _, entitlement := range resources {
			if _, vipBound := historical[entitlement.ResourceID]; !vipBound {
				filtered = append(filtered, entitlement)
				continue
			}
			benefitCodes := currentVipBenefitCodesAtOrBelow(bindings, effectiveLevel, effectiveSource,
				entitlement.ResourceID, entitlement.Resource.ResourceType)
			if preserveNonExclusiveInventory {
				if _, exclusive := currentLevelOnly[entitlement.ResourceID]; exclusive {
					// 回收记录继续保留审计，但不再属于用户可见资产；即使调用方请求
					// active_only=false，也不能把 revoked/expired 的当前等级实例重新暴露。
					if len(benefitCodes) == 0 ||
						entitlement.Status != resourcedomain.StatusActive ||
						entitlement.EffectiveAtMS > nowMS ||
						(entitlement.ExpiresAtMS != 0 && entitlement.ExpiresAtMS <= nowMS) ||
						entitlement.RemainingQuantity <= 0 {
						continue
					}
				}
				if entitlement.Equipped && len(benefitCodes) == 0 {
					entitlement.Equipped = false
				}
				if len(benefitCodes) > 0 {
					entitlement.VIPBenefitCodes = benefitCodes
				}
				filtered = append(filtered, entitlement)
				continue
			}
			if len(benefitCodes) == 0 {
				continue
			}
			// 同为 badge 的 VIP 身份标识、勋章和普通徽章不能靠素材名称区分；将 wallet
			// 已完成等级、trial 和改绑校验后的资源级权益事实随 entitlement 一并投影。
			entitlement.VIPBenefitCodes = benefitCodes
			filtered = append(filtered, entitlement)
		}
		grouped[userID] = filtered
	}
}

// currentVipBenefitCodesAtOrBelow 只在后台配置的有限 VIP 等级内倒序查找；称号、长徽章和
// 短徽章只接受 effective level，其余收藏型资源仍可从低等级命中。
func currentVipBenefitCodesAtOrBelow(bindings map[vipResourceBindingKey]currentVipResourceBinding, effectiveLevel int32, effectiveSource string, resourceID int64, resourceType string) []string {
	resourceType = resourcedomain.NormalizeResourceType(resourceType)
	result := make([]string, 0, 2)
	for level := effectiveLevel; level > 0; level-- {
		binding, exists := bindings[vipResourceBindingKey{Level: level, ResourceID: resourceID}]
		if !exists || binding.ResourceType != resourceType {
			continue
		}
		codes := binding.PaidBenefitCodes
		if effectiveSource == ledger.VipEffectiveSourceTrial {
			codes = binding.TrialBenefitCodes
		}
		for _, code := range codes {
			if level != effectiveLevel && isCurrentLevelOnlyVipBenefit(code) {
				continue
			}
			result = appendUniqueNormalizedVIPCode(result, code)
		}
	}
	return result
}

func appendUniqueNormalizedVIPCode(values []string, value string) []string {
	normalized := strings.ToLower(strings.TrimSpace(value))
	if normalized == "" {
		return values
	}
	for _, existing := range values {
		if existing == normalized {
			return values
		}
	}
	return append(values, normalized)
}
