package mysql

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

// PurchaseVip 在一个事务内完成金币扣费、VIP 状态更新、订单历史和资源组发放。
func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVipCommand) (ledger.PurchaseVipReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)

	requestHash := stableHash(fmt.Sprintf("vip_purchase|%s|%d|%d", command.AppCode, command.UserID, command.Level))
	nowMs := time.Now().UnixMilli()
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	if err := r.lockVipCommand(ctx, tx, command.CommandID, bizTypeVIPPurchase, requestHash, nowMs, xerr.LedgerConflict); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if _, exists, err := r.lookupTransactionConsistent(ctx, tx, command.CommandID, requestHash, bizTypeVIPPurchase, xerr.LedgerConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.PurchaseVipReceipt{}, err
		}
		_ = tx.Rollback()
		return r.replayVipPurchase(ctx, command, requestHash)
	}

	program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, command.AppCode))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found")
	}
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if program.Status != ledger.VipStatusActive {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPProgramInactive, "vip program is disabled")
	}
	if command.Level > program.LevelCount {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPPackageNotPurchasable, "vip level exceeds program level_count")
	}
	level, err := r.getVipLevelForUpdate(ctx, tx, command.Level)
	if err != nil {
		if xerr.IsCode(err, xerr.VIPLevelNotFound) {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPPackageNotPurchasable, "vip package not found")
		}
		return ledger.PurchaseVipReceipt{}, err
	}
	if level.Status != ledger.VipStatusActive {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPPackageNotPurchasable, "vip package is disabled")
	}
	current, err := r.ensureUserVipForUpdate(ctx, tx, command.UserID, nowMs)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	previousLevel := int32(0)
	if current.Active {
		previousLevel = current.Level
		if current.Level > command.Level {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed")
		}
	}
	expiryPolicy := program.SameLevelExpiryPolicy
	if current.Active && command.Level > current.Level {
		expiryPolicy = program.UpgradeExpiryPolicy
	}
	startedAtMS, newExpiresAt, err := vipActivationWindow(current, level, expiryPolicy, nowMs)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
	if err != nil {
		// 从未持有 COIN 的用户没有 wallet_account 行，lockAccount 会先返回通用余额不足；
		// VIP 购买对 Flutter 的稳定契约必须与“已有账户但余额不足”一致，统一成专用错误码。
		if xerr.IsCode(err, xerr.InsufficientBalance) {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPInsufficientCoin, "insufficient coin balance")
		}
		return ledger.PurchaseVipReceipt{}, err
	}
	if account.AvailableAmount < level.PriceCoin {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPInsufficientCoin, "insufficient coin balance")
	}

	transactionID := transactionID(command.AppCode, command.CommandID)
	orderID := "vip_order_" + stableHash(command.AppCode+"|"+command.CommandID)
	coinBalanceAfter := account.AvailableAmount - level.PriceCoin
	metadata := map[string]any{
		"app_code":       command.AppCode,
		"order_id":       orderID,
		"user_id":        command.UserID,
		"target_level":   command.Level,
		"vip_name":       level.Name,
		"previous_level": previousLevel,
		"price_coin":     level.PriceCoin,
		"expires_at_ms":  newExpiresAt,
		"balance_after":  coinBalanceAfter,
		// 余额版本必须与本次交易后的快照一起固化，幂等重放不能读取已被后续交易推进的当前版本。
		"balance_version":        account.Version + 1,
		"frozen_after":           account.FrozenAmount,
		"reward_group_id":        level.RewardResourceGroupID,
		"duration_ms":            level.DurationMS,
		"previous_expires_at_ms": current.ExpiresAtMS,
		"started_at_ms":          startedAtMS,
		"program_type":           program.ProgramType,
		"config_version":         program.ConfigVersion,
		"expiry_policy":          expiryPolicy,
		// 兼容旧账务元数据字段；业务已不再按累计充值限制 VIP 购买，因此固定记录 0。
		"required_recharge_coin_amount": int64(0),
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPPurchase, requestHash, orderID, metadata, nowMs); err != nil {
		if isMySQLDuplicateError(err) {
			_ = tx.Rollback()
			return r.replayVipPurchase(ctx, command, requestHash)
		}
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, -level.PriceCoin, 0, nowMs); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: -level.PriceCoin,
		FrozenDelta:    0,
		AvailableAfter: coinBalanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	activation, err := r.activateUserVip(ctx, tx, vipActivationCommand{
		AppCode:        command.AppCode,
		CommandID:      command.CommandID,
		UserID:         command.UserID,
		Source:         ledger.VipGrantSourcePurchase,
		Reason:         "vip purchase reward",
		OperatorUserID: command.UserID,
		TransactionID:  transactionID,
		RecordID:       orderID,
		Level:          level,
		Current:        current,
		Program:        program,
		ExpiryPolicy:   expiryPolicy,
		NowMS:          nowMs,
	})
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO vip_purchase_orders (
		   app_code, order_id, command_id, user_id, target_level, previous_level,
		   previous_expires_at_ms, new_expires_at_ms, price_coin, duration_ms,
		   program_type, config_version, expiry_policy, status, wallet_transaction_id,
		   resource_grant_id, request_hash, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'succeeded', ?, ?, ?, ?, ?)`,
		command.AppCode, orderID, command.CommandID, command.UserID, command.Level, previousLevel,
		current.ExpiresAtMS, activation.VIP.ExpiresAtMS, level.PriceCoin, level.DurationMS,
		program.ProgramType, program.ConfigVersion, expiryPolicy, transactionID,
		activation.ResourceGrantID, requestHash, nowMs, nowMs,
	); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	vipEventType := "VipPurchased"
	if activation.Action == "renew" {
		vipEventType = "VipRenewed"
	} else if activation.Action == "upgrade" {
		vipEventType = "VipUpgraded"
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, -level.PriceCoin, 0, coinBalanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs, bizTypeVIPPurchase),
		{
			EventID:        eventID(transactionID, vipEventType, command.UserID, "VIP"),
			EventType:      vipEventType,
			TransactionID:  transactionID,
			CommandID:      command.CommandID,
			UserID:         command.UserID,
			AssetType:      "VIP",
			AvailableDelta: 0,
			FrozenDelta:    0,
			Payload:        metadata,
			CreatedAtMS:    nowMs,
		},
		vipActivatedEvent(transactionID, command.CommandID, command.UserID, ledger.VipGrantSourcePurchase, activation.Action, activation.PreviousLevel, activation.PreviousExpiresAtMS, activation.ExpiryPolicy, activation.VIP, level.RewardResourceGroupID, activation.ResourceGrantID, activation.RewardItems, nowMs),
	}); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}

	state, err := r.GetVipState(ctx, command.UserID)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	return ledger.PurchaseVipReceipt{
		OrderID:          orderID,
		TransactionID:    transactionID,
		Vip:              activation.VIP,
		CoinSpent:        level.PriceCoin,
		CoinBalanceAfter: coinBalanceAfter,
		CoinBalance: ledger.AssetBalance{
			AppCode: command.AppCode, UserID: command.UserID, AssetType: ledger.AssetCoin,
			AvailableAmount: coinBalanceAfter, FrozenAmount: account.FrozenAmount,
			Version: account.Version + 1, UpdatedAtMs: nowMs,
		},
		RewardItems: activation.RewardItems,
		State:       state,
	}, nil
}

func (r *Repository) replayVipPurchase(ctx context.Context, command ledger.PurchaseVipCommand, requestHash string) (ledger.PurchaseVipReceipt, error) {
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead})
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()
	txRow, exists, err := r.lookupTransactionConsistent(ctx, tx, command.CommandID, requestHash, bizTypeVIPPurchase, xerr.LedgerConflict)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if !exists {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.Unavailable, "vip purchase idempotent receipt is not visible")
	}
	receipt, err := r.vipPurchaseReceiptForTransaction(ctx, tx, txRow, command.UserID)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	_ = tx.Rollback()
	receipt.State, err = r.GetVipState(ctx, command.UserID)
	return receipt, err
}

func (r *Repository) activateUserVip(ctx context.Context, tx *sql.Tx, command vipActivationCommand) (vipActivationResult, error) {
	previousLevel := int32(0)
	if command.Current.Active {
		previousLevel = command.Current.Level
		if command.Current.Level > command.Level.Level {
			return vipActivationResult{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed")
		}
	}
	startedAt, expiresAt, err := vipActivationWindow(command.Current, command.Level, command.ExpiryPolicy, command.NowMS)
	if err != nil {
		return vipActivationResult{}, err
	}
	activationVIP := ledger.UserVip{
		UserID: command.UserID, Level: command.Level.Level, Name: command.Level.Name, Active: true,
		StartedAtMS: startedAt, ExpiresAtMS: expiresAt, UpdatedAtMS: command.NowMS,
		ProgramType: command.Program.ProgramType, ConfigVersion: command.Program.ConfigVersion,
	}
	// 先更新 paid membership，再计算 paid+trial 的最终来源。这样用户正佩戴体验卡时续费，
	// paid entitlement 会延长但当前 appearance 仍保持 trial，而不会被错误切回 paid 等级。
	if _, err := tx.ExecContext(ctx, `
			UPDATE user_vip_memberships
			SET level = ?, name = ?, status = ?, program_type = ?, config_version = ?,
			    started_at_ms = ?, expires_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?`,
		command.Level.Level, command.Level.Name, ledger.VipStatusActive,
		command.Program.ProgramType, command.Program.ConfigVersion,
		startedAt, expiresAt, command.NowMS, command.AppCode, command.UserID,
	); err != nil {
		return vipActivationResult{}, err
	}
	if command.Source == ledger.VipGrantSourcePurchase &&
		command.Program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 &&
		command.Program.TrialCardEnabled {
		equippedTrial, trialErr := r.queryEquippedVipTrialCardWithQuery(ctx, tx, command.UserID, command.NowMS)
		if trialErr != nil && !errors.Is(trialErr, sql.ErrNoRows) {
			return vipActivationResult{}, trialErr
		}
		if trialErr == nil && command.Level.Level >= equippedTrial.Level {
			// 购买等级覆盖当前体验等级时，真实 VIP 必须在本次账务事务内接管身份。
			// 体验卡本体、由该卡生成的资源 entitlement 和佩戴指针一并回收；购买时长
			// 仍只由 paid membership 的 replace_from_now 窗口决定，绝不叠加体验卡余量。
			if err := r.replaceVipTrialCardWithPaidTx(
				ctx,
				tx,
				*equippedTrial,
				command.Level.Level,
				command.CommandID,
				command.NowMS,
			); err != nil {
				return vipActivationResult{}, err
			}
		}
	}
	var rewardItems []ledger.VipRewardItem
	var resourceGrantID string
	var tieredState ledger.VipState
	if command.Program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 {
		// 付费身份素材是否发放必须先看 paid+trial 的最终来源。更高 trial 生效时本次
		// 购买只累计收藏型资源，绝不创建随后又被回收的低等级身份 entitlement/outbox。
		tieredState, err = r.getVipStateFromSnapshot(ctx, tx, command.UserID, command.NowMS, command.Program)
		if err != nil {
			return vipActivationResult{}, err
		}
		// tiered 资源直接来自 active benefit bindings；reward group 不再是 entitlement 的人工同步前置条件。
		rewardItems, resourceGrantID, err = r.grantPaidVipBenefitResourcesTx(
			ctx,
			tx,
			activationVIP,
			tieredState.EffectiveSource == ledger.VipEffectiveSourcePaid,
			command.NowMS,
		)
	} else {
		rewardItems, resourceGrantID, err = r.applyVipRewardGroup(ctx, tx, vipRewardGrantCommand{
			AppCode: command.AppCode, CommandID: command.CommandID, UserID: command.UserID,
			Source: command.Source, Reason: command.Reason, OperatorUserID: command.OperatorUserID,
		}, command.Level, expiresAt, command.NowMS)
	}
	if err != nil {
		return vipActivationResult{}, err
	}
	if command.Program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 {
		state := tieredState
		if state.EffectiveSource == ledger.VipEffectiveSourceTrial && state.EquippedTrialCard != nil {
			if err := r.grantVipTrialBenefitResourcesTx(ctx, tx, *state.EquippedTrialCard, command.Program.ConfigVersion, command.NowMS); err != nil {
				return vipActivationResult{}, err
			}
		}
		effectiveLevel := int32(0)
		effectiveSource := ledger.VipEffectiveSourceNone
		if state.EffectiveVip.Active {
			effectiveLevel = state.EffectiveVip.Level
			effectiveSource = state.EffectiveSource
		}
		// 只有首次生效或 paid 等级提升时才主动切换默认素材；同等级续费以及更高
		// trial 仍生效时必须保留用户当前合法选择。
		preserveValidSelection := state.EffectiveSource != ledger.VipEffectiveSourcePaid ||
			(command.Current.Active && command.Level.Level <= command.Current.Level)
		if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, command.UserID, effectiveLevel, effectiveSource, command.CommandID, command.NowMS, preserveValidSelection); err != nil {
			return vipActivationResult{}, err
		}
	}
	action := vipActivationAction(command.Source, previousLevel, command.Level.Level)
	if _, err := tx.ExecContext(ctx, `
		INSERT INTO user_vip_history (
		   app_code, user_id, action, previous_level, new_level, previous_expires_at_ms,
		   new_expires_at_ms, program_type, config_version, order_id, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		command.AppCode, command.UserID, action, previousLevel, command.Level.Level,
		command.Current.ExpiresAtMS, expiresAt, command.Program.ProgramType,
		command.Program.ConfigVersion, command.RecordID, command.NowMS,
	); err != nil {
		return vipActivationResult{}, err
	}
	return vipActivationResult{
		VIP:                 activationVIP,
		PreviousLevel:       previousLevel,
		PreviousExpiresAtMS: command.Current.ExpiresAtMS,
		ExpiryPolicy:        command.ExpiryPolicy,
		Action:              action,
		ResourceGrantID:     resourceGrantID,
		RewardItems:         rewardItems,
	}, nil
}

