package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"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"
)

const (
	bizTypeVIPPurchase              = "vip_purchase"
	bizTypeVIPGrant                 = "vip_grant"
	vipOutboxAsset                  = "VIP"
	managerCenterVIPGrantDurationMS = 30 * 24 * 60 * 60 * 1000
)

// GetWalletOverview 返回钱包首页摘要；开关放在 wallet-service 内，避免 gateway 硬编码账务能力。
func (r *Repository) GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error) {
	// 钱包首页返回所有可展示的资金口袋；工资钱包按身份拆分，0 余额也返回，便于客户端固定布局。
	balances, err := r.GetBalances(ctx, userID, []string{
		ledger.AssetCoin,
		ledger.AssetHostSalaryUSD,
		ledger.AssetAgencySalaryUSD,
		ledger.AssetBDSalaryUSD,
		ledger.AssetAdminSalaryUSD,
	})
	if err != nil {
		return ledger.WalletOverview{}, err
	}
	return ledger.WalletOverview{
		Balances: balances,
		FeatureFlags: ledger.WalletFeatureFlags{
			RechargeEnabled:        true,
			DiamondExchangeEnabled: false,
		},
	}, nil
}

// GetWalletValueSummary 返回我的页钱包卡片需要的最小金币余额。
func (r *Repository) GetWalletValueSummary(ctx context.Context, userID int64) (ledger.WalletValueSummary, error) {
	balances, err := r.GetBalances(ctx, userID, []string{ledger.AssetCoin})
	if err != nil {
		return ledger.WalletValueSummary{}, err
	}
	if len(balances) == 0 {
		return ledger.WalletValueSummary{}, nil
	}
	return ledger.WalletValueSummary{
		CoinAmount:  balances[0].AvailableAmount,
		UpdatedAtMS: balances[0].UpdatedAtMs,
	}, nil
}

