package resource

import (
	"encoding/json"
	"fmt"
	"math"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/middleware"
	walletv1 "hyapp.local/api/proto/wallet/v1"

	"github.com/gin-gonic/gin"
)

const dayMillis int64 = 24 * 60 * 60 * 1000

const (
	resourceTypeCoin         = "coin"
	resourceTypeAvatarFrame  = "avatar_frame"
	resourceTypeBadge        = "badge"
	resourceTypeEmojiPack    = "emoji_pack"
	resourceTypeGiftTraySkin = "gift_tray_skin"
	resourceTypeMicAnimation = "mic_seat_animation"
	resourceTypeProfileCard  = "profile_card"
	resourceTypeRoomBorder   = "room_border"
	resourceTypeRoomName     = "room_name_style"
	resourceTypeVIPTrialCard = "vip_trial_card"
)

var roomNameStyleColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`)

var vipVisualResourceFormats = map[string]struct{}{
	"gif": {}, "jpeg": {}, "mp4": {}, "pag": {}, "png": {}, "svga": {}, "webp": {},
}

const (
	resourceAnimationFormatMP4 = "mp4"
)

const (
	mp4AlphaLayoutNormal                  = "normal"
	mp4AlphaLayoutAlphaLeftRGBRight       = "alpha_left_rgb_right"
	mp4AlphaLayoutRGBLeftAlphaRight       = "rgb_left_alpha_right"
	mp4AlphaLayoutAlphaTopRGBBottom       = "alpha_top_rgb_bottom"
	mp4AlphaLayoutRGBTopAlphaBottom       = "rgb_top_alpha_bottom"
	mp4AlphaLayoutCustom                  = "custom"
	mp4AlphaLayoutDetectVersion     int64 = 1
)

const (
	resourcePriceTypeCoin = "coin"
	resourcePriceTypeFree = "free"
)

const (
	badgeFormStrip = "strip"
	badgeFormTile  = "tile"
)

const (
	badgeKindNormal = "normal"
	badgeKindLevel  = "level"
)

const (
	avatarFrameKindNormal = "normal"
	avatarFrameKindLevel  = "level"
)

const (
	defaultEmojiPackCategory = "默认"
	emojiPackPricingFree     = "free"
	emojiPackPricingPaid     = "paid"
)

type resourceRequest struct {
	ResourceCode        string `json:"resourceCode"`
	ResourceType        string `json:"resourceType"`
	Name                string `json:"name"`
	Status              string `json:"status"`
	Amount              int64  `json:"amount"`
	PriceType           string `json:"priceType"`
	CoinPrice           int64  `json:"coinPrice"`
	AvatarFrameKind     string `json:"avatarFrameKind"`
	BadgeForm           string `json:"badgeForm"`
	BadgeKind           string `json:"badgeKind"`
	LevelTrack          string `json:"levelTrack"`
	MetadataJSON        string `json:"metadataJson"`
	ManagerGrantEnabled *bool  `json:"managerGrantEnabled"`
	AssetURL            string `json:"assetUrl"`
	PreviewURL          string `json:"previewUrl"`
	AnimationURL        string `json:"animationUrl"`
	SortOrder           int32  `json:"sortOrder"`
	VIPLevel            int32  `json:"vipLevel"`
	DurationDays        int64  `json:"durationDays"`
}

type resourceMP4LayoutBatchRequest struct {
	Items []resourceMP4LayoutUpdateRequest `json:"items"`
}

type resourceDeleteBatchRequest struct {
	ResourceIDs []int64 `json:"resourceIds"`
}

type resourceMP4LayoutUpdateRequest struct {
	ResourceID   int64  `json:"resourceId"`
	MetadataJSON string `json:"metadataJson"`
}

type emojiPackRequest struct {
	Name         string  `json:"name"`
	Category     string  `json:"category"`
	PricingType  string  `json:"pricingType"`
	CoverURL     string  `json:"coverUrl"`
	AnimationURL string  `json:"animationUrl"`
	RegionIDs    []int64 `json:"regionIds"`
	SortOrder    int32   `json:"sortOrder"`
}

type emojiPackMetadataPayload struct {
	Category    string  `json:"category"`
	PricingType string  `json:"pricing_type"`
	RegionIDs   []int64 `json:"region_ids,omitempty"`
	RegionScope string  `json:"region_scope"`
}

type badgeMetadataPayload struct {
	BadgeForm   string `json:"badge_form"`
	BadgeKind   string `json:"badge_kind,omitempty"`
	LevelTrack  string `json:"level_track,omitempty"`
	DefaultSlot string `json:"default_slot,omitempty"`
}

type resourceMetadataPayload struct {
	AnimationFormat   string                            `json:"animation_format,omitempty"`
	AvatarFrameKind   string                            `json:"avatar_frame_kind,omitempty"`
	Format            string                            `json:"format,omitempty"`
	LevelTrack        string                            `json:"level_track,omitempty"`
	MP4AlphaLayout    *mp4AlphaLayoutMetadataPayload    `json:"mp4_alpha_layout,omitempty"`
	ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
	TextColors        []string                          `json:"text_colors,omitempty"`
	Stops             []float64                         `json:"stops,omitempty"`
	AngleDegrees      *float64                          `json:"angle_degrees,omitempty"`
	VIPLevel          int32                             `json:"vip_level,omitempty"`
	DurationDays      int64                             `json:"duration_days,omitempty"`
}

type mp4AlphaLayoutMetadataPayload struct {
	AlphaLayout   string  `json:"alpha_layout"`
	VideoWidth    int64   `json:"video_w"`
	VideoHeight   int64   `json:"video_h"`
	RGBFrame      []int64 `json:"rgb_frame"`
	AlphaFrame    []int64 `json:"alpha_frame"`
	Confirmed     bool    `json:"confirmed"`
	Confidence    float64 `json:"confidence,omitempty"`
	DetectVersion int64   `json:"detect_version"`
}

type profileCardMetadataPayload struct {
	ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"`
}