// vipActivationWindow 把有效期策略收敛成一个纯函数，购买、后台直发和测试共享同一边界。
// replace_from_now 只作用于有效会员的高级升级；同级续费仍始终从当前截止时间累加。
func vipActivationWindow(current ledger.UserVip, level ledger.VipLevel, expiryPolicy string, nowMS int64) (int64, int64, error) {
	if level.DurationMS <= 0 {
		return 0, 0, xerr.New(xerr.InvalidArgument, "vip duration_ms must be positive")
	}
	startedAtMS := nowMS
	baseExpiresAtMS := nowMS
	if current.Active {
		startedAtMS = current.StartedAtMS
		baseExpiresAtMS = current.ExpiresAtMS
		if level.Level > current.Level && expiryPolicy == ledger.VipExpiryPolicyReplaceFromNow {
			startedAtMS = nowMS
			baseExpiresAtMS = nowMS
		}
	}
	if expiryPolicy != ledger.VipExpiryPolicyExtendRemaining && expiryPolicy != ledger.VipExpiryPolicyReplaceFromNow {
		return 0, 0, xerr.New(xerr.Conflict, "vip expiry policy is invalid")
	}
	if level.DurationMS > int64(^uint64(0)>>1)-baseExpiresAtMS {
		return 0, 0, xerr.New(xerr.InvalidArgument, "vip expiry overflow")
	}
	return startedAtMS, baseExpiresAtMS + level.DurationMS, nil
}

