package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

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

type vipProgramScanner interface {
	Scan(dest ...any) error
}

type vipBenefitRows interface {
	Next() bool
	Scan(dest ...any) error
	Err() error
	Close() error
}

// vipSnapshotQuerier 是 VIP 组合读模型的最小查询能力；*sql.Tx 和 *sql.DB 都满足。
// 组合读取统一接收该接口，防止 helper 意外逃逸到 r.db、破坏调用者的 repeatable-read 快照。
type vipSnapshotQuerier interface {
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

// GetVipProgramConfig 返回当前 App 唯一的 VIP 状态机配置和用于后台展示的权益目录。
// 权益目录取最高等级的显式集合；运行时仍只查询用户有效等级对应的那一组数据。
func (r *Repository) GetVipProgramConfig(ctx context.Context) (ledger.VipProgramConfig, []ledger.VipBenefit, error) {
	if r == nil || r.db == nil {
		return ledger.VipProgramConfig{}, nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	defer func() { _ = tx.Rollback() }()
	config, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	benefits, err := r.listVipLevelBenefits(ctx, tx, config.LevelCount, false)
	return config, benefits, err
}

// GetVipState 合并付费会员和当前佩戴体验卡。低等级切换由 EquipVipTrialCard 在写事务拦截；
// 卡片过期、卸下或 App 关闭体验卡能力后都会自然回落到仍有效的付费会员。
func (r *Repository) GetVipState(ctx context.Context, userID int64) (ledger.VipState, error) {
	if r == nil || r.db == nil {
		return ledger.VipState{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if userID <= 0 {
		return ledger.VipState{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	nowMS := time.Now().UnixMilli()
	// 失效 trial-card equipment 由 Wallet 单写者后台物理清理；这里直接读取同一
	// repeatable-read 快照，避免用户态查询产生 DELETE，同时防止返回跨配置版本组合。
	if err := r.ensureTieredVipResourceState(ctx, userID, nowMS); err != nil {
		return ledger.VipState{}, err
	}
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return ledger.VipState{}, err
	}
	defer func() { _ = tx.Rollback() }()
	config, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return ledger.VipState{}, err
	}
	return r.getVipStateFromSnapshot(ctx, tx, userID, nowMS, config)
}

// BatchGetUserEffectiveVipLevels 在同一个 repeatable-read 快照内读取 App program 和整批
// paid/trial 事实。查询数固定为两次，且用户条件都命中 (app_code,user_id) 复合索引。
func (r *Repository) BatchGetUserEffectiveVipLevels(ctx context.Context, userIDs []int64, nowMS int64) ([]ledger.UserEffectiveVipLevel, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	userIDs = compactPositiveInt64s(userIDs)
	if len(userIDs) == 0 {
		return []ledger.UserEffectiveVipLevel{}, nil
	}
	if len(userIDs) > 100 {
		return nil, xerr.New(xerr.InvalidArgument, "effective vip batch is too large")
	}

	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()
	program, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return nil, err
	}

	levels := make(map[int64]vipBatchEffectiveLevels, len(userIDs))
	if program.Status == ledger.VipStatusActive {
		levels, err = r.batchVipEffectiveLevelsFromSnapshot(ctx, tx, userIDs, nowMS)
		if err != nil {
			return nil, err
		}
	}
	result := make([]ledger.UserEffectiveVipLevel, 0, len(userIDs))
	for _, userID := range userIDs {
		access := levels[userID]
		item := ledger.UserEffectiveVipLevel{
			UserID: userID,
			Source: ledger.VipEffectiveSourceNone,
		}
		if access.Paid > 0 {
			item.Level = access.Paid
			item.ExpiresAtMS = access.PaidExpiresAtMS
			item.Source = ledger.VipEffectiveSourcePaid
		}
		if program.TrialCardEnabled && access.Trial > 0 && access.Trial >= access.Paid {
			item.Level = access.Trial
			item.ExpiresAtMS = access.TrialExpiresAtMS
			item.Source = ledger.VipEffectiveSourceTrial
		}
		result = append(result, item)
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return result, nil
}

// batchAllowedVipBenefits 在同一个只读快照内批量计算展示链路需要的最终权益。
// 查询数量固定为 program、权益矩阵、用户 effective level、用户设置四次，且用户事实均以
// (app_code,user_id) 索引收敛，不能退化成榜单每个用户各调一次 CheckVipBenefit。
func (r *Repository) batchAllowedVipBenefits(ctx context.Context, userIDs []int64, rawBenefitCodes []string, nowMS int64) (map[int64][]string, map[int64]int32, error) {
	result := make(map[int64][]string, len(userIDs))
	effectiveLevels := make(map[int64]int32, len(userIDs))
	userIDs = compactPositiveInt64s(userIDs)
	benefitCodes := normalizeVipBenefitCodes(rawBenefitCodes)
	if len(userIDs) == 0 || len(benefitCodes) == 0 {
		return result, effectiveLevels, nil
	}

	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return nil, nil, err
	}
	defer func() { _ = tx.Rollback() }()
	program, err := r.requireVipProgramConfig(ctx, tx)
	if err != nil {
		return nil, nil, err
	}
	if program.Status != ledger.VipStatusActive {
		return result, effectiveLevels, tx.Commit()
	}

	type benefitAccess struct {
		TrialEnabled bool
	}
	benefits := make(map[string]benefitAccess, len(benefitCodes)*int(program.LevelCount))
	benefitArgs := append([]any{appcode.FromContext(ctx)}, stringAnyArgs(benefitCodes)...)
	benefitArgs = append(benefitArgs, ledger.VipStatusActive)
	benefitRows, err := tx.QueryContext(ctx, `
		SELECT level, benefit_code, trial_enabled
		FROM vip_level_benefits FORCE INDEX (idx_vip_benefit_code)
		WHERE app_code = ? AND benefit_code IN (`+placeholders(len(benefitCodes))+`)
		  AND status = ?`, benefitArgs...)
	if err != nil {
		return nil, nil, err
	}
	for benefitRows.Next() {
		var level int32
		var code string
		var trialEnabled bool
		if err := benefitRows.Scan(&level, &code, &trialEnabled); err != nil {
			_ = benefitRows.Close()
			return nil, nil, err
		}
		benefits[fmt.Sprintf("%d|%s", level, strings.ToLower(strings.TrimSpace(code)))] = benefitAccess{TrialEnabled: trialEnabled}
	}
	if err := benefitRows.Err(); err != nil {
		_ = benefitRows.Close()
		return nil, nil, err
	}
	if err := benefitRows.Close(); err != nil {
		return nil, nil, err
	}

	levels, err := r.batchVipEffectiveLevelsFromSnapshot(ctx, tx, userIDs, nowMS)
	if err != nil {
		return nil, nil, err
	}
	settings := make(map[int64]ledger.VipUserSettings, len(userIDs))
	for _, userID := range userIDs {
		settings[userID] = ledger.DefaultVipUserSettings(appcode.FromContext(ctx), userID)
	}
	settingArgs := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...)
	settingRows, err := tx.QueryContext(ctx, `
		SELECT user_id, room_entry_notice_enabled, online_global_notice_enabled,
		       hide_profile_data_enabled, anonymous_profile_visit_enabled,
		       leaderboard_invisible_enabled, updated_at_ms
		FROM user_vip_settings FORCE INDEX (PRIMARY)
		WHERE app_code = ? AND user_id IN (`+placeholders(len(userIDs))+`)`, settingArgs...)
	if err != nil {
		return nil, nil, err
	}
	for settingRows.Next() {
		var value ledger.VipUserSettings
		value.AppCode = appcode.FromContext(ctx)
		if err := settingRows.Scan(
			&value.UserID, &value.RoomEntryNoticeEnabled, &value.OnlineGlobalNoticeEnabled,
			&value.HideProfileDataEnabled, &value.AnonymousProfileVisitEnabled,
			&value.LeaderboardInvisibleEnabled, &value.UpdatedAtMS,
		); err != nil {
			_ = settingRows.Close()
			return nil, nil, err
		}
		settings[value.UserID] = value
	}
	if err := settingRows.Err(); err != nil {
		_ = settingRows.Close()
		return nil, nil, err
	}
	if err := settingRows.Close(); err != nil {
		return nil, nil, err
	}

	for _, userID := range userIDs {
		access := levels[userID]
		effectiveLevel := access.Paid
		effectiveSource := ledger.VipEffectiveSourcePaid
		if program.TrialCardEnabled && access.Trial > 0 && access.Trial >= access.Paid {
			effectiveLevel = access.Trial
			effectiveSource = ledger.VipEffectiveSourceTrial
		}
		effectiveLevels[userID] = effectiveLevel
		if effectiveLevel <= 0 {
			continue
		}
		for _, code := range benefitCodes {
			benefit, exists := benefits[fmt.Sprintf("%d|%s", effectiveLevel, code)]
			if !exists || !settings[userID].AllowsBenefitSetting(code) {
				continue
			}
			// 体验卡只能执行该等级显式允许 trial 的权益；金币返现保持付费会员硬边界。
			if effectiveSource == ledger.VipEffectiveSourceTrial && (!benefit.TrialEnabled || code == ledger.VipBenefitCodeDailyCoinRebate) {
				continue
			}
			result[userID] = append(result[userID], code)
		}
	}
	return result, effectiveLevels, tx.Commit()
}

func normalizeVipBenefitCodes(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		code := strings.ToLower(strings.TrimSpace(value))
		if code == "" {
			continue
		}
		if _, exists := seen[code]; exists {
			continue
		}
		seen[code] = struct{}{}
		result = append(result, code)
	}
	return result
}

// batchVipEffectiveLevelsFromSnapshot 复用 GetVipState 的 paid/trial 优先级事实，但按一批用户
// 只读取一次 membership 和一次已佩戴体验卡集合。所有时间条件与单用户读取保持一致。
func (r *Repository) batchVipEffectiveLevelsFromSnapshot(ctx context.Context, querier vipSnapshotQuerier, userIDs []int64, nowMS int64) (map[int64]vipBatchEffectiveLevels, error) {
	accessArgs := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...)
	accessArgs = append(accessArgs, nowMS, appcode.FromContext(ctx))
	accessArgs = append(accessArgs, int64AnyArgs(userIDs)...)
	accessArgs = append(accessArgs, resourcedomain.TypeVIPTrialCard, ledger.VipTrialCardStatusActive,
		nowMS, nowMS, nowMS, nowMS)
	rows, err := querier.QueryContext(ctx, `
		SELECT membership.user_id, membership.level, membership.expires_at_ms, 'paid' AS effective_source
		FROM user_vip_memberships AS membership FORCE INDEX (PRIMARY)
		WHERE membership.app_code = ? AND membership.user_id IN (`+placeholders(len(userIDs))+`)
		  AND membership.level > 0 AND membership.expires_at_ms > ?
		UNION ALL
		SELECT STRAIGHT_JOIN card.user_id, card.level, card.expires_at_ms, 'trial' AS effective_source
		FROM user_resource_equipment AS equipment FORCE INDEX (PRIMARY)
		JOIN user_vip_trial_cards AS card FORCE INDEX (uk_vip_trial_card_entitlement) 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
		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 IN (`+placeholders(len(userIDs))+`)
		  AND equipment.resource_type = ? 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 AND resource.status = 'active'`, accessArgs...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	levels := make(map[int64]vipBatchEffectiveLevels, len(userIDs))
	for rows.Next() {
		var userID int64
		var level int32
		var expiresAtMS int64
		var source string
		if err := rows.Scan(&userID, &level, &expiresAtMS, &source); err != nil {
			return nil, err
		}
		access := levels[userID]
		if source == ledger.VipEffectiveSourceTrial {
			access.Trial = level
			access.TrialExpiresAtMS = expiresAtMS
		} else {
			access.Paid = level
			access.PaidExpiresAtMS = expiresAtMS
		}
		levels[userID] = access
	}
	return levels, rows.Err()
}

// requireVipProgramConfig 从调用者指定的快照读取 App 配置，并把缺迁移/错 app_code 明确映射为业务错误。
func (r *Repository) requireVipProgramConfig(ctx context.Context, querier vipSnapshotQuerier) (ledger.VipProgramConfig, error) {
	config, err := r.queryVipProgramConfig(ctx, querier.QueryRowContext(ctx, vipProgramSelectSQL(), appcode.FromContext(ctx)))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.VipProgramConfig{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found")
	}
	return config, err
}

// getVipStateFromSnapshot 只使用调用者事务装配 paid、已佩戴卡片、最终身份与权益。
// raw 事实始终返回；program 总开关关闭时不产生 effective VIP，体验卡开关关闭时则仅回落 paid。
func (r *Repository) getVipStateFromSnapshot(ctx context.Context, querier vipSnapshotQuerier, userID int64, nowMS int64, config ledger.VipProgramConfig) (ledger.VipState, error) {
	paid, err := r.queryUserVip(ctx, querier, userID, nowMS, false)
	if errors.Is(err, sql.ErrNoRows) {
		paid = ledger.UserVip{UserID: userID, ProgramType: config.ProgramType, ConfigVersion: config.ConfigVersion}
	} else if err != nil {
		return ledger.VipState{}, err
	}

	trial, err := r.queryEquippedVipTrialCardWithQuery(ctx, querier, userID, nowMS)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return ledger.VipState{}, err
	}
	if errors.Is(err, sql.ErrNoRows) {
		trial = nil
	}
	settings, err := r.queryVipUserSettings(ctx, querier, userID, false)
	if err != nil {
		return ledger.VipState{}, err
	}

	state := ledger.VipState{
		PaidVip:           paid,
		EquippedTrialCard: trial,
		EffectiveSource:   ledger.VipEffectiveSourceNone,
		EvaluatedAtMS:     nowMS,
		ProgramConfig:     config,
		UserSettings:      settings,
		EffectiveVip: ledger.UserVip{
			UserID:        userID,
			ProgramType:   config.ProgramType,
			ConfigVersion: config.ConfigVersion,
		},
	}
	if config.Status != ledger.VipStatusActive {
		return state, nil
	}
	if paid.Active {
		state.EffectiveVip = paid
		state.EffectiveSource = ledger.VipEffectiveSourcePaid
	}
	// 购买等级覆盖体验等级时，购买事务会直接回收卡片；更低等级的历史脏装备先按
	// paid 计算最终身份，再由 ensureTieredVipResourceState 在单用户事务内完整回收。
	if config.TrialCardEnabled && trial != nil && (!paid.Active || trial.Level >= paid.Level) {
		state.EffectiveVip = ledger.UserVip{
			UserID:        userID,
			Level:         trial.Level,
			Name:          trial.Name,
			Active:        true,
			StartedAtMS:   trial.EffectiveAtMS,
			ExpiresAtMS:   trial.ExpiresAtMS,
			UpdatedAtMS:   trial.UpdatedAtMS,
			ProgramType:   config.ProgramType,
			ConfigVersion: config.ConfigVersion,
		}
		state.EffectiveSource = ledger.VipEffectiveSourceTrial
	}
	if !state.EffectiveVip.Active || state.EffectiveVip.Level <= 0 {
		return state, nil
	}
	benefits, err := r.listVipLevelBenefits(ctx, querier, state.EffectiveVip.Level, true)
	if err != nil {
		return ledger.VipState{}, err
	}
	if state.EffectiveSource == ledger.VipEffectiveSourceTrial {
		filtered := benefits[:0]
		for _, benefit := range benefits {
			// 金币返现是付费会员硬边界，不能依赖后台复选框；即使历史配置误写为 true 也必须二次排除。
			if benefit.TrialEnabled && benefit.BenefitCode != ledger.VipBenefitCodeDailyCoinRebate {
				filtered = append(filtered, benefit)
			}
		}
		benefits = filtered
	}
	state.EffectiveBenefits = benefits
	return state, nil
}