type profileCardLayoutMetadataPayload struct {
	SourceWidth        int64   `json:"source_width"`
	SourceHeight       int64   `json:"source_height"`
	ColorContentWidth  int64   `json:"color_content_width"`
	ContentTop         int64   `json:"content_top"`
	ContentBottom      int64   `json:"content_bottom"`
	ContentHeight      int64   `json:"content_height"`
	ContentTopRatio    float64 `json:"content_top_ratio"`
	ContentHeightRatio float64 `json:"content_height_ratio"`
	DetectVersion      int64   `json:"detect_version"`
}

type resourceGroupRequest struct {
	GroupCode   string                     `json:"groupCode"`
	Name        string                     `json:"name"`
	Status      string                     `json:"status"`
	Description string                     `json:"description"`
	SortOrder   int32                      `json:"sortOrder"`
	Items       []resourceGroupItemRequest `json:"items"`
}

type resourceGroupItemRequest struct {
	ItemType     string `json:"itemType"`
	ResourceID   int64  `json:"resourceId"`
	AssetType    string `json:"assetType"`
	Amount       int64  `json:"amount"`
	DurationDays int64  `json:"durationDays"`
	SortOrder    int32  `json:"sortOrder"`
}

type giftRequest struct {
	GiftID           string   `json:"giftId"`
	ResourceID       int64    `json:"resourceId"`
	Status           string   `json:"status"`
	Name             string   `json:"name"`
	SortOrder        int32    `json:"sortOrder"`
	PresentationJSON string   `json:"presentationJson"`
	PriceVersion     string   `json:"priceVersion"`
	GiftTypeCode     string   `json:"giftTypeCode"`
	VIPLevel         int32    `json:"vipLevel"`
	CPRelationType   string   `json:"cpRelationType"`
	ChargeAssetType  string   `json:"chargeAssetType"`
	CoinPrice        int64    `json:"coinPrice"`
	HeatValue        int64    `json:"heatValue"` // 兼容旧后台请求；新后台不再展示或提交热度。
	EffectiveAtMS    int64    `json:"effectiveAtMs"`
	EffectiveFromMS  int64    `json:"effectiveFromMs"`
	EffectiveToMS    int64    `json:"effectiveToMs"`
	EffectTypes      []string `json:"effectTypes"`
	RegionIDs        []int64  `json:"regionIds"`
}

type giftBatchRequest struct {
	Items []giftRequest `json:"items"`
}

type giftTypeRequest struct {
	DisplayName string `json:"displayName"`
	TabName     string `json:"tabName"`
	Status      string `json:"status"`
	SortOrder   int32  `json:"sortOrder"`
}

type giftTypesRequest struct {
	Items []giftTypeItemRequest `json:"items"`
}

type giftTypeItemRequest struct {
	TabKey      string `json:"tabKey"`
	DisplayName string `json:"displayName"`
	TabName     string `json:"tabName"`
	Status      string `json:"status"`
	SortOrder   int32  `json:"sortOrder"`
}

type grantResourceRequest struct {
	CommandID    string `json:"commandId"`
	TargetUserID any    `json:"targetUserId"`
	ResourceID   int64  `json:"resourceId"`
	Quantity     int64  `json:"quantity"`
	DurationMS   int64  `json:"durationMs"`
	Reason       string `json:"reason"`
}

type grantResourcesRequest struct {
	CommandID    string                      `json:"commandId"`
	TargetUserID any                         `json:"targetUserId"`
	Resources    []grantResourcesItemRequest `json:"resources"`
	ResourceIDs  []int64                     `json:"resourceIds"`
	Quantity     int64                       `json:"quantity"`
	DurationMS   int64                       `json:"durationMs"`
	Reason       string                      `json:"reason"`
}

type grantResourcesItemRequest struct {
	ResourceID int64 `json:"resourceId"`
	Quantity   int64 `json:"quantity"`
	DurationMS int64 `json:"durationMs"`
}

type grantGroupRequest struct {
	CommandID    string `json:"commandId"`
	TargetUserID any    `json:"targetUserId"`
	GroupID      int64  `json:"groupId"`
	Reason       string `json:"reason"`
}

type revokeUserResourceRequest struct {
	Reason string `json:"reason"`
}

type resourceShopItemsRequest struct {
	Items []resourceShopItemRequest `json:"items"`
}

type resourceShopItemRequest struct {
	ShopItemID      int64  `json:"shopItemId"`
	ResourceID      int64  `json:"resourceId"`
	Status          string `json:"status"`
	DurationDays    int32  `json:"durationDays"`
	CoinPrice       int64  `json:"coinPrice"`
	EffectiveFromMS int64  `json:"effectiveFromMs"`
	EffectiveToMS   int64  `json:"effectiveToMs"`
	SortOrder       int32  `json:"sortOrder"`
}

func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceRequest {
	resourceType := normalizeResourceType(r.ResourceType)
	resourceCode, name := resourceIdentity(r)
	walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
	priceType, coinPrice, giftPointAmount := resourcePricingForRequest(r)
	metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm, r.MetadataJSON, r.BadgeKind, r.LevelTrack, r.AvatarFrameKind, int64(r.VIPLevel), r.DurationDays)
	assetURL, previewURL, animationURL := resourceMaterialURLs(r)
	return &walletv1.CreateResourceRequest{
		RequestId:           middleware.CurrentRequestID(c),
		AppCode:             appctx.FromContext(c.Request.Context()),
		ResourceCode:        resourceCode,
		ResourceType:        resourceType,
		Name:                name,
		Status:              strings.TrimSpace(r.Status),
		Grantable:           true,
		ManagerGrantEnabled: managerGrantEnabledOrDefault(r.ManagerGrantEnabled),
		GrantStrategy:       resourceGrantStrategy(resourceType),
		WalletAssetType:     walletAssetType,
		WalletAssetAmount:   walletAssetAmount,
		PriceType:           priceType,
		CoinPrice:           coinPrice,
		GiftPointAmount:     giftPointAmount,
		UsageScopes:         []string{resourceType},
		AssetUrl:            assetURL,
		PreviewUrl:          previewURL,
		AnimationUrl:        animationURL,
		MetadataJson:        metadataJSON,
		SortOrder:           r.SortOrder,
		OperatorUserId:      actorID(c),
	}
}

