package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

const vipPlaceholderDurationMS int64 = 30 * 24 * 60 * 60 * 1000

// UpdateAdminVipLevels 按 program.level_count 保存完整等级与每级显式权益集合。
// 每次变更同时递增 config_version，使 room/user 等执行方能够识别最新权限快照。
func (r *Repository) UpdateAdminVipLevels(ctx context.Context, levels []ledger.AdminVipLevelCommand, operatorUserID int64) (ledger.VipProgramConfig, []ledger.VipLevel, error) {
	if r == nil || r.db == nil {
		return ledger.VipProgramConfig{}, nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if operatorUserID <= 0 {
		return ledger.VipProgramConfig{}, nil, xerr.New(xerr.InvalidArgument, "operator_user_id is required")
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	defer func() { _ = tx.Rollback() }()

	program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` FOR UPDATE`, appcode.FromContext(ctx)))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.VipProgramConfig{}, nil, xerr.New(xerr.NotFound, "vip program config not found")
	}
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	normalized, err := normalizeAdminVipLevelCommands(levels, program)
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}

	nowMS := time.Now().UnixMilli()
	newConfigVersion := program.ConfigVersion + 1
	oldResourceIDs, err := r.listCurrentVipBenefitResourceIDsTx(ctx, tx)
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	// 先保存完整旧矩阵，再执行先删后插；即使运营移除权益或把 A 改绑 B，A 仍永久保留
	// tiered VIP 身份，后续装备和 appearance 都不会把旧 entitlement 当作普通资源放行。
	if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, newConfigVersion, "before", program.ProgramType, nowMS); err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	for _, level := range normalized {
		if level.Status == ledger.VipStatusActive && level.RewardResourceGroupID > 0 {
			if err := r.requireActiveResourceGroup(ctx, tx, level.RewardResourceGroupID); err != nil {
				return ledger.VipProgramConfig{}, nil, err
			}
		}
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO vip_levels (
			   app_code, level, name, status, price_coin, duration_ms, reward_resource_group_id,
			   required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
			 ON DUPLICATE KEY UPDATE
			   name = VALUES(name), status = VALUES(status), price_coin = VALUES(price_coin),
			   duration_ms = VALUES(duration_ms), reward_resource_group_id = VALUES(reward_resource_group_id),
			   required_recharge_coin_amount = 0, sort_order = VALUES(sort_order),
			   updated_at_ms = VALUES(updated_at_ms)`,
			appcode.FromContext(ctx), level.Level, level.Name, level.Status, level.PriceCoin,
			level.DurationMS, level.RewardResourceGroupID, level.SortOrder, nowMS, nowMS,
		); err != nil {
			return ledger.VipProgramConfig{}, nil, err
		}
		// 请求携带的是该等级最终完整集合，因此先删后插，明确支持运营移除权益且不做 unlock_level 自动继承。
		if _, err := tx.ExecContext(ctx, `DELETE FROM vip_level_benefits WHERE app_code = ? AND level = ?`, appcode.FromContext(ctx), level.Level); err != nil {
			return ledger.VipProgramConfig{}, nil, err
		}
		for _, benefit := range level.Benefits {
			if err := r.validateAdminVipBenefit(ctx, tx, program, level.Level, benefit); err != nil {
				return ledger.VipProgramConfig{}, nil, err
			}
			var metadata any
			if strings.TrimSpace(benefit.MetadataJSON) != "" {
				metadata = benefit.MetadataJSON
			}
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO vip_level_benefits (
				   app_code, level, benefit_code, name, benefit_type, unlock_level, status,
				   trial_enabled, resource_id, resource_type, execution_scope, auto_equip,
				   sort_order, metadata_json, created_at_ms, updated_at_ms
				 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
				appcode.FromContext(ctx), level.Level, benefit.BenefitCode, benefit.Name,
				benefit.BenefitType, benefit.UnlockLevel, benefit.Status, benefit.TrialEnabled,
				benefit.ResourceID, benefit.ResourceType, benefit.ExecutionScope, benefit.AutoEquip,
				benefit.SortOrder, metadata, nowMS, nowMS,
			); err != nil {
				return ledger.VipProgramConfig{}, nil, err
			}
		}
	}
	newResourceIDs, err := r.listCurrentVipBenefitResourceIDsTx(ctx, tx)
	if err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, newConfigVersion, "after", program.ProgramType, nowMS); err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	if _, err := tx.ExecContext(ctx, `
			UPDATE vip_program_configs
			SET config_version = ?, updated_by_admin_id = ?, updated_at_ms = ?
			WHERE app_code = ?`, newConfigVersion, operatorUserID, nowMS, appcode.FromContext(ctx)); err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	changeCommandID := boundedDerivedCommandID("vip_program_changed:", strconv.FormatInt(newConfigVersion, 10))
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("VipProgramChanged", changeCommandID, 0, 0, map[string]any{
			"app_code": appcode.FromContext(ctx), "config_version": newConfigVersion,
			"change_scope": "levels", "operator_user_id": operatorUserID,
			"old_resource_ids": oldResourceIDs, "new_resource_ids": newResourceIDs,
			"updated_at_ms": nowMS,
		}, nowMS),
	}); err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.VipProgramConfig{}, nil, err
	}
	// 提交后用单独 repeatable-read 快照返回 program+levels；即使另一管理员紧接着更新，回包也不会跨版本拼接。
	return r.ListAdminVipLevels(ctx)
}

// UpdateAdminVipProgramConfig 只接受服务端已实现的策略组合，并在锁内递增配置版本。
func (r *Repository) UpdateAdminVipProgramConfig(ctx context.Context, command ledger.AdminVipProgramConfigCommand) (ledger.VipProgramConfig, error) {
	if r == nil || r.db == nil {
		return ledger.VipProgramConfig{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	config := normalizeAdminVipProgramConfig(command.Config)
	if err := validateAdminVipProgramConfig(config); err != nil {
		return ledger.VipProgramConfig{}, err
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.VipProgramConfig{}, err
	}
	defer func() { _ = tx.Rollback() }()

	nowMS := time.Now().UnixMilli()
	oldConfig, oldConfigErr := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` FOR UPDATE`, appcode.FromContext(ctx)))
	if oldConfigErr != nil && !errors.Is(oldConfigErr, sql.ErrNoRows) {
		return ledger.VipProgramConfig{}, oldConfigErr
	}
	newConfigVersion := int64(1)
	oldProgramType := config.ProgramType
	if oldConfigErr == nil {
		newConfigVersion = oldConfig.ConfigVersion + 1
		oldProgramType = oldConfig.ProgramType
		if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, newConfigVersion, "before", oldProgramType, nowMS); err != nil {
			return ledger.VipProgramConfig{}, err
		}
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO vip_program_configs (
		   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
			 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		 ON DUPLICATE KEY UPDATE
		   program_type = VALUES(program_type), level_count = VALUES(level_count),
		   same_level_expiry_policy = VALUES(same_level_expiry_policy),
		   upgrade_expiry_policy = VALUES(upgrade_expiry_policy),
		   downgrade_purchase_policy = VALUES(downgrade_purchase_policy),
		   benefit_inheritance_policy = VALUES(benefit_inheritance_policy),
		   grant_mode = VALUES(grant_mode), trial_card_enabled = VALUES(trial_card_enabled),
		   status = VALUES(status), config_version = config_version + 1,
		   updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`,
		appcode.FromContext(ctx), config.ProgramType, config.LevelCount, config.SameLevelExpiryPolicy,
		config.UpgradeExpiryPolicy, config.DowngradePurchasePolicy,
		config.BenefitInheritancePolicy, config.GrantMode, config.TrialCardEnabled,
		config.Status, newConfigVersion, command.OperatorUserID, nowMS, nowMS,
	); err != nil {
		return ledger.VipProgramConfig{}, err
	}
	// level_count 增大或新 App 首次建体系时，事务内补齐不可购买的结构占位等级。
	// 价格和资源组保持 0，只有运营随后提交完整等级配置并显式启用后才能进入购买链路。
	for level := int32(1); level <= config.LevelCount; level++ {
		if _, err := tx.ExecContext(ctx, `
			INSERT IGNORE INTO vip_levels (
			   app_code, level, name, status, price_coin, duration_ms,
			   reward_resource_group_id, required_recharge_coin_amount, sort_order,
			   created_at_ms, updated_at_ms
			 ) VALUES (?, ?, ?, ?, 0, ?, 0, 0, ?, ?, ?)`,
			appcode.FromContext(ctx), level, "VIP"+strconv.Itoa(int(level)), ledger.VipStatusDisabled,
			vipPlaceholderDurationMS, level*10, nowMS, nowMS,
		); err != nil {
			return ledger.VipProgramConfig{}, err
		}
	}
	config, err = r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL(), appcode.FromContext(ctx)))
	if err != nil {
		return ledger.VipProgramConfig{}, err
	}
	if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, config.ConfigVersion, "after", config.ProgramType, nowMS); err != nil {
		return ledger.VipProgramConfig{}, err
	}
	resourceIDs, err := r.listCurrentVipBenefitResourceIDsTx(ctx, tx)
	if err != nil {
		return ledger.VipProgramConfig{}, err
	}
	changeCommandID := boundedDerivedCommandID("vip_program_changed:", strconv.FormatInt(config.ConfigVersion, 10))
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("VipProgramChanged", changeCommandID, 0, 0, map[string]any{
			"app_code": appcode.FromContext(ctx), "config_version": config.ConfigVersion,
			"change_scope": "program", "program_type": config.ProgramType,
			"status": config.Status, "level_count": config.LevelCount,
			"trial_card_enabled": config.TrialCardEnabled,
			"operator_user_id":   command.OperatorUserID, "resource_ids": resourceIDs,
			"previous_program_type": oldProgramType, "updated_at_ms": nowMS,
		}, nowMS),
	}); err != nil {
		return ledger.VipProgramConfig{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.VipProgramConfig{}, err
	}
	return config, nil
}

