package vipconfig

import (
	"strconv"
	"strings"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/activityclient"
	"hyapp-admin-server/internal/integration/walletclient"
	"hyapp-admin-server/internal/middleware"
	"hyapp-admin-server/internal/modules/shared"
	"hyapp-admin-server/internal/response"
	walletv1 "hyapp.local/api/proto/wallet/v1"

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

type Handler struct {
	wallet   walletclient.Client
	activity activityclient.Client
	audit    shared.OperationLogger
}

func New(wallet walletclient.Client, audit shared.OperationLogger, activities ...activityclient.Client) *Handler {
	var activity activityclient.Client
	if len(activities) > 0 {
		activity = activities[0]
	}
	return &Handler{wallet: wallet, activity: activity, audit: audit}
}

type configRequest struct {
	Levels []vipLevelDTO `json:"levels"`
}

type programRequest struct {
	Config *vipProgramConfigDTO `json:"config"`
}

type grantVipRequest struct {
	CommandID    string `json:"commandId"`
	TargetUserID any    `json:"targetUserId"`
	Level        int32  `json:"level"`
	Reason       string `json:"reason"`
}

type grantVipTrialCardRequest struct {
	CommandID    string `json:"commandId"`
	TargetUserID any    `json:"targetUserId"`
	Level        int32  `json:"level"`
	DurationMS   int64  `json:"durationMs"`
	ResourceID   *int64 `json:"resourceId"`
	Reason       string `json:"reason"`
}

type configDTO struct {
	Levels        []vipLevelDTO       `json:"levels"`
	ProgramConfig vipProgramConfigDTO `json:"programConfig"`
	ServerTimeMS  int64               `json:"serverTimeMs"`
}

type programDTO struct {
	Config       vipProgramConfigDTO `json:"config"`
	Benefits     []vipBenefitDTO     `json:"benefits"`
	ServerTimeMS int64               `json:"serverTimeMs"`
}

type grantVipDTO struct {
	TransactionID string             `json:"transactionId"`
	VIP           userVipDTO         `json:"vip"`
	RewardItems   []vipRewardItemDTO `json:"rewardItems"`
	ServerTimeMS  int64              `json:"serverTimeMs"`
}

type grantVipTrialCardDTO struct {
	TrialCard    vipTrialCardDTO `json:"trialCard"`
	State        vipStateDTO     `json:"state"`
	ServerTimeMS int64           `json:"serverTimeMs"`
}

type userVipDTO struct {
	UserID        string `json:"userId"`
	Level         int32  `json:"level"`
	Name          string `json:"name"`
	Active        bool   `json:"active"`
	StartedAtMS   int64  `json:"startedAtMs"`
	ExpiresAtMS   int64  `json:"expiresAtMs"`
	UpdatedAtMS   int64  `json:"updatedAtMs"`
	ProgramType   string `json:"programType"`
	ConfigVersion int64  `json:"configVersion"`
}

type vipLevelDTO struct {
	Level                      int32           `json:"level"`
	Name                       string          `json:"name"`
	Status                     string          `json:"status"`
	PriceCoin                  int64           `json:"priceCoin"`
	DurationMS                 int64           `json:"durationMs"`
	RewardResourceGroupID      int64           `json:"rewardResourceGroupId"`
	SortOrder                  int32           `json:"sortOrder"`
	RechargeGateRequired       bool            `json:"rechargeGateRequired"`
	RequiredRechargeCoinAmount int64           `json:"requiredRechargeCoinAmount"`
	UserRechargeCoinAmount     int64           `json:"userRechargeCoinAmount"`
	PurchaseLockedReason       string          `json:"purchaseLockedReason"`
	CreatedAtMS                int64           `json:"createdAtMs"`
	UpdatedAtMS                int64           `json:"updatedAtMs"`
	Benefits                   []vipBenefitDTO `json:"benefits"`
	ConfigVersion              int64           `json:"configVersion"`
}

type vipProgramConfigDTO struct {
	AppCode                  string `json:"appCode"`
	ProgramType              string `json:"programType"`
	LevelCount               int32  `json:"levelCount"`
	SameLevelExpiryPolicy    string `json:"sameLevelExpiryPolicy"`
	UpgradeExpiryPolicy      string `json:"upgradeExpiryPolicy"`
	DowngradePurchasePolicy  string `json:"downgradePurchasePolicy"`
	BenefitInheritancePolicy string `json:"benefitInheritancePolicy"`
	GrantMode                string `json:"grantMode"`
	TrialCardEnabled         bool   `json:"trialCardEnabled"`
	Status                   string `json:"status"`
	ConfigVersion            int64  `json:"configVersion"`
	UpdatedByAdminID         int64  `json:"updatedByAdminId"`
	CreatedAtMS              int64  `json:"createdAtMs"`
	UpdatedAtMS              int64  `json:"updatedAtMs"`
}

type vipBenefitDTO struct {
	BenefitCode    string                     `json:"benefitCode"`
	Name           string                     `json:"name"`
	BenefitType    string                     `json:"benefitType"`
	UnlockLevel    int32                      `json:"unlockLevel"`
	Status         string                     `json:"status"`
	TrialEnabled   bool                       `json:"trialEnabled"`
	ResourceID     int64                      `json:"resourceId"`
	ResourceType   string                     `json:"resourceType"`
	ExecutionScope string                     `json:"executionScope"`
	AutoEquip      bool                       `json:"autoEquip"`
	SortOrder      int32                      `json:"sortOrder"`
	MetadataJSON   string                     `json:"metadataJson"`
	CreatedAtMS    int64                      `json:"createdAtMs"`
	UpdatedAtMS    int64                      `json:"updatedAtMs"`
	Presentation   *vipBenefitPresentationDTO `json:"presentation"`
}

type vipBenefitPresentationDTO struct {
	Description   string                      `json:"description"`
	PreviewItems  []vipBenefitPreviewItemDTO  `json:"previewItems"`
	NumericReward *vipBenefitNumericRewardDTO `json:"numericReward"`
}

type vipBenefitPreviewItemDTO struct {
	PreviewID       string `json:"previewId"`
	Title           string `json:"title"`
	MediaType       string `json:"mediaType"`
	AssetURL        string `json:"assetUrl"`
	PreviewURL      string `json:"previewUrl"`
	AnimationURL    string `json:"animationUrl"`
	CompositionType string `json:"compositionType"`
	SortOrder       int32  `json:"sortOrder"`
}

type vipBenefitNumericRewardDTO struct {
	Label  string `json:"label"`
	Value  int64  `json:"value"`
	Unit   string `json:"unit"`
	Period string `json:"period"`
}

type vipTrialCardDTO struct {
	TrialCardID         string `json:"trialCardId"`
	EntitlementID       string `json:"entitlementId"`
	ResourceID          int64  `json:"resourceId"`
	UserID              string `json:"userId"`
	Level               int32  `json:"level"`
	Name                string `json:"name"`
	Status              string `json:"status"`
	Equipped            bool   `json:"equipped"`
	DurationMS          int64  `json:"durationMs"`
	EffectiveAtMS       int64  `json:"effectiveAtMs"`
	ExpiresAtMS         int64  `json:"expiresAtMs"`
	RemainingDurationMS int64  `json:"remainingDurationMs"`
	GrantSource         string `json:"grantSource"`
	SourceGrantID       string `json:"sourceGrantId"`
	CreatedAtMS         int64  `json:"createdAtMs"`
	UpdatedAtMS         int64  `json:"updatedAtMs"`
}

type vipStateDTO struct {
	PaidVIP           userVipDTO          `json:"paidVip"`
	EquippedTrialCard vipTrialCardDTO     `json:"equippedTrialCard"`
	EffectiveVIP      userVipDTO          `json:"effectiveVip"`
	EffectiveSource   string              `json:"effectiveSource"`
	EffectiveBenefits []vipBenefitDTO     `json:"effectiveBenefits"`
	EvaluatedAtMS     int64               `json:"evaluatedAtMs"`
	ProgramConfig     vipProgramConfigDTO `json:"programConfig"`
	UserSettings      vipUserSettingsDTO  `json:"userSettings"`
}

type vipUserSettingsDTO struct {
	AppCode                      string `json:"appCode"`
	UserID                       string `json:"userId"`
	RoomEntryNoticeEnabled       bool   `json:"roomEntryNoticeEnabled"`
	OnlineGlobalNoticeEnabled    bool   `json:"onlineGlobalNoticeEnabled"`
	HideProfileDataEnabled       bool   `json:"hideProfileDataEnabled"`
	AnonymousProfileVisitEnabled bool   `json:"anonymousProfileVisitEnabled"`
	LeaderboardInvisibleEnabled  bool   `json:"leaderboardInvisibleEnabled"`
	UpdatedAtMS                  int64  `json:"updatedAtMs"`
}

type vipRewardItemDTO struct {
	ResourceID   string `json:"resourceId"`
	ResourceCode string `json:"resourceCode"`
	ResourceType string `json:"resourceType"`
	Name         string `json:"name"`
	Quantity     int64  `json:"quantity"`
	ExpiresAtMS  int64  `json:"expiresAtMs"`
}

func (h *Handler) GetProgram(c *gin.Context) {
	resp, err := h.wallet.GetVipProgramConfig(c.Request.Context(), &walletv1.GetVipProgramConfigRequest{
		RequestId: middleware.CurrentRequestID(c),
		AppCode:   appctx.FromContext(c.Request.Context()),
	})
	if err != nil {
		response.ServerError(c, "获取 VIP 体系失败")
		return
	}
	// 体系规则与权益目录同次返回，保证后台切换 App 后不会把上一 App 的权益矩阵误带入当前表单。
	response.OK(c, programDTO{
		Config:       vipProgramConfigFromProto(resp.GetConfig()),
		Benefits:     vipBenefitsFromProto(resp.GetBenefits()),
		ServerTimeMS: resp.GetServerTimeMs(),
	})
}

func (h *Handler) UpdateProgram(c *gin.Context) {
	var req programRequest
	if err := c.ShouldBindJSON(&req); err != nil || req.Config == nil {
		response.BadRequest(c, "VIP 体系参数不正确")
		return
	}
	config := req.Config.toProto()
	if config.GetLevelCount() <= 0 || strings.TrimSpace(config.GetProgramType()) == "" || strings.TrimSpace(config.GetStatus()) == "" {
		response.BadRequest(c, "VIP 体系参数不正确")
		return
	}
	appCode := appctx.FromContext(c.Request.Context())
	// AppCode、版本和审计操作者只取服务端上下文；即使客户端提交同名字段也不能跨 App 或伪造审计信息。
	config.AppCode = appCode
	config.ConfigVersion = 0
	config.UpdatedByAdminId = 0
	config.CreatedAtMs = 0
	config.UpdatedAtMs = 0
	resp, err := h.wallet.UpdateAdminVipProgramConfig(c.Request.Context(), &walletv1.UpdateAdminVipProgramConfigRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appCode,
		Config:         config,
		OperatorUserId: int64(middleware.CurrentUserID(c)),
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "update-vip-program", "vip_program_configs", appCode, "success", resp.GetConfig().GetProgramType())
	response.OK(c, programDTO{
		Config:       vipProgramConfigFromProto(resp.GetConfig()),
		Benefits:     []vipBenefitDTO{},
		ServerTimeMS: resp.GetServerTimeMs(),
	})
}

func (h *Handler) ListLevels(c *gin.Context) {
	resp, err := h.wallet.ListAdminVipLevels(c.Request.Context(), &walletv1.ListAdminVipLevelsRequest{
		RequestId: middleware.CurrentRequestID(c),
		AppCode:   appctx.FromContext(c.Request.Context()),
	})
	if err != nil {
		response.ServerError(c, "获取 VIP 配置失败")
		return
	}
	response.OK(c, configDTO{
		Levels:        vipLevelsFromProto(resp.GetLevels()),
		ProgramConfig: vipProgramConfigFromProto(resp.GetProgramConfig()),
		ServerTimeMS:  resp.GetServerTimeMs(),
	})
}

func (h *Handler) GrantVIP(c *gin.Context) {
	h.grantVIP(c, "admin_grant", "admin_vip_grant:")
}

// GrantExternalVIP uses Wallet's manager-center source so the owner service
// atomically applies its external-manager grant policy instead of trusting only
// this gateway's route permission.
func (h *Handler) GrantExternalVIP(c *gin.Context) {
	h.grantVIP(c, "manager_center", "external_vip_grant:")
}

func (h *Handler) grantVIP(c *gin.Context, grantSource string, defaultCommandPrefix string) {
	var req grantVipRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "VIP 赠送参数不正确")
		return
	}
	targetUserID, ok := parseFlexibleUserID(req.TargetUserID)
	if !ok || req.Level <= 0 {
		response.BadRequest(c, "VIP 赠送参数不正确")
		return
	}
	reason := strings.TrimSpace(req.Reason)
	if reason == "" {
		response.BadRequest(c, "赠送原因不能为空")
		return
	}
	commandID := strings.TrimSpace(req.CommandID)
	if commandID == "" {
		commandID = defaultCommandPrefix + middleware.CurrentRequestID(c)
	}
	resp, err := h.wallet.GrantVip(c.Request.Context(), &walletv1.GrantVipRequest{
		CommandId:      commandID,
		AppCode:        appctx.FromContext(c.Request.Context()),
		TargetUserId:   targetUserID,
		Level:          req.Level,
		GrantSource:    grantSource,
		OperatorUserId: int64(middleware.CurrentUserID(c)),
		Reason:         reason,
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "grant-vip", "user_vip_memberships", strconv.FormatInt(targetUserID, 10), "success", "level="+strconv.Itoa(int(req.Level)))
	if err := h.createVIPGrantNotice(c.Request.Context(), middleware.CurrentRequestID(c), appctx.FromContext(c.Request.Context()), commandID, targetUserID, req.Level, resp.GetTransactionId()); err != nil {
		response.ServerError(c, "VIP已赠送，但系统消息发送失败，请使用相同命令重试")
		return
	}
	response.Created(c, grantVipFromProto(resp))
}