func parseGrantTargetUserID(value any) (int64, error) {
	// Admin 前端会先把短号解析成内部 user_id；内部 user_id 已超过 JS 安全整数，
	// 所以新请求用字符串传递，避免浏览器把 318705991371722752 这类 ID 四舍五入。
	switch typed := value.(type) {
	case string:
		userID, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
		if err != nil || userID <= 0 {
			return 0, fmt.Errorf("target_user_id is invalid")
		}
		return userID, nil
	case json.Number:
		userID, err := typed.Int64()
		if err != nil || userID <= 0 {
			return 0, fmt.Errorf("target_user_id is invalid")
		}
		return userID, nil
	case float64:
		// 兼容旧 admin-platform 对短号或小整数 user_id 传 JSON number 的请求；
		// 长 ID 必须走字符串，否则到这里前精度已经不可恢复。
		userID := int64(typed)
		if float64(userID) != typed || userID <= 0 {
			return 0, fmt.Errorf("target_user_id is invalid")
		}
		return userID, nil
	case int64:
		if typed <= 0 {
			return 0, fmt.Errorf("target_user_id is invalid")
		}
		return typed, nil
	case int:
		if typed <= 0 {
			return 0, fmt.Errorf("target_user_id is invalid")
		}
		return int64(typed), nil
	default:
		return 0, fmt.Errorf("target_user_id is required")
	}
}

func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1.UpdateResourceRequest {
	resourceType := normalizeResourceType(r.ResourceType)
	resourceCode, name := resourceIdentity(r)
	walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)
	priceType, coinPrice, giftPointAmount := resourcePricingForRequest(r)
	metadataJSON := resourceMetadataJSON(resourceType, r.BadgeForm, r.MetadataJSON, r.BadgeKind, r.LevelTrack, r.AvatarFrameKind, int64(r.VIPLevel), r.DurationDays)
	assetURL, previewURL, animationURL := resourceMaterialURLs(r)
	return &walletv1.UpdateResourceRequest{
		RequestId:           middleware.CurrentRequestID(c),
		AppCode:             appctx.FromContext(c.Request.Context()),
		ResourceId:          resourceID,
		ResourceCode:        resourceCode,
		ResourceType:        resourceType,
		Name:                name,
		Status:              strings.TrimSpace(r.Status),
		Grantable:           true,
		ManagerGrantEnabled: managerGrantEnabledOrDefault(r.ManagerGrantEnabled),
		GrantStrategy:       resourceGrantStrategy(resourceType),
		WalletAssetType:     walletAssetType,
		WalletAssetAmount:   walletAssetAmount,
		PriceType:           priceType,
		CoinPrice:           coinPrice,
		GiftPointAmount:     giftPointAmount,
		UsageScopes:         []string{resourceType},
		AssetUrl:            assetURL,
		PreviewUrl:          previewURL,
		AnimationUrl:        animationURL,
		MetadataJson:        metadataJSON,
		SortOrder:           r.SortOrder,
		OperatorUserId:      actorID(c),
	}
}

func (r emojiPackRequest) createProto(c *gin.Context) (*walletv1.CreateResourceRequest, error) {
	name := strings.TrimSpace(r.Name)
	coverURL := strings.TrimSpace(r.CoverURL)
	animationURL := strings.TrimSpace(r.AnimationURL)
	if name == "" || coverURL == "" || animationURL == "" {
		return nil, fmt.Errorf("表情包参数不完整")
	}
	category := normalizeEmojiPackCategory(r.Category)
	if utf8.RuneCountInString(category) > 64 {
		return nil, fmt.Errorf("表情包分类不能超过 64 个字符")
	}
	metadataJSON, err := emojiPackMetadataJSON(r.RegionIDs, category, r.PricingType)
	if err != nil {
		return nil, err
	}
	return &walletv1.CreateResourceRequest{
		RequestId:           middleware.CurrentRequestID(c),
		AppCode:             appctx.FromContext(c.Request.Context()),
		ResourceCode:        fmt.Sprintf("emoji_pack_%d", time.Now().UTC().UnixNano()),
		ResourceType:        resourceTypeEmojiPack,
		Name:                name,
		Status:              "active",
		Grantable:           false,
		ManagerGrantEnabled: boolPointer(false),
		GrantStrategy:       "increase_quantity",
		UsageScopes:         []string{resourceTypeEmojiPack},
		AssetUrl:            coverURL,
		PreviewUrl:          coverURL,
		AnimationUrl:        animationURL,
		MetadataJson:        metadataJSON,
		SortOrder:           r.SortOrder,
		OperatorUserId:      actorID(c),
	}, nil
}

func (r resourceGroupRequest) createProto(c *gin.Context) *walletv1.CreateResourceGroupRequest {
	return &walletv1.CreateResourceGroupRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		GroupCode:      strings.TrimSpace(r.GroupCode),
		Name:           strings.TrimSpace(r.Name),
		Status:         strings.TrimSpace(r.Status),
		Description:    strings.TrimSpace(r.Description),
		SortOrder:      r.SortOrder,
		Items:          groupItemInputs(r.Items),
		OperatorUserId: actorID(c),
	}
}

func (r resourceGroupRequest) updateProto(c *gin.Context, groupID int64) *walletv1.UpdateResourceGroupRequest {
	return &walletv1.UpdateResourceGroupRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		GroupId:        groupID,
		GroupCode:      strings.TrimSpace(r.GroupCode),
		Name:           strings.TrimSpace(r.Name),
		Status:         strings.TrimSpace(r.Status),
		Description:    strings.TrimSpace(r.Description),
		SortOrder:      r.SortOrder,
		Items:          groupItemInputs(r.Items),
		OperatorUserId: actorID(c),
	}
}

func (r giftRequest) createProto(c *gin.Context) *walletv1.CreateGiftConfigRequest {
	return &walletv1.CreateGiftConfigRequest{
		RequestId:        middleware.CurrentRequestID(c),
		AppCode:          appctx.FromContext(c.Request.Context()),
		GiftId:           strings.TrimSpace(r.GiftID),
		ResourceId:       r.ResourceID,
		Status:           strings.TrimSpace(r.Status),
		Name:             strings.TrimSpace(r.Name),
		SortOrder:        r.SortOrder,
		PresentationJson: strings.TrimSpace(r.PresentationJSON),
		PriceVersion:     strings.TrimSpace(r.PriceVersion),
		GiftTypeCode:     strings.TrimSpace(r.GiftTypeCode),
		VipLevel:         r.VIPLevel,
		CpRelationType:   r.cpRelationType(),
		ChargeAssetType:  strings.TrimSpace(r.ChargeAssetType),
		CoinPrice:        r.CoinPrice,
		GiftPointAmount:  0,
		HeatValue:        r.CoinPrice,
		EffectiveAtMs:    r.EffectiveAtMS,
		EffectiveFromMs:  r.EffectiveFromMS,
		EffectiveToMs:    r.EffectiveToMS,
		EffectTypes:      r.EffectTypes,
		OperatorUserId:   actorID(c),
		RegionIds:        r.RegionIDs,
	}
}