// recordVipBenefitResourceBindingSnapshotTx 只复制当前 App 的小型权益矩阵。history_id 与唯一键
// 都由 config_version+phase+level+benefit_code 决定，事务重试不会产生重复历史。
func (r *Repository) recordVipBenefitResourceBindingSnapshotTx(ctx context.Context, tx *sql.Tx, configVersion int64, phase string, programType string, nowMS int64) error {
	phase = strings.TrimSpace(phase)
	programType = strings.TrimSpace(programType)
	if configVersion <= 0 || (phase != "before" && phase != "after") || programType == "" {
		return xerr.New(xerr.Internal, "vip benefit binding snapshot context is invalid")
	}
	_, err := tx.ExecContext(ctx, `
		INSERT INTO vip_benefit_resource_binding_history (
		  app_code, history_id, config_version, change_phase, program_type, level,
		  benefit_code, benefit_status, trial_enabled, resource_id, resource_type,
		  auto_equip, recorded_at_ms
		)
		SELECT b.app_code,
		       SHA2(CONCAT_WS('|', b.app_code, ?, ?, b.level, b.benefit_code), 256),
		       ?, ?, ?, b.level, b.benefit_code, b.status, b.trial_enabled,
		       b.resource_id, b.resource_type, b.auto_equip, ?
		FROM vip_level_benefits AS b
		WHERE b.app_code = ?`,
		configVersion, phase, configVersion, phase, programType, nowMS, appcode.FromContext(ctx),
	)
	return err
}