// CheckVipBenefit 只匹配 wallet 已合并并过滤后的 effective_benefits，不能退化成 level >= N。
func (r *Repository) CheckVipBenefit(ctx context.Context, userID int64, benefitCode string) (ledger.CheckVipBenefitResult, error) {
	state, err := r.GetVipState(ctx, userID)
	if err != nil {
		return ledger.CheckVipBenefitResult{}, err
	}
	code := strings.ToLower(strings.TrimSpace(benefitCode))
	var requiredLevel int32
	// idx_vip_benefit_code(app_code, benefit_code, status, level) 让该 MIN 只扫描单 App、
	// 单权益的 active 索引区间，不会随所有等级和权益目录增长退化成全表扫描。
	if err := r.db.QueryRowContext(ctx, `
		SELECT COALESCE(MIN(level), 0)
		FROM vip_level_benefits
		WHERE app_code = ? AND benefit_code = ? AND status = ?`,
		appcode.FromContext(ctx), code, ledger.VipStatusActive,
	).Scan(&requiredLevel); err != nil {
		return ledger.CheckVipBenefitResult{}, err
	}
	result := ledger.CheckVipBenefitResult{
		State:                 state,
		DenialReason:          "benefit_not_enabled",
		EvaluatedAtMS:         state.EvaluatedAtMS,
		RequiredLevel:         requiredLevel,
		CurrentEffectiveLevel: state.EffectiveVip.Level,
	}
	if !state.EffectiveVip.Active {
		result.DenialReason = "vip_inactive"
		return result, nil
	}
	for _, benefit := range state.EffectiveBenefits {
		if strings.ToLower(strings.TrimSpace(benefit.BenefitCode)) != code {
			continue
		}
		result.Benefit = benefit
		// effective_benefits 保留用户实际拥有的权益资格；可关闭通知只在执行时做设置门禁，
		// 从而客户端关闭后仍能看见权益并重新开启。
		if !state.UserSettings.AllowsBenefitSetting(code) {
			result.DenialReason = "user_setting_disabled"
			return result, nil
		}
		result.Allowed = true
		result.DenialReason = ""
		return result, nil
	}
	return result, nil
}

