package resourceapi

import (
	"encoding/json"
	"errors"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"io"
	"net/http"
	"strconv"
	"strings"

	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/services/gateway-service/internal/appconfig"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/resourceview"
)

const resourceTypeEmojiPack = "emoji_pack"

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

type resourceData struct {
	ResourceID        int64    `json:"resource_id"`
	ResourceCode      string   `json:"resource_code"`
	ResourceType      string   `json:"resource_type"`
	Name              string   `json:"name"`
	Status            string   `json:"status"`
	Grantable         bool     `json:"grantable"`
	GrantStrategy     string   `json:"grant_strategy"`
	WalletAssetType   string   `json:"wallet_asset_type,omitempty"`
	WalletAssetAmount int64    `json:"wallet_asset_amount,omitempty"`
	PriceType         string   `json:"price_type,omitempty"`
	CoinPrice         int64    `json:"coin_price,omitempty"`
	GiftPointAmount   int64    `json:"gift_point_amount,omitempty"`
	UsageScopes       []string `json:"usage_scopes"`
	AssetURL          string   `json:"asset_url"`
	PreviewURL        string   `json:"preview_url"`
	AnimationURL      string   `json:"animation_url"`
	Format            string   `json:"format,omitempty"`
	MetadataJSON      string   `json:"metadata_json"`
	SortOrder         int32    `json:"sort_order"`
	CreatedAtMS       int64    `json:"created_at_ms"`
	UpdatedAtMS       int64    `json:"updated_at_ms"`
}

type resourceGroupItemData struct {
	GroupItemID       int64         `json:"group_item_id"`
	ItemType          string        `json:"item_type"`
	ResourceID        int64         `json:"resource_id"`
	Resource          *resourceData `json:"resource,omitempty"`
	WalletAssetType   string        `json:"wallet_asset_type,omitempty"`
	WalletAssetAmount int64         `json:"wallet_asset_amount,omitempty"`
	Quantity          int64         `json:"quantity"`
	DurationMS        int64         `json:"duration_ms"`
	SortOrder         int32         `json:"sort_order"`
}

type resourceGroupData struct {
	GroupID     int64                   `json:"group_id"`
	GroupCode   string                  `json:"group_code"`
	Name        string                  `json:"name"`
	Status      string                  `json:"status"`
	Description string                  `json:"description"`
	SortOrder   int32                   `json:"sort_order"`
	Items       []resourceGroupItemData `json:"items"`
	CreatedAtMS int64                   `json:"created_at_ms"`
	UpdatedAtMS int64                   `json:"updated_at_ms"`
}

type resourceShopItemData struct {
	ShopItemID      int64        `json:"shop_item_id"`
	ResourceID      int64        `json:"resource_id"`
	Resource        resourceData `json:"resource"`
	Status          string       `json:"status"`
	DurationDays    int32        `json:"duration_days"`
	PriceType       string       `json:"price_type"`
	CoinPrice       int64        `json:"coin_price"`
	EffectiveFromMS int64        `json:"effective_from_ms"`
	EffectiveToMS   int64        `json:"effective_to_ms"`
	SortOrder       int32        `json:"sort_order"`
	UpdatedAtMS     int64        `json:"updated_at_ms"`
}

type resourceShopPurchaseRequestBody struct {
	CommandID    string `json:"command_id"`
	CommandIDAlt string `json:"commandId"`
}

type resourceEquipRequestBody struct {
	EntitlementID string `json:"entitlement_id"`
	CommandID     string `json:"command_id"`
	CommandIDAlt  string `json:"commandId"`
}

type resourceShopPurchaseData struct {
	OrderID         string                   `json:"order_id"`
	TransactionID   string                   `json:"transaction_id"`
	ResourceGrantID string                   `json:"resource_grant_id"`
	CoinSpent       int64                    `json:"coin_spent"`
	Balance         resourceAssetBalanceData `json:"balance"`
	ShopItem        resourceShopItemData     `json:"shop_item"`
	Resource        userResourceData         `json:"resource"`
}

type resourceAssetBalanceData struct {
	AssetType       string `json:"asset_type"`
	AvailableAmount int64  `json:"available_amount"`
	FrozenAmount    int64  `json:"frozen_amount"`
	Version         int64  `json:"version"`
}