func (r *Repository) listCurrentVipBenefitResourceIDsTx(ctx context.Context, tx *sql.Tx) ([]int64, error) {
	rows, err := tx.QueryContext(ctx, `
		SELECT DISTINCT resource_id
		FROM vip_level_benefits
		WHERE app_code = ? AND resource_id > 0
		ORDER BY resource_id ASC`, appcode.FromContext(ctx))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	resourceIDs := make([]int64, 0, 8)
	for rows.Next() {
		var resourceID int64
		if err := rows.Scan(&resourceID); err != nil {
			return nil, err
		}
		resourceIDs = append(resourceIDs, resourceID)
	}
	return resourceIDs, rows.Err()
}

func normalizeAdminVipLevelCommands(levels []ledger.AdminVipLevelCommand, program ledger.VipProgramConfig) ([]ledger.AdminVipLevelCommand, error) {
	if int32(len(levels)) != program.LevelCount {
		return nil, xerr.New(xerr.InvalidArgument, "vip config level count does not match program")
	}
	byLevel := make(map[int32]ledger.AdminVipLevelCommand, len(levels))
	for _, level := range levels {
		level.Name = strings.TrimSpace(level.Name)
		level.Status = strings.ToLower(strings.TrimSpace(level.Status))
		if level.Status == "" {
			level.Status = ledger.VipStatusDisabled
		}
		if level.Level < 1 || level.Level > program.LevelCount {
			return nil, xerr.New(xerr.InvalidArgument, "vip level is outside program level_count")
		}
		if _, exists := byLevel[level.Level]; exists {
			return nil, xerr.New(xerr.InvalidArgument, "vip level is duplicated")
		}
		level.RequiredRechargeCoinAmount = 0
		benefitCodes := make(map[string]struct{}, len(level.Benefits))
		for index := range level.Benefits {
			benefit := &level.Benefits[index]
			benefit.BenefitCode = strings.ToLower(strings.TrimSpace(benefit.BenefitCode))
			benefit.Name = strings.TrimSpace(benefit.Name)
			benefit.BenefitType = strings.ToLower(strings.TrimSpace(benefit.BenefitType))
			benefit.Status = strings.ToLower(strings.TrimSpace(benefit.Status))
			benefit.ResourceType = resourcedomain.NormalizeResourceType(benefit.ResourceType)
			benefit.ExecutionScope = strings.ToLower(strings.TrimSpace(benefit.ExecutionScope))
			benefit.MetadataJSON = strings.TrimSpace(benefit.MetadataJSON)
			// 后台可直接提交结构化 presentation；钱包仍把它与 coin_amount/message 等扩展字段
			// 原子保存在同一 JSON 列，避免展示配置出现双写版本。
			mergedMetadata, mergeErr := ledger.MergeVipBenefitPresentation(benefit.MetadataJSON, benefit.Presentation)
			if mergeErr != nil {
				return nil, xerr.New(xerr.InvalidArgument, "vip benefit presentation is invalid")
			}
			benefit.MetadataJSON = mergedMetadata
			// 体验卡永远不参与金币返现；保存时强制收敛，运行时仍会按编码再次兜底。
			if benefit.BenefitCode == ledger.VipBenefitCodeDailyCoinRebate {
				benefit.TrialEnabled = false
			}
			if _, exists := benefitCodes[benefit.BenefitCode]; benefit.BenefitCode == "" || exists {
				return nil, xerr.New(xerr.InvalidArgument, "vip benefit code is empty or duplicated")
			}
			benefitCodes[benefit.BenefitCode] = struct{}{}
		}
		if err := validateAdminVipLevelCommand(level, program); err != nil {
			return nil, err
		}
		byLevel[level.Level] = level
	}
	normalized := make([]ledger.AdminVipLevelCommand, 0, program.LevelCount)
	for level := int32(1); level <= program.LevelCount; level++ {
		item, ok := byLevel[level]
		if !ok {
			return nil, xerr.New(xerr.InvalidArgument, "vip config requires a complete level sequence")
		}
		normalized = append(normalized, item)
	}
	return normalized, nil
}