func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.UpdateGiftConfigRequest {
	return &walletv1.UpdateGiftConfigRequest{
		RequestId:        middleware.CurrentRequestID(c),
		AppCode:          appctx.FromContext(c.Request.Context()),
		GiftId:           strings.TrimSpace(giftID),
		ResourceId:       r.ResourceID,
		Status:           strings.TrimSpace(r.Status),
		Name:             strings.TrimSpace(r.Name),
		SortOrder:        r.SortOrder,
		PresentationJson: strings.TrimSpace(r.PresentationJSON),
		PriceVersion:     strings.TrimSpace(r.PriceVersion),
		GiftTypeCode:     strings.TrimSpace(r.GiftTypeCode),
		VipLevel:         r.VIPLevel,
		CpRelationType:   r.cpRelationType(),
		ChargeAssetType:  strings.TrimSpace(r.ChargeAssetType),
		CoinPrice:        r.CoinPrice,
		GiftPointAmount:  0,
		HeatValue:        r.CoinPrice,
		EffectiveAtMs:    r.EffectiveAtMS,
		EffectiveFromMs:  r.EffectiveFromMS,
		EffectiveToMs:    r.EffectiveToMS,
		EffectTypes:      r.EffectTypes,
		OperatorUserId:   actorID(c),
		RegionIds:        r.RegionIDs,
	}
}

func (r giftRequest) cpRelationType() string {
	if strings.TrimSpace(r.GiftTypeCode) != "cp" {
		return ""
	}
	// CP 关系类型最终由 wallet-service 写回 presentation_json；兼容旧前端只提交 JSON 的请求，避免保存时把兄弟/姐妹关系清掉。
	if value := strings.TrimSpace(r.CPRelationType); value != "" {
		return value
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(strings.TrimSpace(r.PresentationJSON)), &payload); err != nil {
		return ""
	}
	if value, ok := payload["cp_relation_type"].(string); ok {
		return strings.TrimSpace(value)
	}
	if value, ok := payload["cpRelationType"].(string); ok {
		return strings.TrimSpace(value)
	}
	return ""
}

func (r giftTypeRequest) upsertProto(c *gin.Context, typeCode string) *walletv1.UpsertGiftTypeConfigRequest {
	return &walletv1.UpsertGiftTypeConfigRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		TypeCode:       strings.TrimSpace(typeCode),
		Name:           strings.TrimSpace(r.DisplayName),
		TabKey:         strings.TrimSpace(r.TabName),
		Status:         strings.TrimSpace(r.Status),
		SortOrder:      r.SortOrder,
		OperatorUserId: actorID(c),
	}
}

func (r giftTypeItemRequest) upsertProto(c *gin.Context) *walletv1.UpsertGiftTypeConfigRequest {
	return giftTypeRequest{
		DisplayName: r.DisplayName,
		TabName:     r.TabName,
		Status:      r.Status,
		SortOrder:   r.SortOrder,
	}.upsertProto(c, r.TabKey)
}

func (r resourceShopItemsRequest) upsertProto(c *gin.Context) *walletv1.UpsertResourceShopItemsRequest {
	return &walletv1.UpsertResourceShopItemsRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		Items:          resourceShopItemInputs(r.Items),
		OperatorUserId: actorID(c),
	}
}

func managerGrantEnabledOrDefault(value *bool) *bool {
	enabled := false
	if value != nil {
		enabled = *value
	}
	return &enabled
}

func groupItemInputs(items []resourceGroupItemRequest) []*walletv1.ResourceGroupItemInput {
	out := make([]*walletv1.ResourceGroupItemInput, 0, len(items))
	for _, item := range items {
		itemType := strings.ToLower(strings.TrimSpace(item.ItemType))
		if itemType == "wallet_asset" {
			out = append(out, &walletv1.ResourceGroupItemInput{
				ItemType:          itemType,
				WalletAssetType:   strings.TrimSpace(item.AssetType),
				WalletAssetAmount: item.Amount,
				Quantity:          1,
				SortOrder:         item.SortOrder,
			})
			continue
		}
		out = append(out, &walletv1.ResourceGroupItemInput{
			ItemType:   "resource",
			ResourceId: item.ResourceID,
			Quantity:   1,
			DurationMs: item.DurationDays * dayMillis,
			SortOrder:  item.SortOrder,
		})
	}
	return out
}

func resourceShopItemInputs(items []resourceShopItemRequest) []*walletv1.ResourceShopItemInput {
	out := make([]*walletv1.ResourceShopItemInput, 0, len(items))
	for _, item := range items {
		out = append(out, &walletv1.ResourceShopItemInput{
			ShopItemId:      item.ShopItemID,
			ResourceId:      item.ResourceID,
			Status:          strings.TrimSpace(item.Status),
			DurationDays:    item.DurationDays,
			CoinPrice:       item.CoinPrice,
			EffectiveFromMs: item.EffectiveFromMS,
			EffectiveToMs:   item.EffectiveToMS,
			SortOrder:       item.SortOrder,
		})
	}
	return out
}