// GetUserGiftWall 读取送礼事务维护的收礼聚合表；它只表达已扣费成功的礼物事实。
func (r *Repository) GetUserGiftWall(ctx context.Context, userID int64) (ledger.UserGiftWall, error) {
	if r == nil || r.db == nil {
		return ledger.UserGiftWall{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	appCode := appcode.FromContext(ctx)
	rows, err := r.db.QueryContext(ctx, `
		SELECT gift_id, gift_name, resource_id, COALESCE(CAST(resource_snapshot_json AS CHAR), '{}'),
		       gift_type_code, COALESCE(CAST(presentation_json AS CHAR), '{}'), gift_count,
		       total_value, total_coin_value, total_diamond_value, total_gift_point, total_heat_value,
		       charge_asset_type, last_price_version, first_received_at_ms, last_received_at_ms, sort_order
		FROM user_gift_wall
		WHERE app_code = ? AND user_id = ?
		ORDER BY sort_order ASC, total_value DESC, last_received_at_ms DESC, gift_id ASC`,
		appCode, userID,
	)
	if err != nil {
		return ledger.UserGiftWall{}, err
	}
	defer rows.Close()

	wall := ledger.UserGiftWall{Items: make([]ledger.GiftWallItem, 0)}
	for rows.Next() {
		var item ledger.GiftWallItem
		if err := rows.Scan(
			&item.GiftID,
			&item.GiftName,
			&item.ResourceID,
			&item.ResourceSnapshotJSON,
			&item.GiftTypeCode,
			&item.PresentationJSON,
			&item.GiftCount,
			&item.TotalValue,
			&item.TotalCoinValue,
			&item.TotalDiamondValue,
			&item.TotalGiftPoint,
			&item.TotalHeatValue,
			&item.ChargeAssetType,
			&item.LastPriceVersion,
			&item.FirstReceivedAtMS,
			&item.LastReceivedAtMS,
			&item.SortOrder,
		); err != nil {
			return ledger.UserGiftWall{}, err
		}
		item.Resource = giftWallResourceFromJSON(item.ResourceSnapshotJSON)
		wall.Items = append(wall.Items, item)
		wall.GiftKindCount++
		wall.GiftTotalCount += item.GiftCount
		wall.TotalValue += item.TotalValue
		wall.TotalCoinValue += item.TotalCoinValue
		wall.TotalDiamondValue += item.TotalDiamondValue
		wall.TotalGiftPoint += item.TotalGiftPoint
		wall.TotalHeatValue += item.TotalHeatValue
	}
	if err := rows.Err(); err != nil {
		return ledger.UserGiftWall{}, err
	}
	return wall, nil
}

func giftWallResourceFromJSON(raw string) ledger.GiftWallResource {
	var resource ledger.GiftWallResource
	if strings.TrimSpace(raw) == "" {
		return resource
	}
	// 礼物墙统计不能因为展示资源快照异常而丢失；原始 JSON 仍会返回给客户端用于排查。
	_ = json.Unmarshal([]byte(raw), &resource)
	return resource
}

// GetDiamondExchangeConfig 返回后台配置的钻石兑换规则，未配置时返回空列表而不是 gateway 默认值。
func (r *Repository) GetDiamondExchangeConfig(ctx context.Context, _ int64) ([]ledger.DiamondExchangeRule, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	return []ledger.DiamondExchangeRule{}, nil
}

// ListWalletTransactions 分页读取当前用户钱包分录，并带出交易业务类型。
func (r *Repository) ListWalletTransactions(ctx context.Context, query ledger.ListWalletTransactionsQuery) ([]ledger.WalletTransaction, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	query.AppCode = appcode.FromContext(ctx)
	query.Page, query.PageSize = normalizePage(query.Page, query.PageSize)
	where := `WHERE e.app_code = ? AND e.user_id = ?`
	args := []any{query.AppCode, query.UserID}
	if strings.TrimSpace(query.AssetType) != "" {
		where += ` AND e.asset_type = ?`
		args = append(args, query.AssetType)
	}

	var total int64
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM wallet_entries e
		`+where,
		args...,
	).Scan(&total); err != nil {
		return nil, 0, err
	}

	rows, err := r.db.QueryContext(ctx, `
		SELECT e.entry_id, e.transaction_id, wt.biz_type, e.asset_type,
		       e.available_delta, e.frozen_delta, e.available_after, e.frozen_after,
		       e.counterparty_user_id, e.room_id, e.created_at_ms
		FROM wallet_entries e
		JOIN wallet_transactions wt ON wt.app_code = e.app_code AND wt.transaction_id = e.transaction_id
		`+where+`
		ORDER BY e.created_at_ms DESC, e.entry_id DESC
		LIMIT ? OFFSET ?`,
		append(args, query.PageSize, resourceOffset(query.Page, query.PageSize))...,
	)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	items := make([]ledger.WalletTransaction, 0, query.PageSize)
	for rows.Next() {
		var item ledger.WalletTransaction
		if err := rows.Scan(
			&item.EntryID,
			&item.TransactionID,
			&item.BizType,
			&item.AssetType,
			&item.AvailableDelta,
			&item.FrozenDelta,
			&item.AvailableAfter,
			&item.FrozenAfter,
			&item.CounterpartyUserID,
			&item.RoomID,
			&item.CreatedAtMS,
		); err != nil {
			return nil, 0, err
		}
		items = append(items, item)
	}
	return items, total, rows.Err()
}

// GetMyVip 返回用户当前 VIP 状态；过期只影响 active 投影，不直接修改历史事实。
func (r *Repository) GetMyVip(ctx context.Context, userID int64) (ledger.UserVip, error) {
	if r == nil || r.db == nil {
		return ledger.UserVip{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	vip, err := r.queryUserVip(ctx, r.db, userID, time.Now().UnixMilli(), false)
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.UserVip{UserID: userID}, nil
	}
	return vip, err
}

// ListVipPackages 返回当前会员和全部 active VIP 包。
func (r *Repository) ListVipPackages(ctx context.Context, userID int64) (ledger.UserVip, []ledger.VipLevel, error) {
	if r == nil || r.db == nil {
		return ledger.UserVip{}, nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	nowMs := time.Now().UnixMilli()
	current, err := r.GetMyVip(ctx, userID)
	if err != nil {
		return ledger.UserVip{}, nil, err
	}
	totalRechargeCoin, err := r.userRechargeCoinAmount(ctx, r.db, userID)
	if err != nil {
		return ledger.UserVip{}, nil, err
	}
	levels, err := r.listVipLevels(ctx, nowMs, current.Level, totalRechargeCoin)
	return current, levels, err
}

// ListAdminVipLevels 返回后台配置页所需的全部 VIP 等级。
func (r *Repository) ListAdminVipLevels(ctx context.Context) ([]ledger.VipLevel, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	rows, err := r.db.QueryContext(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 = ?
		ORDER BY level ASC`,
		appcode.FromContext(ctx),
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	levels := make([]ledger.VipLevel, 0, 10)
	for rows.Next() {
		level, err := r.scanVipLevel(rows)
		if err != nil {
			return nil, err
		}
		if level.RewardResourceGroupID > 0 {
			level.RewardItems, err = r.vipRewardItems(ctx, level.RewardResourceGroupID, time.Now().UnixMilli()+level.DurationMS)
			if err != nil {
				return nil, err
			}
		}
		levels = append(levels, level)
	}
	return levels, rows.Err()
}

// UpdateAdminVipLevels 保存完整 10 级 VIP 配置。
func (r *Repository) UpdateAdminVipLevels(ctx context.Context, levels []ledger.AdminVipLevelCommand, operatorUserID int64) ([]ledger.VipLevel, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if operatorUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "operator_user_id is required")
	}
	normalized, err := normalizeAdminVipLevelCommands(levels)
	if err != nil {
		return nil, err
	}

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer func() { _ = tx.Rollback() }()

	nowMs := time.Now().UnixMilli()
	for _, level := range normalized {
		if level.Status == ledger.VipStatusActive {
			if err := r.requireActiveResourceGroup(ctx, tx, level.RewardResourceGroupID); err != nil {
				return 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
			 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 = VALUES(required_recharge_coin_amount),
			   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.RequiredRechargeCoinAmount,
			level.SortOrder,
			nowMs,
			nowMs,
		); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return r.ListAdminVipLevels(ctx)
}

// 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)

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := stableHash(fmt.Sprintf("vip_purchase|%s|%d|%d", command.AppCode, command.UserID, command.Level))
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeVIPPurchase); err != nil || exists {
		if err != nil || !exists {
			return ledger.PurchaseVipReceipt{}, err
		}
		return r.vipPurchaseReceiptForTransaction(ctx, tx, txRow.TransactionID, command.UserID)
	}

	nowMs := time.Now().UnixMilli()
	level, err := r.getVipLevelForUpdate(ctx, tx, command.Level)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if level.Status != ledger.VipStatusActive {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPLevelDisabled, "vip level is disabled")
	}
	current, err := r.ensureUserVipForUpdate(ctx, tx, command.UserID, nowMs)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	previousLevel := int32(0)
	baseExpiresAt := nowMs
	if current.Active {
		previousLevel = current.Level
		baseExpiresAt = current.ExpiresAtMS
		if current.Level > command.Level {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed")
		}
	}
	if level.RechargeGateRequired {
		totalRechargeCoin, err := r.userRechargeCoinAmount(ctx, tx, command.UserID)
		if err != nil {
			return ledger.PurchaseVipReceipt{}, err
		}
		if totalRechargeCoin < level.RequiredRechargeCoinAmount {
			return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPRechargeRequired, "vip recharge requirement is not met")
		}
	}
	newExpiresAt := baseExpiresAt + level.DurationMS
	account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if account.AvailableAmount < level.PriceCoin {
		return ledger.PurchaseVipReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient 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,
		"previous_level":                previousLevel,
		"price_coin":                    level.PriceCoin,
		"expires_at_ms":                 newExpiresAt,
		"balance_after":                 coinBalanceAfter,
		"reward_group_id":               level.RewardResourceGroupID,
		"duration_ms":                   level.DurationMS,
		"required_recharge_coin_amount": level.RequiredRechargeCoinAmount,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPPurchase, requestHash, orderID, metadata, nowMs); err != nil {
		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,
		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, price_coin,
		   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,
		level.PriceCoin, 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),
		{
			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.VIP, level.RewardResourceGroupID, activation.ResourceGrantID, activation.RewardItems, nowMs),
	}); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}

	return ledger.PurchaseVipReceipt{
		OrderID:          orderID,
		TransactionID:    transactionID,
		Vip:              activation.VIP,
		CoinSpent:        level.PriceCoin,
		CoinBalanceAfter: coinBalanceAfter,
		RewardItems:      activation.RewardItems,
	}, nil
}