// CheckVipResourceBenefit 校验具体装扮资源是否属于当前 effective VIP 等级或任一更低等级。
// 查询先由 idx_vip_benefit_resource 把范围收敛到单 App、单 resource_id，再受最多 level_count
// 个等级约束；它不会扫描用户背包，也不会改变 target_only 功能权益的执行语义。
func (r *Repository) CheckVipResourceBenefit(ctx context.Context, userID int64, benefitCode string, resourceID int64, resourceType string) (ledger.CheckVipBenefitResult, error) {
	state, err := r.GetVipState(ctx, userID)
	if err != nil {
		return ledger.CheckVipBenefitResult{}, err
	}
	code := strings.ToLower(strings.TrimSpace(benefitCode))
	resourceType = resourcedomain.NormalizeResourceType(resourceType)
	var requiredLevel int32
	if err := r.db.QueryRowContext(ctx, `
		SELECT COALESCE(MIN(b.level), 0)
		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 b.resource_id = ? AND b.status = ?
		  AND b.benefit_code = ? AND b.resource_type = ?`,
		appcode.FromContext(ctx), resourceID, ledger.VipStatusActive, code, resourceType,
	).Scan(&requiredLevel); err != nil {
		return ledger.CheckVipBenefitResult{}, err
	}
	result := ledger.CheckVipBenefitResult{
		State: state, DenialReason: "resource_benefit_not_enabled", EvaluatedAtMS: state.EvaluatedAtMS,
		RequiredLevel: requiredLevel, CurrentEffectiveLevel: state.EffectiveVip.Level,
	}
	if !state.EffectiveVip.Active {
		result.DenialReason = "vip_inactive"
		return result, nil
	}
	trialOnly := state.EffectiveSource == ledger.VipEffectiveSourceTrial
	if state.EffectiveSource != ledger.VipEffectiveSourcePaid && !trialOnly {
		return result, nil
	}
	row := r.db.QueryRowContext(ctx, `
		SELECT b.benefit_code, b.name, b.benefit_type, b.unlock_level, b.status, b.trial_enabled,
		       b.resource_id, b.resource_type, b.execution_scope, b.auto_equip, b.sort_order,
		       b.metadata_json, b.created_at_ms, b.updated_at_ms,
		       `+nullableResourceColumnsWithAlias("resource")+`
		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 b.resource_id = ? AND b.status = ?
		  AND b.benefit_code = ? AND b.resource_type = ? AND b.level <= ?
		  AND (? = FALSE OR (b.trial_enabled = TRUE AND b.benefit_code <> ?))
		ORDER BY b.level DESC
		LIMIT 1`,
		appcode.FromContext(ctx), resourceID, ledger.VipStatusActive, code, resourceType,
		state.EffectiveVip.Level, trialOnly, ledger.VipBenefitCodeDailyCoinRebate,
	)
	benefit, err := scanVipBenefit(row)
	if errors.Is(err, sql.ErrNoRows) {
		return result, nil
	}
	if err != nil {
		return ledger.CheckVipBenefitResult{}, err
	}
	result.Allowed = true
	result.Benefit = benefit
	result.DenialReason = ""
	return result, nil
}