func normalizeResourceType(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func resourceWalletAsset(resourceType string, amount int64) (string, int64) {
	if resourceType == resourceTypeCoin {
		return "COIN", amount
	}
	return "", 0
}

func resourcePricing(priceType string, coinPrice int64) (string, int64, int64) {
	priceType = strings.ToLower(strings.TrimSpace(priceType))
	if priceType == resourcePriceTypeFree {
		return resourcePriceTypeFree, 0, 0
	}
	return resourcePriceTypeCoin, coinPrice, 0
}

func resourcePricingForRequest(req resourceRequest) (string, int64, int64) {
	// VIP 体验卡不是商城商品；价格是资源目录的无关字段，统一落 free/0，避免后台
	// 隐藏价格控件后仍被旧表单默认值污染。
	if normalizeResourceType(req.ResourceType) == resourceTypeVIPTrialCard {
		return resourcePriceTypeFree, 0, 0
	}
	return resourcePricing(req.PriceType, req.CoinPrice)
}

func resourceIdentity(req resourceRequest) (string, string) {
	resourceCode := strings.TrimSpace(req.ResourceCode)
	name := strings.TrimSpace(req.Name)
	if normalizeResourceType(req.ResourceType) != resourceTypeVIPTrialCard {
		return resourceCode, name
	}
	// 新建体验卡只让运营选择等级和天数，稳定编码由后台生成；编辑历史标准资源时
	// 保留原编码，避免把 vip_trial_card_<level> 改名后破坏专用发卡接口的回查。
	if resourceCode == "" {
		resourceCode = fmt.Sprintf("vip_trial_card_%d_%dd", req.VIPLevel, req.DurationDays)
	}
	if name == "" {
		name = fmt.Sprintf("VIP%d体验卡（%d天）", req.VIPLevel, req.DurationDays)
	}
	return resourceCode, name
}

func resourceMaterialURLs(req resourceRequest) (string, string, string) {
	if normalizeResourceType(req.ResourceType) == resourceTypeVIPTrialCard {
		return "", "", ""
	}
	return strings.TrimSpace(req.AssetURL), strings.TrimSpace(req.PreviewURL), strings.TrimSpace(req.AnimationURL)
}

func validateResourcePricing(req resourceRequest) error {
	if normalizeResourceType(req.ResourceType) == resourceTypeVIPTrialCard {
		return validateVIPTrialCardRequest(req)
	}
	priceType := strings.ToLower(strings.TrimSpace(req.PriceType))
	switch priceType {
	case resourcePriceTypeCoin:
		if req.CoinPrice <= 0 {
			return fmt.Errorf("请输入资源价格")
		}
	case resourcePriceTypeFree:
		return nil
	default:
		return fmt.Errorf("请选择资源价格类型")
	}
	return nil
}

func validateVIPTrialCardRequest(req resourceRequest) error {
	if req.VIPLevel <= 0 {
		return fmt.Errorf("请选择VIP等级")
	}
	if req.DurationDays <= 0 || req.DurationDays > 36500 {
		return fmt.Errorf("VIP体验卡有效天数必须在 1 到 36500 天之间")
	}
	return nil
}

func validateResourceBadgeForm(req resourceRequest) error {
	if normalizeResourceType(req.ResourceType) != resourceTypeBadge {
		return nil
	}
	if normalizeBadgeForm(req.BadgeForm) == "" {
		return fmt.Errorf("请选择徽章属性")
	}
	badgeKind := normalizeBadgeKind(req.BadgeKind)
	if badgeKind == "" {
		return fmt.Errorf("请选择徽章类型")
	}
	if badgeKind == badgeKindLevel && !validLevelTrack(req.LevelTrack) {
		return fmt.Errorf("请选择等级徽章所属等级")
	}
	return nil
}

func validateResourceAvatarFrameKind(req resourceRequest) error {
	if normalizeResourceType(req.ResourceType) != resourceTypeAvatarFrame {
		return nil
	}
	avatarFrameKind := normalizeAvatarFrameKind(req.AvatarFrameKind)
	if avatarFrameKind == "" {
		return fmt.Errorf("请选择头像框类型")
	}
	if avatarFrameKind == avatarFrameKindLevel && !validLevelTrack(req.LevelTrack) {
		return fmt.Errorf("请选择等级头像框所属等级")
	}
	return nil
}

func validateResourceMetadata(req resourceRequest) error {
	resourceType := normalizeResourceType(req.ResourceType)
	payload, fields, err := parseResourceMetadataPayload(req.MetadataJSON)
	if err != nil {
		return fmt.Errorf("资源元数据格式不正确")
	}
	if payload == nil {
		if resourceType == resourceTypeRoomName {
			return fmt.Errorf("请配置房间名称渐变样式")
		}
		return nil
	}
	if resourceType == resourceTypeRoomName {
		if err := validateRoomNameStyleMetadata(payload); err != nil {
			return err
		}
	}
	if resourceRequiresExplicitVisualFormat(resourceType) {
		format := strings.ToLower(strings.TrimSpace(payload.Format))
		if _, allowed := vipVisualResourceFormats[format]; !allowed {
			return fmt.Errorf("视觉资源素材格式不支持")
		}
		if animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat)); animationFormat != "" && animationFormat != format {
			return fmt.Errorf("视觉资源素材格式与动效格式不一致")
		}
	}
	if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 {
		animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat))
		if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 {
			return fmt.Errorf("资源动效格式不支持")
		}
	}
	if _, ok := fields["mp4_alpha_layout"]; !ok {
		return nil
	}
	if !resourceAllowsAnimationMetadata(resourceType) {
		return fmt.Errorf("当前资源类型不支持 MP4 透明布局")
	}
	layout, err := sanitizeMP4AlphaLayoutMetadata(payload.MP4AlphaLayout)
	if err != nil {
		return err
	}
	// MP4 透明布局是客户端合成协议，后台只能保存人工确认后的显式矩形，
	// 否则 Flutter 会重新落回不稳定的自动识别路径，黑场或弱 mask 素材仍会展示错误。
	if !layout.Confirmed {
		return fmt.Errorf("请确认 MP4 透明布局")
	}
	return nil
}

func resourceGrantStrategy(resourceType string) string {
	switch resourceType {
	case resourceTypeCoin:
		return "wallet_credit"
	case resourceTypeAvatarFrame, "profile_card", "vehicle", "chat_bubble":
		return "extend_expiry"
	case "badge":
		return "set_active_flag"
	case resourceTypeVIPTrialCard:
		return "new_entitlement"
	default:
		return "increase_quantity"
	}
}