func (h *Handler) GrantVIPTrialCard(c *gin.Context) {
	h.grantVIPTrialCard(c, "admin_grant", "admin_vip_trial_card_grant:")
}

// GrantExternalVIPTrialCard keeps duration/resource enforcement in Wallet's
// manager-center transaction, including the authoritative ManagerGrantEnabled
// check for an explicitly selected trial-card resource.
func (h *Handler) GrantExternalVIPTrialCard(c *gin.Context) {
	h.grantVIPTrialCard(c, "manager_center", "external_vip_trial_card_grant:")
}

func (h *Handler) grantVIPTrialCard(c *gin.Context, grantSource string, defaultCommandPrefix string) {
	var req grantVipTrialCardRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "VIP 体验卡赠送参数不正确")
		return
	}
	targetUserID, ok := parseFlexibleUserID(req.TargetUserID)
	if !ok || req.Level <= 0 || req.DurationMS <= 0 || (req.ResourceID != nil && *req.ResourceID <= 0) {
		response.BadRequest(c, "VIP 体验卡赠送参数不正确")
		return
	}
	if grantSource == "manager_center" {
		if req.Level > 6 {
			response.BadRequest(c, "外管最多只能赠送 VIP6 体验卡")
			return
		}
		scope, external := appctx.ExternalScopeFromContext(c.Request.Context())
		if !external || !containsExternalVIPTarget(scope, targetUserID) {
			response.Forbidden(c, "目标用户不在当前团队或区域")
			return
		}
	}
	reason := strings.TrimSpace(req.Reason)
	if reason == "" {
		response.BadRequest(c, "赠送原因不能为空")
		return
	}
	commandID := strings.TrimSpace(req.CommandID)
	if commandID == "" {
		commandID = defaultCommandPrefix + middleware.CurrentRequestID(c)
	}
	resp, err := h.wallet.GrantVipTrialCard(c.Request.Context(), &walletv1.GrantVipTrialCardRequest{
		CommandId:      commandID,
		AppCode:        appctx.FromContext(c.Request.Context()),
		TargetUserId:   targetUserID,
		Level:          req.Level,
		DurationMs:     req.DurationMS,
		ResourceId:     req.ResourceID,
		GrantSource:    grantSource,
		OperatorUserId: int64(middleware.CurrentUserID(c)),
		Reason:         reason,
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	// 体验卡是独立背包实例，审计目标使用 trial_card_id，后续切换佩戴不会覆盖本次赠送记录。
	shared.OperationLogWithResourceID(c, h.audit, "grant-vip-trial-card", "user_vip_trial_cards", resp.GetTrialCard().GetTrialCardId(), "success", "target_user_id="+strconv.FormatInt(targetUserID, 10))
	if err := h.createVIPTrialCardGrantNotice(c.Request.Context(), middleware.CurrentRequestID(c), appctx.FromContext(c.Request.Context()), commandID, targetUserID, req.Level, resp.GetTrialCard().GetTrialCardId()); err != nil {
		response.ServerError(c, "VIP体验卡已赠送，但系统消息发送失败，请使用相同命令重试")
		return
	}
	response.Created(c, grantVipTrialCardFromProto(resp))
}

func containsExternalVIPTarget(scope appctx.ExternalScope, targetUserID int64) bool {
	if scope.RegionID <= 0 || targetUserID <= 0 {
		return false
	}
	for _, userID := range scope.OwnerUserIDs {
		if userID == targetUserID {
			return true
		}
	}
	return false
}

func (h *Handler) UpdateLevels(c *gin.Context) {
	var req configRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.BadRequest(c, "VIP 配置参数不正确")
		return
	}
	input := make([]*walletv1.AdminVipLevelInput, 0, len(req.Levels))
	for _, level := range req.Levels {
		benefits := make([]*walletv1.VipBenefit, 0, len(level.Benefits))
		for _, benefit := range level.Benefits {
			benefits = append(benefits, benefit.toProto())
		}
		input = append(input, &walletv1.AdminVipLevelInput{
			Level:                 level.Level,
			Name:                  strings.TrimSpace(level.Name),
			Status:                strings.TrimSpace(level.Status),
			PriceCoin:             level.PriceCoin,
			DurationMs:            level.DurationMS,
			RewardResourceGroupId: level.RewardResourceGroupID,
			// VIP 累充门槛已下线；后台旧请求即使携带旧字段，也统一写 0，避免旧页面或缓存把门槛重新保存回来。
			RequiredRechargeCoinAmount: 0,
			SortOrder:                  level.SortOrder,
			Benefits:                   benefits,
		})
	}
	resp, err := h.wallet.UpdateAdminVipLevels(c.Request.Context(), &walletv1.UpdateAdminVipLevelsRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		Levels:         input,
		OperatorUserId: int64(middleware.CurrentUserID(c)),
	})
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "update-vip-config", "vip_levels", appctx.FromContext(c.Request.Context()), "success", strconv.Itoa(len(resp.GetLevels())))
	response.OK(c, configDTO{
		Levels:        vipLevelsFromProto(resp.GetLevels()),
		ProgramConfig: vipProgramConfigFromProto(resp.GetProgramConfig()),
		ServerTimeMS:  resp.GetServerTimeMs(),
	})
}