func vipProgramSelectSQL() string {
	return `
		SELECT app_code, program_type, level_count, same_level_expiry_policy,
		       upgrade_expiry_policy, downgrade_purchase_policy, benefit_inheritance_policy,
		       grant_mode, trial_card_enabled, status, config_version, updated_by_admin_id,
		       created_at_ms, updated_at_ms
		FROM vip_program_configs
		WHERE app_code = ?`
}

func (r *Repository) queryVipProgramConfig(ctx context.Context, scanner vipProgramScanner) (ledger.VipProgramConfig, error) {
	var config ledger.VipProgramConfig
	err := scanner.Scan(
		&config.AppCode,
		&config.ProgramType,
		&config.LevelCount,
		&config.SameLevelExpiryPolicy,
		&config.UpgradeExpiryPolicy,
		&config.DowngradePurchasePolicy,
		&config.BenefitInheritancePolicy,
		&config.GrantMode,
		&config.TrialCardEnabled,
		&config.Status,
		&config.ConfigVersion,
		&config.UpdatedByAdminID,
		&config.CreatedAtMS,
		&config.UpdatedAtMS,
	)
	return config, err
}

type vipBenefitQuerier interface {
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
}

func (r *Repository) listVipLevelBenefits(ctx context.Context, querier vipBenefitQuerier, level int32, activeOnly bool) ([]ledger.VipBenefit, error) {
	query := `
		SELECT b.benefit_code, b.name, b.benefit_type, b.unlock_level, b.status, b.trial_enabled,
		       b.resource_id, b.resource_type, b.execution_scope, b.auto_equip, b.sort_order,
		       b.metadata_json, b.created_at_ms, b.updated_at_ms,
		       ` + nullableResourceColumnsWithAlias("r") + `
		FROM vip_level_benefits AS b
		LEFT JOIN resources AS r ON r.app_code = b.app_code
		 AND r.resource_id = b.resource_id
		 AND r.status = 'active'
		 AND r.resource_type = b.resource_type
		WHERE b.app_code = ? AND b.level = ?`
	args := []any{appcode.FromContext(ctx), level}
	if activeOnly {
		query += ` AND b.status = ?`
		args = append(args, ledger.VipStatusActive)
	}
	query += ` ORDER BY b.sort_order DESC, b.benefit_code ASC`
	rows, err := querier.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	benefits := make([]ledger.VipBenefit, 0)
	for rows.Next() {
		benefit, err := scanVipBenefit(rows)
		if err != nil {
			return nil, err
		}
		benefits = append(benefits, benefit)
	}
	return benefits, rows.Err()
}