func validateAdminVipLevelCommand(level ledger.AdminVipLevelCommand, program ledger.VipProgramConfig) error {
	if level.Name == "" {
		return xerr.New(xerr.InvalidArgument, "vip level name is required")
	}
	switch level.Status {
	case ledger.VipStatusActive, ledger.VipStatusDisabled:
	default:
		return xerr.New(xerr.InvalidArgument, "vip level status is invalid")
	}
	if level.PriceCoin < 0 || level.DurationMS <= 0 || level.RewardResourceGroupID < 0 {
		return xerr.New(xerr.InvalidArgument, "vip level config is invalid")
	}
	if level.Status == ledger.VipStatusActive && level.PriceCoin <= 0 {
		return xerr.New(xerr.InvalidArgument, "active vip level price_coin must be greater than zero")
	}
	// legacy_timed 继续允许无资源组的既有等级；tiered 体系的能力也来自显式 benefits，不能强制复制旧资源组模型。
	_ = program
	return nil
}

func (r *Repository) validateAdminVipBenefit(ctx context.Context, tx *sql.Tx, program ledger.VipProgramConfig, level int32, benefit ledger.VipBenefit) error {
	if benefit.BenefitCode == "" || benefit.Name == "" || benefit.UnlockLevel < 0 || benefit.ResourceID < 0 {
		return xerr.New(xerr.InvalidArgument, "vip benefit is incomplete")
	}
	switch benefit.BenefitType {
	case ledger.VipBenefitTypeDecoration, ledger.VipBenefitTypeFunction:
	default:
		return xerr.New(xerr.InvalidArgument, "vip benefit type is invalid")
	}
	switch benefit.Status {
	case ledger.VipStatusActive, ledger.VipStatusDisabled:
	default:
		return xerr.New(xerr.InvalidArgument, "vip benefit status is invalid")
	}
	switch benefit.ExecutionScope {
	case ledger.VipExecutionScopeWallet, ledger.VipExecutionScopeRoom, ledger.VipExecutionScopeUser, ledger.VipExecutionScopeNotice:
	default:
		return xerr.New(xerr.InvalidArgument, "vip benefit execution_scope is invalid")
	}
	if benefit.MetadataJSON != "" && !json.Valid([]byte(benefit.MetadataJSON)) {
		return xerr.New(xerr.InvalidArgument, "vip benefit metadata_json is invalid")
	}
	if _, err := ledger.ParseVipBenefitPresentation(benefit.MetadataJSON); err != nil {
		return xerr.New(xerr.InvalidArgument, "vip benefit presentation is invalid")
	}
	if benefit.BenefitCode == ledger.VipBenefitCodeDailyCoinRebate {
		if benefit.ExecutionScope != ledger.VipExecutionScopeWallet {
			return xerr.New(xerr.InvalidArgument, "daily_coin_rebate execution_scope must be wallet")
		}
		if benefit.TrialEnabled {
			return xerr.New(xerr.InvalidArgument, "daily_coin_rebate cannot be enabled for trial cards")
		}
		// disabled 允许空对象，便于后台表单原样提交未定金额；若已显式带 coin_amount，
		// 即使 disabled 也拒绝非法类型，避免稍后仅切状态时激活脏数据。
		validateAmount := benefit.Status == ledger.VipStatusActive
		if !validateAmount && benefit.MetadataJSON != "" {
			var metadata map[string]any
			if err := json.Unmarshal([]byte(benefit.MetadataJSON), &metadata); err != nil || metadata == nil {
				return xerr.New(xerr.InvalidArgument, "daily_coin_rebate metadata_json must be an object")
			}
			_, validateAmount = metadata["coin_amount"]
		}
		// active 必须是 JSON 正 int64，不猜测任何产品上限。
		if validateAmount {
			if _, err := parseVipDailyCoinRebateAmount(benefit.MetadataJSON); err != nil {
				return xerr.New(xerr.InvalidArgument, "daily_coin_rebate metadata_json.coin_amount must be a positive int64")
			}
		}
	}
	if benefit.BenefitCode == ledger.VipBenefitCodeOnlineGlobalNotice {
		if benefit.ExecutionScope != ledger.VipExecutionScopeNotice {
			return xerr.New(xerr.InvalidArgument, "online_global_notice execution_scope must be notice")
		}
		if benefit.MetadataJSON != "" {
			var metadata map[string]any
			if err := json.Unmarshal([]byte(benefit.MetadataJSON), &metadata); err != nil || metadata == nil {
				return xerr.New(xerr.InvalidArgument, "online_global_notice metadata_json must be an object")
			}
			if rawMessage, exists := metadata["message"]; exists {
				message, ok := rawMessage.(string)
				if !ok || strings.TrimSpace(message) == "" || utf8.RuneCountInString(message) > 256 {
					return xerr.New(xerr.InvalidArgument, "online_global_notice metadata_json.message must be a non-empty string within 256 characters")
				}
			}
		}
	}
	// tiered privilege v1 的固定房间视觉权益是稳定跨服务契约，不能由后台把 code 改成
	// function 或绑定到近似类型。disabled 可以暂不选素材，但一旦携带 resource_id，
	// 下方仍会校验资源 active 且类型精确匹配，避免脏绑定在之后切状态时直接生效。
	expectedResourceType := ""
	if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 {
		switch benefit.BenefitCode {
		case ledger.VipBenefitCodeMicSkin:
			// 麦克风皮肤替换麦位中心图标；mic_seat_animation 留给 voice_wave 声波动效，
			// 不能因两者都显示在麦位周围就复用同一目录类型。
			expectedResourceType = resourcedomain.TypeMicSeatIcon
		case ledger.VipBenefitCodeRoomBorder:
			expectedResourceType = resourcedomain.TypeRoomBorder
		case ledger.VipBenefitCodeColoredRoomName:
			expectedResourceType = resourcedomain.TypeRoomNameStyle
		case ledger.VipBenefitCodeGiftTraySkin:
			// Fami 客户端的托盘承接器只播放网络 SVGA；固定目录类型后，执行链路只需透传 wallet 已验证的 animation_url。
			expectedResourceType = resourcedomain.TypeGiftTraySkin
		}
	}
	if expectedResourceType != "" {
		if benefit.BenefitType != ledger.VipBenefitTypeDecoration {
			return xerr.New(xerr.InvalidArgument, "vip room decoration benefit_type must be decoration")
		}
		if benefit.ResourceType != expectedResourceType {
			return xerr.New(xerr.InvalidArgument, "vip room decoration resource_type mismatch")
		}
		if benefit.Status == ledger.VipStatusActive && benefit.ResourceID <= 0 {
			return xerr.New(xerr.InvalidArgument, "active vip room decoration requires a resource")
		}
	}
	if benefit.ResourceID == 0 {
		return nil
	}
	resource, err := r.getResourceForUpdate(ctx, tx, benefit.ResourceID)
	if err != nil {
		return err
	}
	if resource.Status != resourcedomain.StatusActive {
		return xerr.New(xerr.Conflict, "vip benefit resource is disabled")
	}
	if benefit.ResourceType != "" && resourcedomain.NormalizeResourceType(resource.ResourceType) != benefit.ResourceType {
		return xerr.New(xerr.Conflict, "vip benefit resource_type mismatch")
	}
	if benefit.BenefitCode == ledger.VipBenefitCodeGiftTraySkin && !validGiftTraySkinResource(resource) {
		return xerr.New(xerr.Conflict, "vip gift tray resource must provide svga animation")
	}
	_ = level
	return nil
}