func validateRoomDecorationResourceMaterial(req resourceRequest) error {
	resourceType := normalizeResourceType(req.ResourceType)
	if !resourceRequiresExplicitVisualFormat(resourceType) {
		return nil
	}
	// 麦位动效和房间边框都是实际视觉资源，不能创建只有编码的占位行；
	// 样式型 room_name_style 则由 metadata 独立渲染。
	if strings.TrimSpace(req.AssetURL) == "" && strings.TrimSpace(req.AnimationURL) == "" {
		return fmt.Errorf("请上传视觉资源素材")
	}
	if strings.TrimSpace(req.PreviewURL) == "" {
		return fmt.Errorf("请上传视觉资源预览图")
	}
	payload, _, err := parseResourceMetadataPayload(req.MetadataJSON)
	if err != nil || payload == nil {
		return fmt.Errorf("请配置视觉资源素材格式")
	}
	format := strings.ToLower(strings.TrimSpace(payload.Format))
	if _, allowed := vipVisualResourceFormats[format]; !allowed {
		return fmt.Errorf("视觉资源素材格式不支持")
	}
	if resourceType == resourceTypeGiftTraySkin && (format != "svga" || strings.TrimSpace(req.AnimationURL) == "") {
		return fmt.Errorf("送礼托盘必须上传 SVGA 动效")
	}
	return nil
}

func resourceRequiresExplicitVisualFormat(resourceType string) bool {
	switch normalizeResourceType(resourceType) {
	case resourceTypeGiftTraySkin, resourceTypeMicAnimation, resourceTypeRoomBorder:
		return true
	default:
		return false
	}
}

func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string, avatarFrameKind string, vipFields ...int64) string {
	if resourceType == resourceTypeBadge {
		return badgeMetadataJSON(badgeForm, badgeKind, levelTrack)
	}
	if resourceType == resourceTypeVIPTrialCard {
		var vipLevel int32
		var durationDays int64
		if len(vipFields) > 0 {
			vipLevel = int32(vipFields[0])
		}
		if len(vipFields) > 1 {
			durationDays = vipFields[1]
		}
		body, err := json.Marshal(resourceMetadataPayload{VIPLevel: vipLevel, DurationDays: durationDays})
		if err != nil {
			return "{}"
		}
		return string(body)
	}

	rawPayload, _, err := parseResourceMetadataPayload(metadataJSON)
	if err != nil {
		return "{}"
	}
	if rawPayload == nil {
		rawPayload = &resourceMetadataPayload{}
	}
	payload := resourceMetadataPayload{}
	if resourceType == resourceTypeProfileCard {
		payload.ProfileCardLayout = sanitizeProfileCardLayoutMetadata(rawPayload.ProfileCardLayout)
	}
	if resourceType == resourceTypeAvatarFrame {
		kind := normalizeAvatarFrameKind(avatarFrameKind)
		payload.AvatarFrameKind = kind
		if kind == avatarFrameKindLevel {
			// 等级头像框会被等级奖励任务按轨道过滤并发放；普通头像框不写 level_track，避免商店、经理赠送等入口误判。
			payload.LevelTrack = normalizeLevelTrack(levelTrack)
		}
	}
	if resourceType == resourceTypeRoomName || resourceRequiresExplicitVisualFormat(resourceType) {
		payload.Format = strings.ToLower(strings.TrimSpace(rawPayload.Format))
	}
	if resourceType == resourceTypeRoomName {
		payload.TextColors = append([]string(nil), rawPayload.TextColors...)
		payload.Stops = append([]float64(nil), rawPayload.Stops...)
		payload.AngleDegrees = rawPayload.AngleDegrees
	}
	if resourceAllowsAnimationMetadata(resourceType) {
		if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed {
			payload.AnimationFormat = resourceAnimationFormatMP4
			payload.MP4AlphaLayout = layout
		}
	}
	return marshalResourceMetadataPayload(payload)
}

func badgeMetadataJSON(badgeForm string, badgeKind string, levelTrack string) string {
	payload := badgeMetadataPayload{BadgeForm: normalizeBadgeForm(badgeForm), BadgeKind: normalizeBadgeKind(badgeKind)}
	switch payload.BadgeForm {
	case badgeFormStrip:
		payload.DefaultSlot = "profile_strip"
	case badgeFormTile:
		payload.DefaultSlot = "honor_wall"
	default:
		return "{}"
	}
	if payload.BadgeKind == badgeKindLevel {
		payload.LevelTrack = normalizeLevelTrack(levelTrack)
		if payload.LevelTrack == "" {
			return "{}"
		}
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return "{}"
	}
	return string(body)
}

func profileCardMetadataJSON(metadataJSON string) string {
	raw := strings.TrimSpace(metadataJSON)
	if raw == "" {
		return "{}"
	}
	payload := resourceMetadataPayload{}
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return "{}"
	}
	layout := sanitizeProfileCardLayoutMetadata(payload.ProfileCardLayout)
	if layout == nil {
		return "{}"
	}
	// 资料卡上传解析只允许写入布局字段；不要把后台临时调试数据透给 app。
	body, err := json.Marshal(profileCardMetadataPayload{ProfileCardLayout: layout})
	if err != nil {
		return "{}"
	}
	return string(body)
}

func parseResourceMetadataPayload(metadataJSON string) (*resourceMetadataPayload, map[string]json.RawMessage, error) {
	raw := strings.TrimSpace(metadataJSON)
	if raw == "" {
		return nil, nil, nil
	}
	fields := map[string]json.RawMessage{}
	if err := json.Unmarshal([]byte(raw), &fields); err != nil {
		return nil, nil, err
	}
	payload := resourceMetadataPayload{}
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return nil, nil, err
	}
	return &payload, fields, nil
}

func marshalResourceMetadataPayload(payload resourceMetadataPayload) string {
	if payload.AnimationFormat == "" && payload.AvatarFrameKind == "" && payload.Format == "" && payload.LevelTrack == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil && len(payload.TextColors) == 0 && len(payload.Stops) == 0 && payload.AngleDegrees == nil {
		return "{}"
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return "{}"
	}
	return string(body)
}