func vipActivationAction(source string, previousLevel int32, newLevel int32) string {
	if source != ledger.VipGrantSourcePurchase {
		return source
	}
	if previousLevel == newLevel {
		return "renew"
	}
	if previousLevel > 0 && previousLevel < newLevel {
		return "upgrade"
	}
	return "purchase"
}

func (r *Repository) getVipLevelForUpdate(ctx context.Context, tx *sql.Tx, level int32) (ledger.VipLevel, error) {
	item, err := r.scanVipLevel(tx.QueryRowContext(ctx, `
		SELECT level, name, status, price_coin, duration_ms, reward_resource_group_id,
		       required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
		FROM vip_levels
		WHERE app_code = ? AND level = ?
		FOR UPDATE`,
		appcode.FromContext(ctx), level,
	))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.VipLevel{}, xerr.New(xerr.VIPLevelNotFound, "vip level not found")
	}
	return item, err
}

func (r *Repository) applyVipRewardGroup(ctx context.Context, tx *sql.Tx, command vipRewardGrantCommand, level ledger.VipLevel, expiresAtMS int64, nowMs int64) ([]ledger.VipRewardItem, string, error) {
	if level.RewardResourceGroupID <= 0 {
		return nil, "", nil
	}
	group, err := r.getResourceGroup(ctx, tx, level.RewardResourceGroupID, true)
	if err != nil {
		return nil, "", err
	}
	if group.Status != resourcedomain.StatusActive {
		return nil, "", xerr.New(xerr.Conflict, "vip reward group is disabled")
	}
	group.Items, err = r.listResourceGroupItemsTx(ctx, tx, level.RewardResourceGroupID, true)
	if err != nil {
		return nil, "", err
	}
	groupSnapshot, err := json.Marshal(group)
	if err != nil {
		return nil, "", err
	}
	rewardCommandID := boundedDerivedCommandID("vip_reward:", command.CommandID)
	grantID := resourceGrantID(command.AppCode, rewardCommandID)
	source := ledger.NormalizeVipGrantSource(command.Source)
	if source == "" {
		source = ledger.VipGrantSourcePurchase
	}
	reason := strings.TrimSpace(command.Reason)
	if reason == "" {
		reason = source
	}
	operatorUserID := command.OperatorUserID
	if operatorUserID <= 0 {
		operatorUserID = command.UserID
	}
	if err := r.insertResourceGrant(ctx, tx, grantID, rewardCommandID, command.UserID, source, resourcedomain.GrantSubjectGroup, fmt.Sprintf("%d", level.RewardResourceGroupID), stableHash(string(groupSnapshot)), string(groupSnapshot), reason, operatorUserID, nowMs); err != nil {
		return nil, "", err
	}
	durationMS := expiresAtMS - nowMs
	rewards := make([]ledger.VipRewardItem, 0, len(group.Items))
	for _, item := range group.Items {
		if resourcedomain.NormalizeGroupItemType(item.ItemType) == resourcedomain.GroupItemTypeWalletAsset {
			inserted, err := r.applyWalletAssetGrantItem(ctx, tx, grantID, rewardCommandID, command.UserID, item, nowMs)
			if err != nil {
				return nil, "", err
			}
			assetType := resourcedomain.NormalizeWalletAssetType(item.WalletAssetType)
			// 首次回执与幂等回放都使用同一资源快照形状，wallet_asset 不能在重试时凭空多出字段。
			rewards = append(rewards, ledger.VipRewardItem{
				ResourceCode: assetType,
				ResourceType: "wallet_asset",
				Name:         assetType,
				Quantity:     inserted.Quantity,
				ExpiresAtMS:  0,
			})
			continue
		}
		if err := validateActiveResourceGroupResource(item.Resource); err != nil {
			return nil, "", err
		}
		if resourcedomain.NormalizeResourceType(item.Resource.ResourceType) == resourcedomain.TypeVIPTrialCard {
			return nil, "", xerr.New(xerr.InvalidArgument, "vip reward group cannot contain vip_trial_card")
		}
		// VIP 每次生效来源（购买/直赠/体验卡）必须拥有独立 entitlement。资源目录常用
		// increase_quantity/extend_expiry，但跨来源合并会让体验卡缩短付费权益或让低等级继续引用高等级素材。
		inserted, err := r.applyGrantItemWithEntitlementStrategy(ctx, tx, grantID, rewardCommandID, command.UserID, item.Resource, item.Quantity, durationMS, nowMs, resourcedomain.GrantStrategyNewEntitlement)
		if err != nil {
			return nil, "", err
		}
		if inserted.EntitlementID != "" {
			if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, expiresAtMS, nowMs); err != nil {
				return nil, "", err
			}
		}
		rewards = append(rewards, ledger.VipRewardItem{
			ResourceID:   item.Resource.ResourceID,
			ResourceCode: item.Resource.ResourceCode,
			ResourceType: item.Resource.ResourceType,
			Name:         item.Resource.Name,
			Quantity:     inserted.Quantity,
			ExpiresAtMS:  expiresAtMS,
			AssetURL:     item.Resource.AssetURL,
			PreviewURL:   item.Resource.PreviewURL,
			AnimationURL: item.Resource.AnimationURL,
		})
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		resourceOutboxEvent("ResourceGroupGranted", rewardCommandID, command.UserID, level.RewardResourceGroupID, map[string]any{"grant_id": grantID, "source": source}, nowMs),
	}); err != nil {
		return nil, "", err
	}
	return rewards, grantID, nil
}