func scanVipBenefit(scanner vipProgramScanner) (ledger.VipBenefit, error) {
	var benefit ledger.VipBenefit
	var metadata sql.NullString
	var resource resourcedomain.Resource
	var resourceScopesJSON string
	err := scanner.Scan(
		&benefit.BenefitCode,
		&benefit.Name,
		&benefit.BenefitType,
		&benefit.UnlockLevel,
		&benefit.Status,
		&benefit.TrialEnabled,
		&benefit.ResourceID,
		&benefit.ResourceType,
		&benefit.ExecutionScope,
		&benefit.AutoEquip,
		&benefit.SortOrder,
		&metadata,
		&benefit.CreatedAtMS,
		&benefit.UpdatedAtMS,
		&resource.AppCode,
		&resource.ResourceID,
		&resource.ResourceCode,
		&resource.ResourceType,
		&resource.Name,
		&resource.Status,
		&resource.Grantable,
		&resource.ManagerGrantEnabled,
		&resource.GrantStrategy,
		&resource.WalletAssetType,
		&resource.WalletAssetAmount,
		&resource.PriceType,
		&resource.CoinPrice,
		&resource.GiftPointAmount,
		&resourceScopesJSON,
		&resource.AssetURL,
		&resource.PreviewURL,
		&resource.AnimationURL,
		&resource.MetadataJSON,
		&resource.SortOrder,
		&resource.CreatedByUserID,
		&resource.UpdatedByUserID,
		&resource.CreatedAtMS,
		&resource.UpdatedAtMS,
	)
	if metadata.Valid {
		benefit.MetadataJSON = metadata.String
	}
	if err == nil {
		// presentation 是 metadata_json 的稳定结构化投影；读取时统一解析，HTTP/gRPC 调用方
		// 无需按 benefit_code 重复维护 JSON schema。
		benefit.Presentation, err = ledger.ParseVipBenefitPresentation(benefit.MetadataJSON)
	}
	if err == nil && resource.ResourceID > 0 {
		resource.UsageScopes = parseStringArray(resourceScopesJSON)
		benefit.Resource = vipBenefitResourceProjection(resource)
	}
	return benefit, err
}