func sanitizeMP4AlphaLayoutMetadata(layout *mp4AlphaLayoutMetadataPayload) (*mp4AlphaLayoutMetadataPayload, error) {
	if layout == nil {
		return nil, fmt.Errorf("MP4 透明布局参数不完整")
	}
	videoWidth := layout.VideoWidth
	videoHeight := layout.VideoHeight
	if videoWidth <= 0 || videoHeight <= 0 {
		return nil, fmt.Errorf("MP4 视频宽高不正确")
	}
	alphaLayout := normalizeMP4AlphaLayout(layout.AlphaLayout)
	if alphaLayout == "" {
		return nil, fmt.Errorf("MP4 透明布局类型不支持")
	}
	detectVersion := layout.DetectVersion
	if detectVersion <= 0 {
		detectVersion = mp4AlphaLayoutDetectVersion
	}
	confidence := layout.Confidence
	if confidence < 0 {
		confidence = 0
	}
	if confidence > 1 {
		confidence = 1
	}

	if alphaLayout == mp4AlphaLayoutNormal {
		// normal 也是显式布局：RGB 使用完整视频帧，alpha_frame 固定为 null，
		// 这样 native 层能明确跳过 split-alpha 猜测，普通 MP4 不再被误判。
		return &mp4AlphaLayoutMetadataPayload{
			AlphaLayout:   mp4AlphaLayoutNormal,
			VideoWidth:    videoWidth,
			VideoHeight:   videoHeight,
			RGBFrame:      []int64{0, 0, videoWidth, videoHeight},
			AlphaFrame:    nil,
			Confirmed:     layout.Confirmed,
			Confidence:    confidence,
			DetectVersion: detectVersion,
		}, nil
	}
	if !validMP4Frame(layout.RGBFrame, videoWidth, videoHeight) || !validMP4Frame(layout.AlphaFrame, videoWidth, videoHeight) {
		return nil, fmt.Errorf("MP4 透明布局矩形不正确")
	}
	// 透明 MP4 的 RGB 与 alpha 矩形直接影响客户端 shader 取样，后端只做边界归一化，
	// 不把字段重算成左右或上下默认值，避免覆盖前端人工确认过的 custom 细节。
	return &mp4AlphaLayoutMetadataPayload{
		AlphaLayout:   alphaLayout,
		VideoWidth:    videoWidth,
		VideoHeight:   videoHeight,
		RGBFrame:      append([]int64(nil), layout.RGBFrame[:4]...),
		AlphaFrame:    append([]int64(nil), layout.AlphaFrame[:4]...),
		Confirmed:     layout.Confirmed,
		Confidence:    confidence,
		DetectVersion: detectVersion,
	}, nil
}

func validMP4Frame(frame []int64, videoWidth int64, videoHeight int64) bool {
	if len(frame) < 4 {
		return false
	}
	left, top, width, height := frame[0], frame[1], frame[2], frame[3]
	if left < 0 || top < 0 || width <= 0 || height <= 0 {
		return false
	}
	return left+width <= videoWidth && top+height <= videoHeight
}

func normalizeMP4AlphaLayout(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case mp4AlphaLayoutNormal:
		return mp4AlphaLayoutNormal
	case mp4AlphaLayoutAlphaLeftRGBRight:
		return mp4AlphaLayoutAlphaLeftRGBRight
	case mp4AlphaLayoutRGBLeftAlphaRight:
		return mp4AlphaLayoutRGBLeftAlphaRight
	case mp4AlphaLayoutAlphaTopRGBBottom:
		return mp4AlphaLayoutAlphaTopRGBBottom
	case mp4AlphaLayoutRGBTopAlphaBottom:
		return mp4AlphaLayoutRGBTopAlphaBottom
	case mp4AlphaLayoutCustom:
		return mp4AlphaLayoutCustom
	default:
		return ""
	}
}

func resourceAllowsAnimationMetadata(resourceType string) bool {
	switch resourceType {
	case "gift", resourceTypeGiftTraySkin, "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard, resourceTypeRoomBorder:
		return true
	default:
		return strings.HasPrefix(resourceType, "mic_seat_")
	}
}

func validateRoomNameStyleMetadata(payload *resourceMetadataPayload) error {
	if payload == nil || len(payload.TextColors) == 0 || len(payload.TextColors) > 8 {
		return fmt.Errorf("房名渐变颜色必须配置 1 至 8 个")
	}
	for index, color := range payload.TextColors {
		color = strings.TrimSpace(color)
		if !roomNameStyleColorPattern.MatchString(color) {
			return fmt.Errorf("房名渐变颜色必须使用 #RRGGBB 或 #RRGGBBAA")
		}
		payload.TextColors[index] = color
	}
	if len(payload.Stops) != 0 && len(payload.Stops) != len(payload.TextColors) {
		return fmt.Errorf("房名渐变位置数量必须与颜色一致")
	}
	previous := -1.0
	for _, stop := range payload.Stops {
		if math.IsNaN(stop) || math.IsInf(stop, 0) || stop < 0 || stop > 1 || stop < previous {
			return fmt.Errorf("房名渐变位置必须按 0 至 1 递增")
		}
		previous = stop
	}
	if payload.AngleDegrees != nil && (math.IsNaN(*payload.AngleDegrees) || math.IsInf(*payload.AngleDegrees, 0)) {
		return fmt.Errorf("房名渐变角度不正确")
	}
	return nil
}

func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload {
	if layout == nil {
		return nil
	}
	if layout.SourceWidth <= 0 || layout.SourceHeight <= 0 {
		return nil
	}
	if layout.ContentTop < 0 || layout.ContentBottom < layout.ContentTop || layout.ContentBottom >= layout.SourceHeight {
		return nil
	}
	contentHeight := layout.ContentBottom - layout.ContentTop + 1
	if layout.ContentHeight != contentHeight {
		return nil
	}
	colorContentWidth := layout.ColorContentWidth
	if colorContentWidth <= 0 || colorContentWidth > layout.SourceWidth {
		colorContentWidth = layout.SourceWidth
	}
	// 比例字段由整数边界重新计算，避免前端浮点误差或脏数据进入客户端展示协议。
	return &profileCardLayoutMetadataPayload{
		SourceWidth:        layout.SourceWidth,
		SourceHeight:       layout.SourceHeight,
		ColorContentWidth:  colorContentWidth,
		ContentTop:         layout.ContentTop,
		ContentBottom:      layout.ContentBottom,
		ContentHeight:      contentHeight,
		ContentTopRatio:    profileCardRatio(layout.ContentTop, layout.SourceHeight),
		ContentHeightRatio: profileCardRatio(contentHeight, layout.SourceHeight),
		DetectVersion:      1,
	}
}