type giftConfigData struct {
	GiftID           string       `json:"gift_id"`
	ResourceID       int64        `json:"resource_id"`
	Resource         resourceData `json:"resource"`
	Status           string       `json:"status"`
	Name             string       `json:"name"`
	SortOrder        int32        `json:"sort_order"`
	PresentationJSON string       `json:"presentation_json"`
	PriceVersion     string       `json:"price_version"`
	GiftTypeCode     string       `json:"gift_type_code"`
	// VIPLevel 让客户端直接按结构化等级展示/过滤 VIP 礼物；非 VIP 礼物为 0。
	VIPLevel        int32    `json:"vip_level"`
	ChargeAssetType string   `json:"charge_asset_type"`
	CoinPrice       int64    `json:"coin_price"`
	GiftPointAmount int64    `json:"gift_point_amount"`
	HeatValue       int64    `json:"heat_value"`
	EffectiveFromMS int64    `json:"effective_from_ms"`
	EffectiveToMS   int64    `json:"effective_to_ms"`
	EffectTypes     []string `json:"effect_types"`
	CreatedAtMS     int64    `json:"created_at_ms"`
	UpdatedAtMS     int64    `json:"updated_at_ms"`
	RegionIDs       []int64  `json:"region_ids"`
}

type giftTabData struct {
	Key          string           `json:"key"`
	GiftTypeCode string           `json:"gift_type_code"`
	Name         string           `json:"name"`
	Label        string           `json:"label"`
	TabKey       string           `json:"tab_key"`
	Status       string           `json:"status"`
	Order        int32            `json:"order"`
	SortOrder    int32            `json:"sort_order"`
	CreatedAtMS  int64            `json:"created_at_ms"`
	UpdatedAtMS  int64            `json:"updated_at_ms"`
	Gifts        []giftConfigData `json:"gifts"`
}

type emojiPackData struct {
	ResourceID   int64   `json:"resource_id"`
	ResourceCode string  `json:"resource_code"`
	Name         string  `json:"name"`
	Category     string  `json:"category"`
	PricingType  string  `json:"pricing_type"`
	CoverURL     string  `json:"cover_url"`
	AnimationURL string  `json:"animation_url"`
	RegionIDs    []int64 `json:"region_ids"`
	IsGlobal     bool    `json:"is_global"`
	SortOrder    int32   `json:"sort_order"`
	UpdatedAtMS  int64   `json:"updated_at_ms"`
}

type emojiPackMetadata struct {
	Category    string  `json:"category"`
	PricingType string  `json:"pricing_type"`
	RegionIDs   []int64 `json:"region_ids"`
}

type userResourceData struct {
	EntitlementID     string       `json:"entitlement_id"`
	ResourceID        int64        `json:"resource_id"`
	Resource          resourceData `json:"resource"`
	Status            string       `json:"status"`
	Quantity          int64        `json:"quantity"`
	RemainingQuantity int64        `json:"remaining_quantity"`
	EffectiveAtMS     int64        `json:"effective_at_ms"`
	ExpiresAtMS       int64        `json:"expires_at_ms"`
	SourceGrantID     string       `json:"source_grant_id"`
	CreatedAtMS       int64        `json:"created_at_ms"`
	UpdatedAtMS       int64        `json:"updated_at_ms"`
	Equipped          bool         `json:"equipped"`
}

func (h *Handler) listResources(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, pageSize, ok := resourcePage(request)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
		Keyword:      strings.TrimSpace(request.URL.Query().Get("keyword")),
		Page:         page,
		PageSize:     pageSize,
		ActiveOnly:   true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]resourceData, 0, len(resp.GetResources()))
	for _, item := range resp.GetResources() {
		items = append(items, resourceFromProto(item))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}