func vipBenefitResourceProjection(resource resourcedomain.Resource) *ledger.VipBenefitResource {
	if resource.ResourceID <= 0 {
		return nil
	}
	return &ledger.VipBenefitResource{
		AppCode: resource.AppCode, ResourceID: resource.ResourceID, ResourceCode: resource.ResourceCode,
		ResourceType: resource.ResourceType, Name: resource.Name, Status: resource.Status,
		Grantable: resource.Grantable, ManagerGrantEnabled: resource.ManagerGrantEnabled,
		GrantStrategy: resource.GrantStrategy, WalletAssetType: resource.WalletAssetType,
		WalletAssetAmount: resource.WalletAssetAmount, PriceType: resource.PriceType,
		CoinPrice: resource.CoinPrice, GiftPointAmount: resource.GiftPointAmount,
		UsageScopes: resource.UsageScopes, AssetURL: resource.AssetURL, PreviewURL: resource.PreviewURL,
		AnimationURL: resource.AnimationURL, MetadataJSON: resource.MetadataJSON, SortOrder: resource.SortOrder,
		CreatedByUserID: resource.CreatedByUserID, UpdatedByUserID: resource.UpdatedByUserID,
		CreatedAtMS: resource.CreatedAtMS, UpdatedAtMS: resource.UpdatedAtMS,
	}
}