func vipActivatedEvent(transactionID string, commandID string, userID int64, source string, action string, previousLevel int32, previousExpiresAtMS int64, expiryPolicy string, vip ledger.UserVip, rewardGroupID int64, resourceGrantID string, rewardItems []ledger.VipRewardItem, nowMs int64) walletOutboxEvent {
	payload := map[string]any{
		"event_type":               "VipActivated",
		"transaction_id":           transactionID,
		"command_id":               commandID,
		"user_id":                  userID,
		"source":                   source,
		"action":                   action,
		"previous_level":           previousLevel,
		"previous_expires_at_ms":   previousExpiresAtMS,
		"level":                    vip.Level,
		"vip_name":                 vip.Name,
		"started_at_ms":            vip.StartedAtMS,
		"expires_at_ms":            vip.ExpiresAtMS,
		"program_type":             vip.ProgramType,
		"config_version":           vip.ConfigVersion,
		"expiry_policy":            expiryPolicy,
		"reward_resource_group_id": rewardGroupID,
		"resource_grant_id":        resourceGrantID,
		"reward_items":             vipRewardItemsNoticePayload(rewardItems),
		"created_at_ms":            nowMs,
	}
	return walletOutboxEvent{
		EventID:        eventID(transactionID, "VipActivated", userID, vipOutboxAsset),
		EventType:      "VipActivated",
		TransactionID:  transactionID,
		CommandID:      commandID,
		UserID:         userID,
		AssetType:      vipOutboxAsset,
		AvailableDelta: 0,
		FrozenDelta:    0,
		Payload:        payload,
		CreatedAtMS:    nowMs,
	}
}