func (h *Handler) getResourceGroup(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	groupID, ok := httpkit.PositiveInt64PathValue(request, "group_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetResourceGroup(request.Context(), &walletv1.GetResourceGroupRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		GroupId:   groupID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	group := resourceGroupFromProto(resp.GetGroup())
	if group.Status != "active" {
		httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
		return
	}
	httpkit.WriteOK(writer, request, group)
}

func (h *Handler) listResourceShopItems(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, pageSize, ok := resourcePage(request)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListResourceShopItems(request.Context(), &walletv1.ListResourceShopItemsRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
		Keyword:      strings.TrimSpace(request.URL.Query().Get("keyword")),
		Page:         page,
		PageSize:     pageSize,
		ActiveOnly:   true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]resourceShopItemData, 0, len(resp.GetItems()))
	for _, item := range resp.GetItems() {
		items = append(items, resourceShopItemFromProto(item))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}

func (h *Handler) purchaseResourceShopItem(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	shopItemID, ok := httpkit.PositiveInt64PathValue(request, "shop_item_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	var body resourceShopPurchaseRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	if commandID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.PurchaseResourceShopItem(request.Context(), &walletv1.PurchaseResourceShopItemRequest{
		CommandId:  commandID,
		AppCode:    appcode.FromContext(request.Context()),
		UserId:     auth.UserIDFromContext(request.Context()),
		ShopItemId: shopItemID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, resourceShopPurchaseFromProto(resp))
}

func (h *Handler) listGifts(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, pageSize, ok := resourcePage(request)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	regionID, _, ok := optionalNonNegativeInt64Query(request, "region_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		Keyword:      strings.TrimSpace(request.URL.Query().Get("keyword")),
		Page:         page,
		PageSize:     pageSize,
		ActiveOnly:   true,
		RegionId:     regionID,
		FilterRegion: true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]giftConfigData, 0, len(resp.GetGifts()))
	for _, item := range resp.GetGifts() {
		items = append(items, giftFromProto(item))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}

func (h *Handler) listGiftTabs(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	regionID, filterRegion, ok := optionalNonNegativeInt64Query(request, "region_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListGiftTypeConfigs(request.Context(), &walletv1.ListGiftTypeConfigsRequest{
		RequestId:  httpkit.RequestIDFromContext(request.Context()),
		AppCode:    appcode.FromContext(request.Context()),
		ActiveOnly: true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	gifts, err := h.listAllActiveGifts(request, regionID, filterRegion)
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	giftsByType := make(map[string][]giftConfigData, len(gifts))
	for _, item := range gifts {
		giftsByType[item.GiftTypeCode] = append(giftsByType[item.GiftTypeCode], item)
	}
	items := make([]giftTabData, 0, len(resp.GetGiftTypes()))
	for _, item := range resp.GetGiftTypes() {
		tab := giftTabFromProto(item)
		tab.Gifts = giftsByType[tab.GiftTypeCode]
		if tab.Gifts == nil {
			tab.Gifts = []giftConfigData{}
		}
		items = append(items, tab)
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}

func (h *Handler) listAllActiveGifts(request *http.Request, regionID int64, filterRegion bool) ([]giftConfigData, error) {
	const pageSize int32 = 100
	items := make([]giftConfigData, 0, pageSize)
	for page := int32(1); ; page++ {
		resp, err := h.walletClient.ListGiftConfigs(request.Context(), &walletv1.ListGiftConfigsRequest{
			RequestId:    httpkit.RequestIDFromContext(request.Context()),
			AppCode:      appcode.FromContext(request.Context()),
			Page:         page,
			PageSize:     pageSize,
			ActiveOnly:   true,
			RegionId:     regionID,
			FilterRegion: filterRegion,
		})
		if err != nil {
			return nil, err
		}
		for _, gift := range resp.GetGifts() {
			items = append(items, giftFromProto(gift))
		}
		if len(resp.GetGifts()) == 0 || int64(len(items)) >= resp.GetTotal() {
			return items, nil
		}
	}
}

func (h *Handler) listEmojiPacks(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, pageSize, ok := resourcePage(request)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	regionID, filterRegion, ok := optionalNonNegativeInt64Query(request, "region_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListResources(request.Context(), &walletv1.ListResourcesRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		ResourceType: resourceTypeEmojiPack,
		Keyword:      strings.TrimSpace(request.URL.Query().Get("keyword")),
		Page:         1,
		PageSize:     1000,
		ActiveOnly:   true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	all := make([]emojiPackData, 0, len(resp.GetResources()))
	for _, item := range resp.GetResources() {
		pack := emojiPackFromProto(item)
		if filterRegion && !emojiPackMatchesRegion(pack, regionID) {
			continue
		}
		all = append(all, pack)
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": paginateEmojiPacks(all, page, pageSize), "total": int64(len(all)), "page": page, "page_size": pageSize})
}

func (h *Handler) listMyResources(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resp, err := h.walletClient.ListUserResources(request.Context(), &walletv1.ListUserResourcesRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		UserId:       auth.UserIDFromContext(request.Context()),
		ResourceType: strings.TrimSpace(request.URL.Query().Get("resource_type")),
		ActiveOnly:   true,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]userResourceData, 0, len(resp.GetResources()))
	for _, item := range resp.GetResources() {
		items = append(items, userResourceFromProto(item))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}

func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodPost {
		httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resourceID, ok := httpkit.PositiveInt64PathValue(request, "resource_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	var body resourceEquipRequestBody
	if request.Body != nil && request.Body != http.NoBody {
		if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) {
			httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body")
			return
		}
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	appCode := appcode.FromContext(request.Context())
	if commandID == "" {
		policy := appconfig.ResourceEquipCommandIDPolicyLegacyOptional
		if h.runtimeCapabilities != nil {
			capabilities, err := h.runtimeCapabilities.GetRuntimeCapabilities(request.Context(), appCode)
			if err != nil {
				// 冷缓存读取失败时不生成降级键；这避免 required App 因后台库故障被静默放宽幂等约束。
				httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
				return
			}
			policy = capabilities.ResourceEquipCommandIDPolicy
		}
		if policy == appconfig.ResourceEquipCommandIDPolicyRequired {
			httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
			return
		}
		if policy != appconfig.ResourceEquipCommandIDPolicyLegacyOptional {
			httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		// 只有后台明确保持 legacy_optional 时才为旧客户端生成请求级兼容键；新客户端仍应传稳定 command_id。
		commandID = "legacy_equip:" + httpkit.RequestIDFromContext(request.Context())
	}
	resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{
		RequestId:     httpkit.RequestIDFromContext(request.Context()),
		CommandId:     commandID,
		AppCode:       appCode,
		UserId:        auth.UserIDFromContext(request.Context()),
		ResourceId:    resourceID,
		EntitlementId: strings.TrimSpace(body.EntitlementID),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, userResourceFromProto(resp.GetResource()))
}

func (h *Handler) unequipMyResource(writer http.ResponseWriter, request *http.Request) {
	if request.Method != http.MethodDelete {
		httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resourceType := strings.TrimSpace(request.PathValue("resource_type"))
	if resourceType == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.UnequipUserResource(request.Context(), &walletv1.UnequipUserResourceRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		UserId:       auth.UserIDFromContext(request.Context()),
		ResourceType: resourceType,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"resource_type": resp.GetResourceType(),
		"unequipped":    resp.GetUnequipped(),
		"updated_at_ms": resp.GetUpdatedAtMs(),
	})
}

func resourceFromProto(item *walletv1.Resource) resourceData {
	if item == nil {
		return resourceData{}
	}
	return resourceData{
		ResourceID:        item.GetResourceId(),
		ResourceCode:      item.GetResourceCode(),
		ResourceType:      item.GetResourceType(),
		Name:              item.GetName(),
		Status:            item.GetStatus(),
		Grantable:         item.GetGrantable(),
		GrantStrategy:     item.GetGrantStrategy(),
		WalletAssetType:   item.GetWalletAssetType(),
		WalletAssetAmount: item.GetWalletAssetAmount(),
		PriceType:         item.GetPriceType(),
		CoinPrice:         item.GetCoinPrice(),
		GiftPointAmount:   item.GetGiftPointAmount(),
		UsageScopes:       item.GetUsageScopes(),
		AssetURL:          item.GetAssetUrl(),
		PreviewURL:        item.GetPreviewUrl(),
		AnimationURL:      item.GetAnimationUrl(),
		Format:            resourceview.FormatFromMetadata(item.GetMetadataJson()),
		MetadataJSON:      item.GetMetadataJson(),
		SortOrder:         item.GetSortOrder(),
		CreatedAtMS:       item.GetCreatedAtMs(),
		UpdatedAtMS:       item.GetUpdatedAtMs(),
	}
}

func resourceGroupFromProto(group *walletv1.ResourceGroup) resourceGroupData {
	if group == nil {
		return resourceGroupData{}
	}
	items := make([]resourceGroupItemData, 0, len(group.GetItems()))
	for _, item := range group.GetItems() {
		items = append(items, resourceGroupItemData{
			GroupItemID:       item.GetGroupItemId(),
			ItemType:          item.GetItemType(),
			ResourceID:        item.GetResourceId(),
			Resource:          resourcePointerFromProto(item.GetResource()),
			WalletAssetType:   item.GetWalletAssetType(),
			WalletAssetAmount: item.GetWalletAssetAmount(),
			Quantity:          item.GetQuantity(),
			DurationMS:        item.GetDurationMs(),
			SortOrder:         item.GetSortOrder(),
		})
	}
	return resourceGroupData{
		GroupID:     group.GetGroupId(),
		GroupCode:   group.GetGroupCode(),
		Name:        group.GetName(),
		Status:      group.GetStatus(),
		Description: group.GetDescription(),
		SortOrder:   group.GetSortOrder(),
		Items:       items,
		CreatedAtMS: group.GetCreatedAtMs(),
		UpdatedAtMS: group.GetUpdatedAtMs(),
	}
}

func resourcePointerFromProto(item *walletv1.Resource) *resourceData {
	if item == nil || item.GetResourceId() == 0 {
		return nil
	}
	data := resourceFromProto(item)
	return &data
}

func resourceShopItemFromProto(item *walletv1.ResourceShopItem) resourceShopItemData {
	if item == nil {
		return resourceShopItemData{}
	}
	return resourceShopItemData{
		ShopItemID:      item.GetShopItemId(),
		ResourceID:      item.GetResourceId(),
		Resource:        resourceFromProto(item.GetResource()),
		Status:          item.GetStatus(),
		DurationDays:    item.GetDurationDays(),
		PriceType:       item.GetPriceType(),
		CoinPrice:       item.GetCoinPrice(),
		EffectiveFromMS: item.GetEffectiveFromMs(),
		EffectiveToMS:   item.GetEffectiveToMs(),
		SortOrder:       item.GetSortOrder(),
		UpdatedAtMS:     item.GetUpdatedAtMs(),
	}
}

func resourceShopPurchaseFromProto(resp *walletv1.PurchaseResourceShopItemResponse) resourceShopPurchaseData {
	if resp == nil {
		return resourceShopPurchaseData{}
	}
	return resourceShopPurchaseData{
		OrderID:         resp.GetOrderId(),
		TransactionID:   resp.GetTransactionId(),
		ResourceGrantID: resp.GetResourceGrantId(),
		CoinSpent:       resp.GetCoinSpent(),
		Balance:         resourceBalanceFromProto(resp.GetBalance()),
		ShopItem:        resourceShopItemFromProto(resp.GetShopItem()),
		Resource:        userResourceFromProto(resp.GetResource()),
	}
}

func resourceBalanceFromProto(balance *walletv1.AssetBalance) resourceAssetBalanceData {
	if balance == nil {
		return resourceAssetBalanceData{}
	}
	return resourceAssetBalanceData{
		AssetType:       balance.GetAssetType(),
		AvailableAmount: balance.GetAvailableAmount(),
		FrozenAmount:    balance.GetFrozenAmount(),
		Version:         balance.GetVersion(),
	}
}

func giftFromProto(gift *walletv1.GiftConfig) giftConfigData {
	if gift == nil {
		return giftConfigData{}
	}
	return giftConfigData{
		GiftID:           gift.GetGiftId(),
		ResourceID:       gift.GetResourceId(),
		Resource:         resourceFromProto(gift.GetResource()),
		Status:           gift.GetStatus(),
		Name:             gift.GetName(),
		SortOrder:        gift.GetSortOrder(),
		PresentationJSON: gift.GetPresentationJson(),
		PriceVersion:     gift.GetPriceVersion(),
		GiftTypeCode:     gift.GetGiftTypeCode(),
		VIPLevel:         gift.GetVipLevel(),
		ChargeAssetType:  gift.GetChargeAssetType(),
		CoinPrice:        gift.GetCoinPrice(),
		GiftPointAmount:  gift.GetGiftPointAmount(),
		HeatValue:        gift.GetHeatValue(),
		EffectiveFromMS:  gift.GetEffectiveFromMs(),
		EffectiveToMS:    gift.GetEffectiveToMs(),
		EffectTypes:      gift.GetEffectTypes(),
		CreatedAtMS:      gift.GetCreatedAtMs(),
		UpdatedAtMS:      gift.GetUpdatedAtMs(),
		RegionIDs:        gift.GetRegionIds(),
	}
}

func giftTabFromProto(item *walletv1.GiftTypeConfig) giftTabData {
	if item == nil {
		return giftTabData{}
	}
	return giftTabData{
		Key:          item.GetTypeCode(),
		GiftTypeCode: item.GetTypeCode(),
		Name:         item.GetName(),
		Label:        item.GetTabKey(),
		TabKey:       item.GetTabKey(),
		Status:       item.GetStatus(),
		Order:        item.GetSortOrder(),
		SortOrder:    item.GetSortOrder(),
		CreatedAtMS:  item.GetCreatedAtMs(),
		UpdatedAtMS:  item.GetUpdatedAtMs(),
		Gifts:        []giftConfigData{},
	}
}

func userResourceFromProto(item *walletv1.UserResourceEntitlement) userResourceData {
	if item == nil {
		return userResourceData{}
	}
	return userResourceData{
		EntitlementID:     item.GetEntitlementId(),
		ResourceID:        item.GetResourceId(),
		Resource:          resourceFromProto(item.GetResource()),
		Status:            item.GetStatus(),
		Quantity:          item.GetQuantity(),
		RemainingQuantity: item.GetRemainingQuantity(),
		EffectiveAtMS:     item.GetEffectiveAtMs(),
		ExpiresAtMS:       item.GetExpiresAtMs(),
		SourceGrantID:     item.GetSourceGrantId(),
		CreatedAtMS:       item.GetCreatedAtMs(),
		UpdatedAtMS:       item.GetUpdatedAtMs(),
		Equipped:          item.GetEquipped(),
	}
}

func emojiPackFromProto(item *walletv1.Resource) emojiPackData {
	if item == nil {
		return emojiPackData{}
	}
	metadata := emojiPackMetadataFromJSON(item.GetMetadataJson())
	return emojiPackData{
		ResourceID:   item.GetResourceId(),
		ResourceCode: item.GetResourceCode(),
		Name:         item.GetName(),
		Category:     metadata.Category,
		PricingType:  metadata.PricingType,
		CoverURL:     firstNonEmpty(item.GetPreviewUrl(), item.GetAssetUrl()),
		AnimationURL: item.GetAnimationUrl(),
		RegionIDs:    metadata.RegionIDs,
		IsGlobal:     len(metadata.RegionIDs) == 0,
		SortOrder:    item.GetSortOrder(),
		UpdatedAtMS:  item.GetUpdatedAtMs(),
	}
}

func emojiPackMetadataFromJSON(metadataJSON string) emojiPackMetadata {
	payload := emojiPackMetadata{
		Category:    defaultEmojiPackCategory,
		PricingType: emojiPackPricingFree,
	}
	if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil {
		return payload
	}
	payload.Category = normalizeEmojiPackCategory(payload.Category)
	payload.PricingType = normalizeEmojiPackPricingType(payload.PricingType)
	payload.RegionIDs = normalizeEmojiPackRegionIDs(payload.RegionIDs)
	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 normalizeEmojiPackRegionIDs(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 _, ok := seen[regionID]; ok {
			continue
		}
		seen[regionID] = struct{}{}
		out = append(out, regionID)
	}
	return out
}

func emojiPackMatchesRegion(pack emojiPackData, regionID int64) bool {
	if pack.IsGlobal || regionID == 0 {
		return true
	}
	for _, candidate := range pack.RegionIDs {
		if candidate == regionID {
			return true
		}
	}
	return false
}

func paginateEmojiPacks(items []emojiPackData, page int32, pageSize int32) []emojiPackData {
	start := int((page - 1) * pageSize)
	if start >= len(items) {
		return []emojiPackData{}
	}
	end := start + int(pageSize)
	if end > len(items) {
		end = len(items)
	}
	return items[start:end]
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func resourcePage(request *http.Request) (int32, int32, bool) {
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		return 0, 0, false
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		return 0, 0, false
	}
	if raw := strings.TrimSpace(request.URL.Query().Get("pageSize")); raw != "" {
		parsed, err := strconv.Atoi(raw)
		if err != nil || parsed <= 0 {
			return 0, 0, false
		}
		pageSize = int32(parsed)
	}
	return page, pageSize, true
}

func optionalNonNegativeInt64Query(request *http.Request, name string) (int64, bool, bool) {
	raw := strings.TrimSpace(request.URL.Query().Get(name))
	if raw == "" {
		return 0, false, true
	}
	value, err := strconv.ParseInt(raw, 10, 64)
	if err != nil || value < 0 {
		return 0, false, false
	}
	return value, true, true
}