type vipRowQuerier interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

func (r *Repository) queryEquippedVipTrialCardWithQuery(ctx context.Context, querier vipRowQuerier, userID int64, nowMS int64) (*ledger.VipTrialCard, error) {
	row := querier.QueryRowContext(ctx, `
		SELECT c.trial_card_id, c.entitlement_id, c.resource_id, c.user_id, c.level,
		       c.name, c.status, c.duration_ms, c.effective_at_ms, c.expires_at_ms,
		       c.grant_source, c.source_grant_id, c.created_at_ms, c.updated_at_ms
		FROM user_resource_equipment eq
		JOIN user_vip_trial_cards c ON c.app_code = eq.app_code
		 AND c.user_id = eq.user_id AND c.entitlement_id = eq.entitlement_id
		JOIN user_resource_entitlements e ON e.app_code = eq.app_code
		 AND e.user_id = eq.user_id AND e.entitlement_id = eq.entitlement_id
		JOIN resources r ON r.app_code = eq.app_code AND r.resource_id = eq.resource_id
		WHERE eq.app_code = ? AND eq.user_id = ? AND eq.resource_type = ?
		  AND c.status = ? AND c.effective_at_ms <= ? AND c.expires_at_ms > ?
		  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 r.status = 'active'
		LIMIT 1`,
		appcode.FromContext(ctx), userID, resourcedomain.TypeVIPTrialCard,
		ledger.VipTrialCardStatusActive, nowMS, nowMS, nowMS, nowMS,
	)
	card, err := scanVipTrialCard(row, nowMS)
	if err != nil {
		return nil, err
	}
	card.Equipped = true
	return &card, nil
}

func scanVipTrialCard(scanner vipProgramScanner, nowMS int64) (ledger.VipTrialCard, error) {
	var card ledger.VipTrialCard
	err := scanner.Scan(
		&card.TrialCardID,
		&card.EntitlementID,
		&card.ResourceID,
		&card.UserID,
		&card.Level,
		&card.Name,
		&card.Status,
		&card.DurationMS,
		&card.EffectiveAtMS,
		&card.ExpiresAtMS,
		&card.GrantSource,
		&card.SourceGrantID,
		&card.CreatedAtMS,
		&card.UpdatedAtMS,
	)
	if err == nil && card.ExpiresAtMS > nowMS {
		card.RemainingDurationMS = card.ExpiresAtMS - nowMS
	}
	return card, err
}