func validGiftTraySkinResource(resource resourcedomain.Resource) bool {
	if resourcedomain.NormalizeResourceType(resource.ResourceType) != resourcedomain.TypeGiftTraySkin || strings.TrimSpace(resource.AnimationURL) == "" {
		return false
	}
	var metadata struct {
		Format string `json:"format"`
	}
	return json.Unmarshal([]byte(resource.MetadataJSON), &metadata) == nil && strings.EqualFold(strings.TrimSpace(metadata.Format), "svga")
}

func normalizeAdminVipProgramConfig(config ledger.VipProgramConfig) ledger.VipProgramConfig {
	config.AppCode = appcode.Normalize(config.AppCode)
	config.ProgramType = strings.ToLower(strings.TrimSpace(config.ProgramType))
	config.SameLevelExpiryPolicy = strings.ToLower(strings.TrimSpace(config.SameLevelExpiryPolicy))
	config.UpgradeExpiryPolicy = strings.ToLower(strings.TrimSpace(config.UpgradeExpiryPolicy))
	config.DowngradePurchasePolicy = strings.ToLower(strings.TrimSpace(config.DowngradePurchasePolicy))
	// “最低等级自动继承”已被产品明确移除。旧后台若仍提交 cumulative，也只能作为兼容输入，
	// 服务端持久化和回包一律收敛为 target_only，防止隐藏字段恢复旧语义。
	config.BenefitInheritancePolicy = ledger.VipBenefitInheritanceTargetOnly
	config.GrantMode = strings.ToLower(strings.TrimSpace(config.GrantMode))
	config.Status = strings.ToLower(strings.TrimSpace(config.Status))
	return config
}