func profileCardRatio(value int64, total int64) float64 {
	if total <= 0 {
		return 0
	}
	return float64(value) / float64(total)
}

func normalizeBadgeForm(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case badgeFormStrip, "long":
		return badgeFormStrip
	case badgeFormTile, "short":
		return badgeFormTile
	default:
		return ""
	}
}

func normalizeBadgeKind(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", badgeKindNormal:
		return badgeKindNormal
	case badgeKindLevel:
		return badgeKindLevel
	default:
		return ""
	}
}

func normalizeAvatarFrameKind(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", avatarFrameKindNormal:
		return avatarFrameKindNormal
	case avatarFrameKindLevel:
		return avatarFrameKindLevel
	default:
		return ""
	}
}

func normalizeLevelTrack(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if validLevelTrack(value) {
		return value
	}
	return ""
}

func validLevelTrack(value string) bool {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "wealth", "game", "charm":
		return true
	default:
		return false
	}
}

func badgeFormFromMetadata(metadataJSON string) string {
	payload := badgeMetadataPayload{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
		return ""
	}
	return normalizeBadgeForm(payload.BadgeForm)
}

func badgeKindFromMetadata(metadataJSON string) string {
	payload := badgeMetadataPayload{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
		return badgeKindNormal
	}
	return normalizeBadgeKind(payload.BadgeKind)
}

func badgeLevelTrackFromMetadata(metadataJSON string) string {
	payload := badgeMetadataPayload{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
		return ""
	}
	return normalizeLevelTrack(payload.LevelTrack)
}

func avatarFrameKindFromMetadata(metadataJSON string) string {
	payload := resourceMetadataPayload{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
		return avatarFrameKindNormal
	}
	kind := normalizeAvatarFrameKind(payload.AvatarFrameKind)
	if kind == "" {
		return avatarFrameKindNormal
	}
	return kind
}

func avatarFrameLevelTrackFromMetadata(metadataJSON string) string {
	payload := resourceMetadataPayload{}
	if err := json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &payload); err != nil {
		return ""
	}
	return normalizeLevelTrack(payload.LevelTrack)
}

func badgeFormForResource(resourceType string, metadataJSON string) string {
	if normalizeResourceType(resourceType) != resourceTypeBadge {
		return ""
	}
	return badgeFormFromMetadata(metadataJSON)
}

func badgeKindForResource(resourceType string, metadataJSON string) string {
	if normalizeResourceType(resourceType) != resourceTypeBadge {
		return ""
	}
	kind := badgeKindFromMetadata(metadataJSON)
	if kind == "" {
		return badgeKindNormal
	}
	return kind
}

func badgeLevelTrackForResource(resourceType string, metadataJSON string) string {
	if normalizeResourceType(resourceType) != resourceTypeBadge {
		return ""
	}
	return badgeLevelTrackFromMetadata(metadataJSON)
}

func avatarFrameKindForResource(resourceType string, metadataJSON string) string {
	if normalizeResourceType(resourceType) != resourceTypeAvatarFrame {
		return ""
	}
	return avatarFrameKindFromMetadata(metadataJSON)
}

func avatarFrameLevelTrackForResource(resourceType string, metadataJSON string) string {
	if normalizeResourceType(resourceType) != resourceTypeAvatarFrame {
		return ""
	}
	return avatarFrameLevelTrackFromMetadata(metadataJSON)
}

func levelTrackForResource(resourceType string, metadataJSON string) string {
	switch normalizeResourceType(resourceType) {
	case resourceTypeBadge:
		return badgeLevelTrackFromMetadata(metadataJSON)
	case resourceTypeAvatarFrame:
		return avatarFrameLevelTrackFromMetadata(metadataJSON)
	default:
		return ""
	}
}

func emojiPackMetadataJSON(regionIDs []int64, category string, pricingType string) (string, error) {
	normalizedRegionIDs := normalizeRegionIDs(regionIDs)
	metadata := emojiPackMetadataPayload{
		Category:    normalizeEmojiPackCategory(category),
		PricingType: normalizeEmojiPackPricingType(pricingType),
		RegionScope: "all",
	}
	if utf8.RuneCountInString(metadata.Category) > 64 {
		return "", fmt.Errorf("表情包分类不能超过 64 个字符")
	}
	if len(normalizedRegionIDs) > 0 {
		metadata.RegionScope = "limited"
		metadata.RegionIDs = normalizedRegionIDs
	}
	body, err := json.Marshal(metadata)
	if err != nil {
		return "", fmt.Errorf("表情包配置不正确")
	}
	return string(body), nil
}

func emojiPackMetadataFromJSON(metadataJSON string) emojiPackMetadataPayload {
	payload := emojiPackMetadataPayload{
		Category:    defaultEmojiPackCategory,
		PricingType: emojiPackPricingFree,
		RegionScope: "all",
	}
	if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil {
		return payload
	}
	payload.Category = normalizeEmojiPackCategory(payload.Category)
	payload.PricingType = normalizeEmojiPackPricingType(payload.PricingType)
	payload.RegionIDs = normalizeRegionIDs(payload.RegionIDs)
	if len(payload.RegionIDs) > 0 {
		payload.RegionScope = "limited"
	} else {
		payload.RegionScope = "all"
	}
	return payload
}

func normalizeEmojiPackCategory(value string) string {
	category := strings.TrimSpace(value)
	if category == "" {
		return defaultEmojiPackCategory
	}
	return category
}

func normalizeEmojiPackPricingType(value string) string {
	if strings.ToLower(strings.TrimSpace(value)) == emojiPackPricingPaid {
		return emojiPackPricingPaid
	}
	return emojiPackPricingFree
}

func normalizeRegionIDs(regionIDs []int64) []int64 {
	out := make([]int64, 0, len(regionIDs))
	seen := make(map[int64]struct{}, len(regionIDs))
	for _, regionID := range regionIDs {
		if regionID == 0 {
			return nil
		}
		if regionID < 0 {
			continue
		}
		if _, ok := seen[regionID]; ok {
			continue
		}
		seen[regionID] = struct{}{}
		out = append(out, regionID)
	}
	return out
}

func boolPointer(value bool) *bool {
	return &value
}