// GrantVip 是活动和后台赠送 VIP 的统一账务入口；它不扣金币，也不检查充值门槛。
func (r *Repository) GrantVip(ctx context.Context, command ledger.GrantVipCommand) (ledger.GrantVipReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.GrantVipReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	command.GrantSource = ledger.NormalizeVipGrantSource(command.GrantSource)
	if command.GrantSource == "" || command.GrantSource == ledger.VipGrantSourcePurchase {
		return ledger.GrantVipReceipt{}, xerr.New(xerr.InvalidArgument, "vip grant_source is invalid")
	}
	command.Reason = strings.TrimSpace(command.Reason)
	if command.Reason == "" {
		command.Reason = command.GrantSource
	}

	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := vipGrantRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeVIPGrant); err != nil || exists {
		if err != nil || !exists {
			return ledger.GrantVipReceipt{}, err
		}
		return r.vipGrantReceiptForTransaction(ctx, tx, txRow, command.TargetUserID)
	}

	nowMs := time.Now().UnixMilli()
	level, err := r.getVipLevelForUpdate(ctx, tx, command.Level)
	if err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	if level.Status != ledger.VipStatusActive {
		return ledger.GrantVipReceipt{}, xerr.New(xerr.VIPLevelDisabled, "vip level is disabled")
	}
	if command.GrantSource == ledger.VipGrantSourceManagerCenter {
		// 经理中心只能赠送 30 天 VIP；这里覆盖配置里的购买周期，保证 H5 即使不能传 duration，也不会受后台 VIP 套餐周期影响。
		level.DurationMS = managerCenterVIPGrantDurationMS
	}
	current, err := r.ensureUserVipForUpdate(ctx, tx, command.TargetUserID, nowMs)
	if err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	recordID := "vip_grant_" + stableHash(command.AppCode+"|"+command.CommandID)
	expectedResourceGrantID := resourceGrantID(command.AppCode, "vip_reward:"+command.CommandID)
	metadata := map[string]any{
		"app_code":                   command.AppCode,
		"grant_id":                   recordID,
		"resource_grant_id":          expectedResourceGrantID,
		"user_id":                    command.TargetUserID,
		"target_level":               command.Level,
		"grant_source":               command.GrantSource,
		"operator_user_id":           command.OperatorUserID,
		"reason":                     command.Reason,
		"reward_group_id":            level.RewardResourceGroupID,
		"duration_ms":                level.DurationMS,
		"transaction_id":             transactionID,
		"required_recharge_bypassed": true,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeVIPGrant, requestHash, recordID, metadata, nowMs); err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	activation, err := r.activateUserVip(ctx, tx, vipActivationCommand{
		AppCode:        command.AppCode,
		CommandID:      command.CommandID,
		UserID:         command.TargetUserID,
		Source:         command.GrantSource,
		Reason:         command.Reason,
		OperatorUserID: command.OperatorUserID,
		TransactionID:  transactionID,
		RecordID:       recordID,
		Level:          level,
		Current:        current,
		NowMS:          nowMs,
	})
	if err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		vipActivatedEvent(transactionID, command.CommandID, command.TargetUserID, command.GrantSource, activation.Action, activation.PreviousLevel, activation.VIP, level.RewardResourceGroupID, activation.ResourceGrantID, activation.RewardItems, nowMs),
	}); err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	return ledger.GrantVipReceipt{
		TransactionID: transactionID,
		Vip:           activation.VIP,
		RewardItems:   activation.RewardItems,
	}, nil
}