func validateAdminVipProgramConfig(config ledger.VipProgramConfig) error {
	switch config.ProgramType {
	case ledger.VipProgramTypeLegacyTimed, ledger.VipProgramTypeTieredPrivilegeV1:
	default:
		return xerr.New(xerr.InvalidArgument, "vip program_type is invalid")
	}
	if config.LevelCount < 1 || config.LevelCount > 20 {
		return xerr.New(xerr.InvalidArgument, "vip level_count must be between 1 and 20")
	}
	if config.SameLevelExpiryPolicy != ledger.VipExpiryPolicyExtendRemaining {
		return xerr.New(xerr.InvalidArgument, "vip same_level_expiry_policy is invalid")
	}
	switch config.UpgradeExpiryPolicy {
	case ledger.VipExpiryPolicyExtendRemaining, ledger.VipExpiryPolicyReplaceFromNow:
	default:
		return xerr.New(xerr.InvalidArgument, "vip upgrade_expiry_policy is invalid")
	}
	if config.DowngradePurchasePolicy != ledger.VipDowngradePurchasePolicyReject {
		return xerr.New(xerr.InvalidArgument, "vip downgrade_purchase_policy is invalid")
	}
	// 产品已明确忽略“最低等级自动继承”；当前运行时只支持后台为每级保存完整集合。
	if config.BenefitInheritancePolicy != ledger.VipBenefitInheritanceTargetOnly {
		return xerr.New(xerr.InvalidArgument, "vip benefit_inheritance_policy must be target_only")
	}
	switch config.GrantMode {
	case ledger.VipGrantModeDirectMembership:
	case ledger.VipGrantModeTrialCard:
		if !config.TrialCardEnabled {
			return xerr.New(xerr.InvalidArgument, "trial_card grant mode requires trial_card_enabled")
		}
	default:
		return xerr.New(xerr.InvalidArgument, "vip grant_mode is invalid")
	}
	switch config.Status {
	case ledger.VipStatusActive, ledger.VipStatusDisabled:
	default:
		return xerr.New(xerr.InvalidArgument, "vip program status is invalid")
	}
	return nil
}

func (r *Repository) requireActiveResourceGroup(ctx context.Context, tx *sql.Tx, groupID int64) error {
	group, err := r.getResourceGroup(ctx, tx, groupID, true)
	if err != nil {
		return err
	}
	if group.Status != resourcedomain.StatusActive {
		return xerr.New(xerr.Conflict, "vip reward group is disabled")
	}
	return nil
}
