package walletapi

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	activityv1 "hyapp.local/api/proto/activity/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/xerr"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

const (
	vipOnlineNoticeBenefitCode   = "online_global_notice"
	vipOnlineNoticeBroadcastType = "vip_online_notice"
	vipOnlineNoticeAvatarFrame   = "avatar_frame"
	vipOnlineNoticeCommandMaxLen = 128
	vipOnlineNoticeMessageMaxLen = 256
)

type vipOnlineNoticeRequestBody struct {
	// CommandID 是单次 App 进程启动的业务幂等键；request_id 只用于链路追踪，不能替代它。
	CommandID    string `json:"command_id"`
	CommandIDAlt string `json:"commandId"`
}

type vipOnlineNoticeMetadata struct {
	Message string `json:"message"`
}

type vipOnlineNoticeAvatarFrameData struct {
	ResourceID   int64
	Code         string
	AssetURL     string
	PreviewURL   string
	AnimationURL string
}

// getMyVIP 同时返回旧版扁平 VIP 字段和新的 paid/trial/effective 状态。
// 扁平字段继续取 GetMyVipResponse.vip，保证旧客户端升级期间无需同时切换解析路径。
func (h *Handler) getMyVIP(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.GetMyVip(request.Context(), &walletv1.GetMyVipRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	state := vipStateFromProto(resp.GetState())
	httpkit.WriteOK(writer, request, vipMeData{
		OverviewVIPData: vipFromProto(resp.GetVip()),
		ProgramConfig:   state.ProgramConfig,
		State:           state,
	})
}

// listVIPPackages 返回可购买 VIP 包和当前会员状态。
func (h *Handler) listVIPPackages(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.ListVipPackages(request.Context(), &walletv1.ListVipPackagesRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	packages := make([]vipPackageData, 0, len(resp.GetPackages()))
	for _, item := range resp.GetPackages() {
		packages = append(packages, vipPackageFromProto(item))
	}
	state := vipStateFromProto(resp.GetState())
	programConfig := vipProgramConfigFromProto(resp.GetProgramConfig())
	if resp.GetProgramConfig() == nil {
		programConfig = state.ProgramConfig
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"current_vip":    vipFromProto(resp.GetCurrentVip()),
		"packages":       packages,
		"program_config": programConfig,
		"state":          state,
	})
}

// purchaseVIP 购买、续期或升级 VIP。
func (h *Handler) purchaseVIP(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body vipPurchaseRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	if commandID == "" || body.Level <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.PurchaseVip(request.Context(), &walletv1.PurchaseVipRequest{
		CommandId: commandID,
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
		Level:     body.Level,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"order_id":           resp.GetOrderId(),
		"transaction_id":     resp.GetTransactionId(),
		"vip":                vipFromProto(resp.GetVip()),
		"coin_spent":         resp.GetCoinSpent(),
		"coin_balance_after": resp.GetCoinBalanceAfter(),
		"coin_balance":       balanceFromProto(resp.GetCoinBalance()),
		"reward_items":       vipRewardItemsFromProto(resp.GetRewardItems()),
		"program_config":     vipProgramConfigFromProto(resp.GetState().GetProgramConfig()),
		"state":              vipStateFromProto(resp.GetState()),
	})
}

// equipVIPTrialCard 只接受背包实例 entitlement_id；同资源的多张卡不能用 resource_id
// 代替，否则切换时会误改另一张卡的独立到期时间。
func (h *Handler) equipVIPTrialCard(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	entitlementID := strings.TrimSpace(request.PathValue("entitlement_id"))
	if entitlementID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.EquipVipTrialCard(request.Context(), &walletv1.EquipVipTrialCardRequest{
		RequestId:     httpkit.RequestIDFromContext(request.Context()),
		AppCode:       appcode.FromContext(request.Context()),
		UserId:        auth.UserIDFromContext(request.Context()),
		EntitlementId: entitlementID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	state := vipStateFromProto(resp.GetState())
	httpkit.WriteOK(writer, request, map[string]any{
		"trial_card":     vipTrialCardFromProto(resp.GetTrialCard()),
		"program_config": state.ProgramConfig,
		"state":          state,
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

// unequipVIPTrialCard 只解除当前装备关系；体验卡实例和其绝对过期时间仍保留在背包，
// 因而客户端可以随后重新佩戴尚未过期的任意等级体验卡。
func (h *Handler) unequipVIPTrialCard(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.UnequipVipTrialCard(request.Context(), &walletv1.UnequipVipTrialCardRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	state := vipStateFromProto(resp.GetState())
	httpkit.WriteOK(writer, request, map[string]any{
		"unequipped":     resp.GetUnequipped(),
		"program_config": state.ProgramConfig,
		"state":          state,
		"server_time_ms": resp.GetServerTimeMs(),
	})
}

// triggerVIPOnlineNotice 是 VIP 上线全服飘屏的唯一 App HTTP 入口。
// Flutter 负责进程内“UTC 当日只请求一次”的展示策略；服务端不做按日持久化去重，因而杀进程
// 后的新进程可以再次请求。单次请求重试仍通过 command_id 映射为稳定 event_id，避免网络重试重复播报。
func (h *Handler) triggerVIPOnlineNotice(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userProfileClient == nil || h.broadcastClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body vipOnlineNoticeRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	if commandID == "" || len(commandID) > vipOnlineNoticeCommandMaxLen {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	ctx := request.Context()
	userID := auth.UserIDFromContext(ctx)
	appCode := appcode.FromContext(ctx)
	benefitResp, err := h.walletClient.CheckVipBenefit(ctx, &walletv1.CheckVipBenefitRequest{
		RequestId:   httpkit.RequestIDFromContext(ctx),
		AppCode:     appCode,
		UserId:      userID,
		BenefitCode: vipOnlineNoticeBenefitCode,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	if !benefitResp.GetAllowed() {
		// 客户端展示态可能滞后，执行前必须重新查 effective benefit；体验卡排除、过期和后台停用
		// 都以 wallet-service 此刻的判定为准，不能仅比较 VIP9 等级。
		statusCode, code, message := httpkit.MapReasonToHTTP(xerr.VIPBenefitRequired)
		httpkit.WriteErrorData(writer, request, statusCode, code, message, map[string]any{
			"benefit_code":            vipOnlineNoticeBenefitCode,
			"required_level":          benefitResp.GetRequiredLevel(),
			"current_effective_level": benefitResp.GetCurrentEffectiveLevel(),
			"action":                  "send_online_global_notice",
		})
		return
	}
	equippedResp, err := h.walletClient.BatchGetUserEquippedResources(ctx, &walletv1.BatchGetUserEquippedResourcesRequest{
		RequestId:     httpkit.RequestIDFromContext(ctx),
		AppCode:       appCode,
		UserIds:       []int64{userID},
		ResourceTypes: []string{vipOnlineNoticeAvatarFrame},
	})
	if err != nil {
		// 头像框是全服消息的一部分，不能在 wallet 查询失败时把“无头像框”当成真实佩戴态广播。
		// 失败请求由客户端复用同一 command_id 重试，成功后仍只会写入同一条 activity outbox。
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	avatarFrame := vipOnlineNoticeAvatarFrameFromResponse(equippedResp, userID)
	profileResp, err := h.userProfileClient.GetUser(ctx, &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	profile := profileResp.GetUser()
	if profile == nil || profile.GetUserId() != userID {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}

	effectiveVIP := benefitResp.GetState().GetEffectiveVip()
	displayID := strings.TrimSpace(profile.GetPrettyDisplayUserId())
	if displayID == "" {
		displayID = strings.TrimSpace(profile.GetDisplayUserId())
	}
	// event_id 同时进入腾讯 IM payload 和 HTTP 成功响应；发送端用它把本地即时回显
	// 与稍后可能回环的 AVChatRoom 消息收敛成一次展示，不能让两条链路各自产生身份。
	commandDigest := sha256.Sum256([]byte(commandID))
	eventID := fmt.Sprintf("vip_online_notice:%d:%x", userID, commandDigest)
	notice := map[string]any{
		"event_id":       eventID,
		"broadcast_type": vipOnlineNoticeBroadcastType,
		"scope":          "global",
		"app_code":       appCode,
		"sent_at_ms":     benefitResp.GetEvaluatedAtMs(),
		// VIP 全服播报进入腾讯 IM，长用户 ID 必须在首次 JSON 编码时就是字符串。
		"user_id":                tencentim.FormatOptionalUserID(userID),
		"sender_user_id":         tencentim.FormatOptionalUserID(userID),
		"nickname":               profile.GetUsername(),
		"avatar":                 profile.GetAvatar(),
		"display_user_id":        displayID,
		"raw_display_user_id":    profile.GetDisplayUserId(),
		"pretty_display_user_id": profile.GetPrettyDisplayUserId(),
		// avatar_frame_url 是客户端高频兼容字段，固定等同资源 asset_url；其余字段保留完整素材信息，
		// 让静态、预览和动画渲染都只消费 wallet 返回的当前佩戴事实。
		"avatar_frame_url":           avatarFrame.AssetURL,
		"avatar_frame_resource_id":   avatarFrame.ResourceID,
		"avatar_frame_code":          avatarFrame.Code,
		"avatar_frame_asset_url":     avatarFrame.AssetURL,
		"avatar_frame_preview_url":   avatarFrame.PreviewURL,
		"avatar_frame_animation_url": avatarFrame.AnimationURL,
		"vip_program_type":           effectiveVIP.GetProgramType(),
		"vip_level":                  effectiveVIP.GetLevel(),
		"vip_name":                   effectiveVIP.GetName(),
		"message":                    vipOnlineNoticeMessage(benefitResp.GetBenefit().GetMetadataJson()),
		"action":                     map[string]string{"type": "none"},
	}
	payload, err := json.Marshal(notice)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
		return
	}
	broadcastResp, err := h.broadcastClient.PublishGlobalBroadcast(ctx, &activityv1.PublishGlobalBroadcastRequest{
		Meta:          httpkit.ActivityMeta(request),
		EventId:       eventID,
		BroadcastType: vipOnlineNoticeBroadcastType,
		PayloadJson:   string(payload),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"event_id":        broadcastResp.GetEventId(),
		"group_id":        broadcastResp.GetGroupId(),
		"status":          broadcastResp.GetStatus(),
		"created":         broadcastResp.GetCreated(),
		"evaluated_at_ms": benefitResp.GetEvaluatedAtMs(),
		// notice 是已经通过权益、用户资料和当前佩戴资源校验的可信快照；客户端只可
		// 在本机即时回显，其他用户仍必须消费 activity-service 投递的腾讯 IM 消息。
		"notice": notice,
	})
}

// vipOnlineNoticeAvatarFrameFromResponse 只接受目标用户下的 avatar_frame。
// wallet 的 batch RPC 已负责过滤过期佩戴，这里仍防御错误分组、错误资源类型和空资源，避免串用户素材。
func vipOnlineNoticeAvatarFrameFromResponse(resp *walletv1.BatchGetUserEquippedResourcesResponse, userID int64) vipOnlineNoticeAvatarFrameData {
	for _, user := range resp.GetUsers() {
		if user.GetUserId() != userID {
			continue
		}
		for _, item := range user.GetResources() {
			resource := item.GetResource()
			if resource.GetResourceType() != vipOnlineNoticeAvatarFrame || resource.GetResourceId() <= 0 {
				continue
			}
			return vipOnlineNoticeAvatarFrameData{
				ResourceID:   resource.GetResourceId(),
				Code:         resource.GetResourceCode(),
				AssetURL:     resource.GetAssetUrl(),
				PreviewURL:   resource.GetPreviewUrl(),
				AnimationURL: resource.GetAnimationUrl(),
			}
		}
	}
	return vipOnlineNoticeAvatarFrameData{}
}

// vipOnlineNoticeMessage 从每级权益 metadata_json 读取产品文案；缺失时使用中性兜底。
func vipOnlineNoticeMessage(metadataJSON string) string {
	var metadata vipOnlineNoticeMetadata
	if json.Unmarshal([]byte(strings.TrimSpace(metadataJSON)), &metadata) == nil {
		if message := strings.TrimSpace(metadata.Message); message != "" && len(message) <= vipOnlineNoticeMessageMaxLen {
			return message
		}
	}
	return "欢迎进入，祝你有美好的一天"
}

func vipFromProto(vip *walletv1.UserVip) overviewVIPData {
	if vip == nil {
		return overviewVIPData{}
	}
	return overviewVIPData{
		Level:         vip.GetLevel(),
		Name:          vip.GetName(),
		Active:        vip.GetActive(),
		StartedAtMS:   vip.GetStartedAtMs(),
		ExpiresAtMS:   vip.GetExpiresAtMs(),
		ProgramType:   vip.GetProgramType(),
		ConfigVersion: vip.GetConfigVersion(),
	}
}

func vipPackageFromProto(item *walletv1.VipLevel) vipPackageData {
	if item == nil {
		return vipPackageData{}
	}
	return vipPackageData{
		Level:                      item.GetLevel(),
		Name:                       item.GetName(),
		Status:                     item.GetStatus(),
		PriceCoin:                  item.GetPriceCoin(),
		DurationMS:                 item.GetDurationMs(),
		RewardResourceGroupID:      item.GetRewardResourceGroupId(),
		RewardItems:                vipRewardItemsFromProto(item.GetRewardItems()),
		CanPurchase:                item.GetCanPurchase(),
		SortOrder:                  item.GetSortOrder(),
		RechargeGateRequired:       item.GetRechargeGateRequired(),
		RequiredRechargeCoinAmount: item.GetRequiredRechargeCoinAmount(),
		UserRechargeCoinAmount:     item.GetUserRechargeCoinAmount(),
		PurchaseLockedReason:       item.GetPurchaseLockedReason(),
		Benefits:                   vipBenefitsFromProto(item.GetBenefits()),
		ConfigVersion:              item.GetConfigVersion(),
	}
}

func vipProgramConfigFromProto(config *walletv1.VipProgramConfig) vipProgramConfigData {
	if config == nil {
		return vipProgramConfigData{}
	}
	return vipProgramConfigData{
		AppCode:                  config.GetAppCode(),
		ProgramType:              config.GetProgramType(),
		LevelCount:               config.GetLevelCount(),
		SameLevelExpiryPolicy:    config.GetSameLevelExpiryPolicy(),
		UpgradeExpiryPolicy:      config.GetUpgradeExpiryPolicy(),
		DowngradePurchasePolicy:  config.GetDowngradePurchasePolicy(),
		BenefitInheritancePolicy: config.GetBenefitInheritancePolicy(),
		GrantMode:                config.GetGrantMode(),
		TrialCardEnabled:         config.GetTrialCardEnabled(),
		Status:                   config.GetStatus(),
		ConfigVersion:            config.GetConfigVersion(),
		UpdatedByAdminID:         config.GetUpdatedByAdminId(),
		CreatedAtMS:              config.GetCreatedAtMs(),
		UpdatedAtMS:              config.GetUpdatedAtMs(),
	}
}

func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitData {
	result := make([]vipBenefitData, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		result = append(result, vipBenefitData{
			BenefitCode:    item.GetBenefitCode(),
			Name:           item.GetName(),
			BenefitType:    item.GetBenefitType(),
			UnlockLevel:    item.GetUnlockLevel(),
			Status:         item.GetStatus(),
			TrialEnabled:   item.GetTrialEnabled(),
			ResourceID:     item.GetResourceId(),
			ResourceType:   item.GetResourceType(),
			ExecutionScope: item.GetExecutionScope(),
			AutoEquip:      item.GetAutoEquip(),
			SortOrder:      item.GetSortOrder(),
			MetadataJSON:   item.GetMetadataJson(),
			CreatedAtMS:    item.GetCreatedAtMs(),
			UpdatedAtMS:    item.GetUpdatedAtMs(),
			Presentation:   vipBenefitPresentationFromProto(item.GetPresentation()),
			Resource:       resourcePointerFromProto(item.GetResource()),
		})
	}
	return result
}

func vipBenefitPresentationFromProto(item *walletv1.VipBenefitPresentation) *vipBenefitPresentationData {
	if item == nil {
		return nil
	}
	result := &vipBenefitPresentationData{
		Description:  item.GetDescription(),
		PreviewItems: make([]vipBenefitPreviewItemData, 0, len(item.GetPreviewItems())),
	}
	for _, preview := range item.GetPreviewItems() {
		if preview == nil {
			continue
		}
		result.PreviewItems = append(result.PreviewItems, vipBenefitPreviewItemData{
			PreviewID: preview.GetPreviewId(), Title: preview.GetTitle(), MediaType: preview.GetMediaType(),
			AssetURL: preview.GetAssetUrl(), PreviewURL: preview.GetPreviewUrl(), AnimationURL: preview.GetAnimationUrl(),
			CompositionType: preview.GetCompositionType(), SortOrder: preview.GetSortOrder(),
		})
	}
	if reward := item.GetNumericReward(); reward != nil {
		result.NumericReward = &vipBenefitNumericRewardData{
			Label: reward.GetLabel(), Value: reward.GetValue(), Unit: reward.GetUnit(), Period: reward.GetPeriod(),
		}
	}
	return result
}

func vipTrialCardFromProto(card *walletv1.VipTrialCard) *vipTrialCardData {
	if card == nil {
		return nil
	}
	return &vipTrialCardData{
		TrialCardID:         card.GetTrialCardId(),
		EntitlementID:       card.GetEntitlementId(),
		ResourceID:          card.GetResourceId(),
		UserID:              card.GetUserId(),
		Level:               card.GetLevel(),
		Name:                card.GetName(),
		Status:              card.GetStatus(),
		Equipped:            card.GetEquipped(),
		DurationMS:          card.GetDurationMs(),
		EffectiveAtMS:       card.GetEffectiveAtMs(),
		ExpiresAtMS:         card.GetExpiresAtMs(),
		RemainingDurationMS: card.GetRemainingDurationMs(),
		GrantSource:         card.GetGrantSource(),
		SourceGrantID:       card.GetSourceGrantId(),
		CreatedAtMS:         card.GetCreatedAtMs(),
		UpdatedAtMS:         card.GetUpdatedAtMs(),
	}
}

func vipStateFromProto(state *walletv1.VipState) vipStateData {
	if state == nil {
		return vipStateData{
			EffectiveBenefits: []vipBenefitData{},
			UserSettings: vipUserSettingsData{
				RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
				HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
				LeaderboardInvisibleEnabled: false,
			},
		}
	}
	return vipStateData{
		PaidVIP:           vipFromProto(state.GetPaidVip()),
		EquippedTrialCard: vipTrialCardFromProto(state.GetEquippedTrialCard()),
		EffectiveVIP:      vipFromProto(state.GetEffectiveVip()),
		EffectiveSource:   state.GetEffectiveSource(),
		EffectiveBenefits: vipBenefitsFromProto(state.GetEffectiveBenefits()),
		EvaluatedAtMS:     state.GetEvaluatedAtMs(),
		ProgramConfig:     vipProgramConfigFromProto(state.GetProgramConfig()),
		UserSettings:      vipUserSettingsFromProto(state.GetUserSettings()),
	}
}

func vipUserSettingsFromProto(settings *walletv1.VipUserSettings) vipUserSettingsData {
	if settings == nil {
		// wallet 滚动升级期间旧实例没有 tag 8；这里必须与 wallet owner 的
		// 缺行语义一致，否则同一用户会因命中不同 gateway 实例而看到不同的隐私默认。
		return vipUserSettingsData{
			RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
			HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
			LeaderboardInvisibleEnabled: false,
		}
	}
	return vipUserSettingsData{
		AppCode: settings.GetAppCode(), UserID: settings.GetUserId(),
		RoomEntryNoticeEnabled:       settings.GetRoomEntryNoticeEnabled(),
		OnlineGlobalNoticeEnabled:    settings.GetOnlineGlobalNoticeEnabled(),
		HideProfileDataEnabled:       settings.GetHideProfileDataEnabled(),
		AnonymousProfileVisitEnabled: settings.GetAnonymousProfileVisitEnabled(),
		LeaderboardInvisibleEnabled:  settings.GetLeaderboardInvisibleEnabled(),
		UpdatedAtMS:                  settings.GetUpdatedAtMs(),
	}
}

func vipRewardItemsFromProto(items []*walletv1.VipRewardItem) []vipRewardItemData {
	resp := make([]vipRewardItemData, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		resp = append(resp, vipRewardItemData{
			ResourceID:   item.GetResourceId(),
			ResourceCode: item.GetResourceCode(),
			ResourceType: item.GetResourceType(),
			Name:         item.GetName(),
			Quantity:     item.GetQuantity(),
			ExpiresAtMS:  item.GetExpiresAtMs(),
			CoverURL:     vipRewardCoverURL(item),
			AssetURL:     item.GetAssetUrl(),
			PreviewURL:   item.GetPreviewUrl(),
			AnimationURL: item.GetAnimationUrl(),
		})
	}
	return resp
}

func vipRewardCoverURL(item *walletv1.VipRewardItem) string {
	if item == nil {
		return ""
	}
	if strings.TrimSpace(item.GetPreviewUrl()) != "" {
		return item.GetPreviewUrl()
	}
	return item.GetAssetUrl()
}