type vipActivationCommand struct {
	AppCode        string
	CommandID      string
	UserID         int64
	Source         string
	Reason         string
	OperatorUserID int64
	TransactionID  string
	RecordID       string
	Level          ledger.VipLevel
	Current        ledger.UserVip
	NowMS          int64
}

type vipActivationResult struct {
	VIP             ledger.UserVip
	PreviousLevel   int32
	Action          string
	ResourceGrantID string
	RewardItems     []ledger.VipRewardItem
}

func (r *Repository) activateUserVip(ctx context.Context, tx *sql.Tx, command vipActivationCommand) (vipActivationResult, error) {
	previousLevel := int32(0)
	baseExpiresAt := command.NowMS
	startedAt := command.NowMS
	if command.Current.Active {
		previousLevel = command.Current.Level
		baseExpiresAt = command.Current.ExpiresAtMS
		startedAt = command.Current.StartedAtMS
		if command.Current.Level > command.Level.Level {
			return vipActivationResult{}, xerr.New(xerr.VIPDowngradeNotAllowed, "vip downgrade is not allowed")
		}
	}
	expiresAt := baseExpiresAt + command.Level.DurationMS
	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 _, err := tx.ExecContext(ctx, `
		UPDATE user_vip_memberships
		SET level = ?, name = ?, status = ?, started_at_ms = ?, expires_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?`,
		command.Level.Level, command.Level.Name, ledger.VipStatusActive, startedAt, expiresAt, command.NowMS, command.AppCode, command.UserID,
	); 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, order_id, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
		command.AppCode, command.UserID, action, previousLevel, command.Level.Level, command.RecordID, command.NowMS,
	); err != nil {
		return vipActivationResult{}, err
	}
	return vipActivationResult{
		VIP: ledger.UserVip{
			UserID:      command.UserID,
			Level:       command.Level.Level,
			Name:        command.Level.Name,
			Active:      true,
			StartedAtMS: startedAt,
			ExpiresAtMS: expiresAt,
			UpdatedAtMS: command.NowMS,
		},
		PreviousLevel:   previousLevel,
		Action:          action,
		ResourceGrantID: resourceGrantID,
		RewardItems:     rewardItems,
	}, 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) listVipLevels(ctx context.Context, nowMs int64, currentLevel int32, totalRechargeCoin int64) ([]ledger.VipLevel, error) {
	rows, err := r.db.QueryContext(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 status = ?
		ORDER BY sort_order ASC, level ASC`,
		appcode.FromContext(ctx), ledger.VipStatusActive,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	levels := make([]ledger.VipLevel, 0)
	for rows.Next() {
		level, err := r.scanVipLevel(rows)
		if err != nil {
			return nil, err
		}
		level.UserRechargeCoinAmount = totalRechargeCoin
		level.RechargeGateRequired = level.Level >= 6
		level.CanPurchase = currentLevel == 0 || level.Level >= currentLevel
		if !level.CanPurchase {
			level.PurchaseLockedReason = "current_vip_higher"
		}
		if level.CanPurchase && level.RechargeGateRequired && totalRechargeCoin < level.RequiredRechargeCoinAmount {
			level.CanPurchase = false
			level.PurchaseLockedReason = "recharge_required"
		}
		level.RewardItems, err = r.vipRewardItems(ctx, level.RewardResourceGroupID, nowMs+level.DurationMS)
		if err != nil {
			return nil, err
		}
		levels = append(levels, level)
	}
	return levels, rows.Err()
}

func (r *Repository) scanVipLevel(scanner scanTarget) (ledger.VipLevel, error) {
	var level ledger.VipLevel
	err := scanner.Scan(
		&level.Level,
		&level.Name,
		&level.Status,
		&level.PriceCoin,
		&level.DurationMS,
		&level.RewardResourceGroupID,
		&level.RequiredRechargeCoinAmount,
		&level.SortOrder,
		&level.CreatedAtMS,
		&level.UpdatedAtMS,
	)
	level.RechargeGateRequired = level.Level >= 6
	return level, err
}

func (r *Repository) userRechargeCoinAmount(ctx context.Context, querier interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}, userID int64) (int64, error) {
	var total int64
	err := querier.QueryRowContext(ctx, `
		SELECT total_coin_amount
		FROM wallet_user_recharge_stats
		WHERE app_code = ? AND user_id = ?`,
		appcode.FromContext(ctx), userID,
	).Scan(&total)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, nil
	}
	return total, err
}

func normalizeAdminVipLevelCommands(levels []ledger.AdminVipLevelCommand) ([]ledger.AdminVipLevelCommand, error) {
	if len(levels) != 10 {
		return nil, xerr.New(xerr.InvalidArgument, "vip config requires exactly 10 levels")
	}
	byLevel := make(map[int32]ledger.AdminVipLevelCommand, 10)
	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 > 10 {
			return nil, xerr.New(xerr.InvalidArgument, "vip level must be between 1 and 10")
		}
		if _, exists := byLevel[level.Level]; exists {
			return nil, xerr.New(xerr.InvalidArgument, "vip level is duplicated")
		}
		if level.Level <= 5 {
			level.RequiredRechargeCoinAmount = 0
		}
		if err := validateAdminVipLevelCommand(level); err != nil {
			return nil, err
		}
		byLevel[level.Level] = level
	}
	normalized := make([]ledger.AdminVipLevelCommand, 0, 10)
	for level := int32(1); level <= 10; level++ {
		item, ok := byLevel[level]
		if !ok {
			return nil, xerr.New(xerr.InvalidArgument, "vip config requires levels 1 to 10")
		}
		normalized = append(normalized, item)
	}
	return normalized, nil
}

func validateAdminVipLevelCommand(level ledger.AdminVipLevelCommand) 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 || level.RequiredRechargeCoinAmount < 0 {
		return xerr.New(xerr.InvalidArgument, "vip level config is invalid")
	}
	if level.Status != ledger.VipStatusActive {
		return nil
	}
	if level.PriceCoin <= 0 {
		return xerr.New(xerr.InvalidArgument, "active vip level price_coin must be greater than zero")
	}
	if level.RewardResourceGroupID <= 0 {
		return xerr.New(xerr.InvalidArgument, "active vip level reward_resource_group_id is required")
	}
	if level.Level >= 6 && level.RequiredRechargeCoinAmount <= 0 {
		return xerr.New(xerr.InvalidArgument, "vip level 6-10 requires recharge threshold")
	}
	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
}

func (r *Repository) ensureUserVipForUpdate(ctx context.Context, tx *sql.Tx, userID int64, nowMs int64) (ledger.UserVip, error) {
	if _, err := tx.ExecContext(ctx, `
		INSERT IGNORE INTO user_vip_memberships (
		   app_code, user_id, level, name, status, started_at_ms, expires_at_ms, created_at_ms, updated_at_ms
		 ) VALUES (?, ?, 0, '', 'inactive', 0, 0, ?, ?)`,
		appcode.FromContext(ctx), userID, nowMs, nowMs,
	); err != nil {
		return ledger.UserVip{}, err
	}
	return r.queryUserVip(ctx, tx, userID, nowMs, true)
}

func (r *Repository) queryUserVip(ctx context.Context, querier interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}, userID int64, nowMs int64, lock bool) (ledger.UserVip, error) {
	query := `
		SELECT m.user_id, m.level, COALESCE(v.name, m.name), m.started_at_ms, m.expires_at_ms, m.updated_at_ms
		FROM user_vip_memberships m
		LEFT JOIN vip_levels v ON v.app_code = m.app_code AND v.level = m.level
		WHERE m.app_code = ? AND m.user_id = ?`
	if lock {
		query += ` FOR UPDATE`
	}
	var vip ledger.UserVip
	if err := querier.QueryRowContext(ctx, query, appcode.FromContext(ctx), userID).Scan(&vip.UserID, &vip.Level, &vip.Name, &vip.StartedAtMS, &vip.ExpiresAtMS, &vip.UpdatedAtMS); err != nil {
		return ledger.UserVip{}, err
	}
	vip.Active = vip.Level > 0 && vip.ExpiresAtMS > nowMs
	if !vip.Active {
		vip.Level = 0
		vip.Name = ""
	}
	return vip, nil
}

type vipRewardGrantCommand struct {
	AppCode        string
	CommandID      string
	UserID         int64
	Source         string
	Reason         string
	OperatorUserID int64
}

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 := "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
			}
			rewards = append(rewards, ledger.VipRewardItem{Quantity: inserted.Quantity, ExpiresAtMS: 0})
			continue
		}
		if err := validateActiveResourceGroupResource(item.Resource); err != nil {
			return nil, "", err
		}
		inserted, err := r.applyGrantItem(ctx, tx, grantID, rewardCommandID, command.UserID, item.Resource, item.Quantity, durationMS, nowMs)
		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 (r *Repository) vipRewardItems(ctx context.Context, groupID int64, expiresAtMS int64) ([]ledger.VipRewardItem, error) {
	if groupID <= 0 {
		return nil, nil
	}
	items, err := r.listResourceGroupItemsWithQuery(ctx, r.db, groupID)
	if err != nil {
		return nil, err
	}
	rewards := make([]ledger.VipRewardItem, 0, len(items))
	for _, item := range items {
		if resourcedomain.NormalizeGroupItemType(item.ItemType) != resourcedomain.GroupItemTypeResource {
			continue
		}
		rewards = append(rewards, ledger.VipRewardItem{
			ResourceID:   item.Resource.ResourceID,
			ResourceCode: item.Resource.ResourceCode,
			ResourceType: item.Resource.ResourceType,
			Name:         item.Resource.Name,
			Quantity:     item.Quantity,
			ExpiresAtMS:  expiresAtMS,
			AssetURL:     item.Resource.AssetURL,
			PreviewURL:   item.Resource.PreviewURL,
			AnimationURL: item.Resource.AnimationURL,
		})
	}
	return rewards, nil
}

func vipActivatedEvent(transactionID string, commandID string, userID int64, source string, action string, previousLevel int32, 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,
		"level":                    vip.Level,
		"vip_name":                 vip.Name,
		"started_at_ms":            vip.StartedAtMS,
		"expires_at_ms":            vip.ExpiresAtMS,
		"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, transactionID string, userID int64) (ledger.PurchaseVipReceipt, error) {
	var orderID string
	var level int32
	var priceCoin int64
	var grantID string
	err := tx.QueryRowContext(ctx, `
		SELECT order_id, target_level, price_coin, resource_grant_id
		FROM vip_purchase_orders
		WHERE app_code = ? AND wallet_transaction_id = ?`,
		appcode.FromContext(ctx), transactionID,
	).Scan(&orderID, &level, &priceCoin, &grantID)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	vip, err := r.queryUserVip(ctx, tx, userID, time.Now().UnixMilli(), false)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	balance, err := r.balanceAfterTransaction(ctx, tx, transactionID, userID, ledger.AssetCoin)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	rewards, err := r.rewardItemsByGrant(ctx, tx, grantID, vip.ExpiresAtMS)
	if err != nil {
		return ledger.PurchaseVipReceipt{}, err
	}
	return ledger.PurchaseVipReceipt{
		OrderID:          orderID,
		TransactionID:    transactionID,
		Vip:              vip,
		CoinSpent:        priceCoin,
		CoinBalanceAfter: balance.AvailableAmount,
		RewardItems:      rewards,
	}, nil
}

func (r *Repository) vipGrantReceiptForTransaction(ctx context.Context, tx *sql.Tx, txRow transactionRow, userID int64) (ledger.GrantVipReceipt, error) {
	var metadata struct {
		ResourceGrantID string `json:"resource_grant_id"`
	}
	_ = json.Unmarshal([]byte(txRow.MetadataJSON), &metadata)
	vip, err := r.queryUserVip(ctx, tx, userID, time.Now().UnixMilli(), false)
	if err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	rewards, err := r.rewardItemsByGrant(ctx, tx, metadata.ResourceGrantID, vip.ExpiresAtMS)
	if err != nil {
		return ledger.GrantVipReceipt{}, err
	}
	return ledger.GrantVipReceipt{
		TransactionID: txRow.TransactionID,
		Vip:           vip,
		RewardItems:   rewards,
	}, nil
}

func (r *Repository) rewardItemsByGrant(ctx context.Context, tx *sql.Tx, grantID string, expiresAtMS int64) ([]ledger.VipRewardItem, error) {
	if grantID == "" {
		return nil, nil
	}
	rows, err := tx.QueryContext(ctx, `
		SELECT i.resource_id, r.resource_code, r.resource_type, r.name, i.quantity,
		       COALESCE(r.asset_url, ''), COALESCE(r.preview_url, ''), COALESCE(r.animation_url, '')
		FROM resource_grant_items i
		JOIN resources r ON r.app_code = i.app_code AND r.resource_id = i.resource_id
		WHERE i.app_code = ? AND i.grant_id = ? AND i.result_type = ?
		ORDER BY i.grant_item_id ASC`,
		appcode.FromContext(ctx), grantID, resourcedomain.ResultEntitlement,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	items := make([]ledger.VipRewardItem, 0)
	for rows.Next() {
		var item ledger.VipRewardItem
		if err := rows.Scan(
			&item.ResourceID,
			&item.ResourceCode,
			&item.ResourceType,
			&item.Name,
			&item.Quantity,
			&item.AssetURL,
			&item.PreviewURL,
			&item.AnimationURL,
		); err != nil {
			return nil, err
		}
		item.ExpiresAtMS = expiresAtMS
		items = append(items, item)
	}
	return items, rows.Err()
}

func vipGrantRequestHash(command ledger.GrantVipCommand) string {
	return stableHash(fmt.Sprintf("vip_grant|%s|%d|%d|%s|%d|%s",
		appcode.Normalize(command.AppCode),
		command.TargetUserID,
		command.Level,
		ledger.NormalizeVipGrantSource(command.GrantSource),
		command.OperatorUserID,
		strings.TrimSpace(command.Reason),
	))
}