func vipRewardItemsNoticePayload(items []ledger.VipRewardItem) []map[string]any {
	payload := make([]map[string]any, 0, len(items))
	for _, item := range items {
		payload = append(payload, map[string]any{
			"resource_id":   item.ResourceID,
			"resource_code": item.ResourceCode,
			"resource_type": item.ResourceType,
			"name":          item.Name,
			"quantity":      item.Quantity,
			"expires_at_ms": item.ExpiresAtMS,
			"asset_url":     item.AssetURL,
			"preview_url":   item.PreviewURL,
			"animation_url": item.AnimationURL,
		})
	}
	return payload
}

func (r *Repository) vipPurchaseReceiptForTransaction(ctx context.Context, tx *sql.Tx, txRow transactionRow, userID int64) (ledger.PurchaseVipReceipt, error) {
	var orderID string
	var level int32
	var priceCoin int64
	var grantID string
	var expiresAtMS int64
	var durationMS int64
	var programType string
	var configVersion int64
	var createdAtMS int64
	var currentLevelName string
	err := tx.QueryRowContext(ctx, `
		SELECT o.order_id, o.target_level, o.price_coin, o.resource_grant_id,
		       o.new_expires_at_ms, o.duration_ms, o.program_type, o.config_version,
		       o.created_at_ms, COALESCE(v.name, '')
		FROM vip_purchase_orders o
		LEFT JOIN vip_levels v ON v.app_code = o.app_code AND v.level = o.target_level
		WHERE o.app_code = ? AND o.wallet_transaction_id = ?`,
		appcode.FromContext(ctx), txRow.TransactionID,
	).Scan(&orderID, &level, &priceCoin, &grantID, &expiresAtMS, &durationMS, &programType, &configVersion, &createdAtMS, &currentLevelName)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	var metadata struct {
		VIPName        string `json:"vip_name"`
		StartedAtMS    int64  `json:"started_at_ms"`
		ExpiresAtMS    int64  `json:"expires_at_ms"`
		BalanceVersion int64  `json:"balance_version"`
	}
	_ = json.Unmarshal([]byte(txRow.MetadataJSON), &metadata)
	if expiresAtMS <= 0 {
		expiresAtMS = metadata.ExpiresAtMS
	}
	startedAtMS := metadata.StartedAtMS
	if startedAtMS <= 0 {
		startedAtMS = createdAtMS
		if durationMS > 0 && expiresAtMS > durationMS && expiresAtMS-durationMS < startedAtMS {
			startedAtMS = expiresAtMS - durationMS
		}
	}
	vipName := strings.TrimSpace(metadata.VIPName)
	if vipName == "" {
		vipName = currentLevelName
	}
	vip := ledger.UserVip{
		UserID:        userID,
		Level:         level,
		Name:          vipName,
		Active:        level > 0 && expiresAtMS > time.Now().UnixMilli(),
		StartedAtMS:   startedAtMS,
		ExpiresAtMS:   expiresAtMS,
		UpdatedAtMS:   createdAtMS,
		ProgramType:   programType,
		ConfigVersion: configVersion,
	}
	balance, err := r.balanceAfterTransaction(ctx, tx, txRow.TransactionID, userID, ledger.AssetCoin)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	// 2026-07-15 之后的新交易始终从交易 metadata 还原原始版本。旧交易没有该字段时保持 0，
	// Flutter 看到 version=0 必须走余额查询，不能拿当前账户版本伪装成本次历史回执。
	balance.Version = metadata.BalanceVersion
	rewards, err := r.rewardItemsByGrant(ctx, tx, grantID, expiresAtMS)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	return ledger.PurchaseVipReceipt{
		OrderID:          orderID,
		TransactionID:    txRow.TransactionID,
		Vip:              vip,
		CoinSpent:        priceCoin,
		CoinBalanceAfter: balance.AvailableAmount,
		CoinBalance:      balance,
		RewardItems:      rewards,
	}, nil
}