func vipLevelsFromProto(items []*walletv1.VipLevel) []vipLevelDTO {
	levels := make([]vipLevelDTO, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		levels = append(levels, vipLevelDTO{
			Level:                 item.GetLevel(),
			Name:                  item.GetName(),
			Status:                item.GetStatus(),
			PriceCoin:             item.GetPriceCoin(),
			DurationMS:            item.GetDurationMs(),
			RewardResourceGroupID: item.GetRewardResourceGroupId(),
			SortOrder:             item.GetSortOrder(),
			// 响应仍保留兼容字段，但后台配置页不再展示或依赖累充资格，统一返回无门槛。
			RechargeGateRequired:       false,
			RequiredRechargeCoinAmount: 0,
			UserRechargeCoinAmount:     0,
			PurchaseLockedReason:       item.GetPurchaseLockedReason(),
			CreatedAtMS:                item.GetCreatedAtMs(),
			UpdatedAtMS:                item.GetUpdatedAtMs(),
			Benefits:                   vipBenefitsFromProto(item.GetBenefits()),
			ConfigVersion:              item.GetConfigVersion(),
		})
	}
	return levels
}

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

func (item vipProgramConfigDTO) toProto() *walletv1.VipProgramConfig {
	return &walletv1.VipProgramConfig{
		ProgramType:              strings.TrimSpace(item.ProgramType),
		LevelCount:               item.LevelCount,
		SameLevelExpiryPolicy:    strings.TrimSpace(item.SameLevelExpiryPolicy),
		UpgradeExpiryPolicy:      strings.TrimSpace(item.UpgradeExpiryPolicy),
		DowngradePurchasePolicy:  strings.TrimSpace(item.DowngradePurchasePolicy),
		BenefitInheritancePolicy: strings.TrimSpace(item.BenefitInheritancePolicy),
		GrantMode:                strings.TrimSpace(item.GrantMode),
		TrialCardEnabled:         item.TrialCardEnabled,
		Status:                   strings.TrimSpace(item.Status),
	}
}

func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitDTO {
	benefits := make([]vipBenefitDTO, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		benefits = append(benefits, vipBenefitDTO{
			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()),
		})
	}
	return benefits
}

func (item vipBenefitDTO) toProto() *walletv1.VipBenefit {
	return &walletv1.VipBenefit{
		BenefitCode:    strings.TrimSpace(item.BenefitCode),
		Name:           strings.TrimSpace(item.Name),
		BenefitType:    strings.TrimSpace(item.BenefitType),
		UnlockLevel:    item.UnlockLevel,
		Status:         strings.TrimSpace(item.Status),
		TrialEnabled:   item.TrialEnabled,
		ResourceId:     item.ResourceID,
		ResourceType:   strings.TrimSpace(item.ResourceType),
		ExecutionScope: strings.TrimSpace(item.ExecutionScope),
		AutoEquip:      item.AutoEquip,
		SortOrder:      item.SortOrder,
		MetadataJson:   strings.TrimSpace(item.MetadataJSON),
		Presentation:   item.Presentation.toProto(),
	}
}

func vipBenefitPresentationFromProto(item *walletv1.VipBenefitPresentation) *vipBenefitPresentationDTO {
	if item == nil {
		return nil
	}
	result := &vipBenefitPresentationDTO{
		Description:  item.GetDescription(),
		PreviewItems: make([]vipBenefitPreviewItemDTO, 0, len(item.GetPreviewItems())),
	}
	for _, preview := range item.GetPreviewItems() {
		if preview == nil {
			continue
		}
		result.PreviewItems = append(result.PreviewItems, vipBenefitPreviewItemDTO{
			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 = &vipBenefitNumericRewardDTO{
			Label: reward.GetLabel(), Value: reward.GetValue(), Unit: reward.GetUnit(), Period: reward.GetPeriod(),
		}
	}
	return result
}

func (item *vipBenefitPresentationDTO) toProto() *walletv1.VipBenefitPresentation {
	if item == nil {
		return nil
	}
	result := &walletv1.VipBenefitPresentation{
		Description:  strings.TrimSpace(item.Description),
		PreviewItems: make([]*walletv1.VipBenefitPreviewItem, 0, len(item.PreviewItems)),
	}
	for _, preview := range item.PreviewItems {
		result.PreviewItems = append(result.PreviewItems, &walletv1.VipBenefitPreviewItem{
			PreviewId: strings.TrimSpace(preview.PreviewID), Title: strings.TrimSpace(preview.Title),
			MediaType: strings.TrimSpace(preview.MediaType), AssetUrl: strings.TrimSpace(preview.AssetURL),
			PreviewUrl: strings.TrimSpace(preview.PreviewURL), AnimationUrl: strings.TrimSpace(preview.AnimationURL),
			CompositionType: strings.TrimSpace(preview.CompositionType), SortOrder: preview.SortOrder,
		})
	}
	if item.NumericReward != nil {
		result.NumericReward = &walletv1.VipBenefitNumericReward{
			Label: strings.TrimSpace(item.NumericReward.Label), Value: item.NumericReward.Value,
			Unit: strings.TrimSpace(item.NumericReward.Unit), Period: strings.TrimSpace(item.NumericReward.Period),
		}
	}
	return result
}

func grantVipFromProto(resp *walletv1.GrantVipResponse) grantVipDTO {
	if resp == nil {
		return grantVipDTO{}
	}
	return grantVipDTO{
		TransactionID: resp.GetTransactionId(),
		VIP:           userVipFromProto(resp.GetVip()),
		RewardItems:   vipRewardItemsFromProto(resp.GetRewardItems()),
		ServerTimeMS:  resp.GetServerTimeMs(),
	}
}

func userVipFromProto(item *walletv1.UserVip) userVipDTO {
	if item == nil {
		return userVipDTO{}
	}
	return userVipDTO{
		UserID:        strconv.FormatInt(item.GetUserId(), 10),
		Level:         item.GetLevel(),
		Name:          item.GetName(),
		Active:        item.GetActive(),
		StartedAtMS:   item.GetStartedAtMs(),
		ExpiresAtMS:   item.GetExpiresAtMs(),
		UpdatedAtMS:   item.GetUpdatedAtMs(),
		ProgramType:   item.GetProgramType(),
		ConfigVersion: item.GetConfigVersion(),
	}
}

func grantVipTrialCardFromProto(resp *walletv1.GrantVipTrialCardResponse) grantVipTrialCardDTO {
	if resp == nil {
		return grantVipTrialCardDTO{}
	}
	return grantVipTrialCardDTO{
		TrialCard:    vipTrialCardFromProto(resp.GetTrialCard()),
		State:        vipStateFromProto(resp.GetState()),
		ServerTimeMS: resp.GetServerTimeMs(),
	}
}

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

func vipStateFromProto(item *walletv1.VipState) vipStateDTO {
	if item == nil {
		return vipStateDTO{EffectiveBenefits: []vipBenefitDTO{}}
	}
	return vipStateDTO{
		PaidVIP:           userVipFromProto(item.GetPaidVip()),
		EquippedTrialCard: vipTrialCardFromProto(item.GetEquippedTrialCard()),
		EffectiveVIP:      userVipFromProto(item.GetEffectiveVip()),
		EffectiveSource:   item.GetEffectiveSource(),
		EffectiveBenefits: vipBenefitsFromProto(item.GetEffectiveBenefits()),
		EvaluatedAtMS:     item.GetEvaluatedAtMs(),
		ProgramConfig:     vipProgramConfigFromProto(item.GetProgramConfig()),
		UserSettings:      vipUserSettingsFromProto(item.GetUserSettings()),
	}
}

func vipUserSettingsFromProto(item *walletv1.VipUserSettings) vipUserSettingsDTO {
	if item == nil {
		return vipUserSettingsDTO{}
	}
	return vipUserSettingsDTO{
		AppCode: item.GetAppCode(), UserID: strconv.FormatInt(item.GetUserId(), 10),
		RoomEntryNoticeEnabled:       item.GetRoomEntryNoticeEnabled(),
		OnlineGlobalNoticeEnabled:    item.GetOnlineGlobalNoticeEnabled(),
		HideProfileDataEnabled:       item.GetHideProfileDataEnabled(),
		AnonymousProfileVisitEnabled: item.GetAnonymousProfileVisitEnabled(),
		LeaderboardInvisibleEnabled:  item.GetLeaderboardInvisibleEnabled(),
		UpdatedAtMS:                  item.GetUpdatedAtMs(),
	}
}

func vipRewardItemsFromProto(items []*walletv1.VipRewardItem) []vipRewardItemDTO {
	rewards := make([]vipRewardItemDTO, 0, len(items))
	for _, item := range items {
		if item == nil {
			continue
		}
		rewards = append(rewards, vipRewardItemDTO{
			ResourceID:   strconv.FormatInt(item.GetResourceId(), 10),
			ResourceCode: item.GetResourceCode(),
			ResourceType: item.GetResourceType(),
			Name:         item.GetName(),
			Quantity:     item.GetQuantity(),
			ExpiresAtMS:  item.GetExpiresAtMs(),
		})
	}
	return rewards
}

func parseFlexibleUserID(value any) (int64, bool) {
	switch typed := value.(type) {
	case float64:
		userID := int64(typed)
		return userID, userID > 0 && float64(userID) == typed
	case string:
		userID, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
		return userID, err == nil && userID > 0
	default:
		return 0, false
	}
}
