package walletapi

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

	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/services/gateway-service/internal/auth"
)

type rechargeProductData struct {
	ProductID         int64   `json:"product_id"`
	ProductCode       string  `json:"product_code"`
	ProductName       string  `json:"product_name"`
	Description       string  `json:"description"`
	AudienceType      string  `json:"audience_type"`
	Platform          string  `json:"platform"`
	Channel           string  `json:"channel"`
	CurrencyCode      string  `json:"currency_code"`
	CoinAmount        int64   `json:"coin_amount"`
	AmountMinor       int64   `json:"amount_minor"`
	AmountMicro       int64   `json:"amount_micro"`
	PolicyVersion     string  `json:"policy_version"`
	RegionIDs         []int64 `json:"region_ids"`
	ResourceAssetType string  `json:"resource_asset_type"`
	Status            string  `json:"status"`
	Enabled           bool    `json:"enabled"`
	AppVisible        bool    `json:"app_visible"`
	SortOrder         int32   `json:"sort_order"`
}

type diamondExchangeRuleData struct {
	ExchangeType  string `json:"exchange_type"`
	FromAssetType string `json:"from_asset_type"`
	ToAssetType   string `json:"to_asset_type"`
	FromAmount    int64  `json:"from_amount"`
	ToAmount      int64  `json:"to_amount"`
	Enabled       bool   `json:"enabled"`
}

type walletOverviewData struct {
	Balances     []assetBalanceData     `json:"balances"`
	FeatureFlags walletFeatureFlagsData `json:"feature_flags"`
}

type walletFeatureFlagsData struct {
	RechargeEnabled        bool `json:"recharge_enabled"`
	DiamondExchangeEnabled bool `json:"diamond_exchange_enabled"`
}

type walletTransactionData struct {
	EntryID                   int64  `json:"entry_id"`
	TransactionID             string `json:"transaction_id"`
	BizType                   string `json:"biz_type"`
	AssetType                 string `json:"asset_type"`
	AvailableDelta            int64  `json:"available_delta"`
	FrozenDelta               int64  `json:"frozen_delta"`
	AvailableAfter            int64  `json:"available_after"`
	FrozenAfter               int64  `json:"frozen_after"`
	CounterpartyUserID        int64  `json:"counterparty_user_id"`
	CounterpartyDisplayUserID string `json:"counterparty_display_user_id,omitempty"`
	CounterpartyUsername      string `json:"counterparty_username,omitempty"`
	CounterpartyAvatar        string `json:"counterparty_avatar,omitempty"`
	RoomID                    string `json:"room_id"`
	CreatedAtMS               int64  `json:"created_at_ms"`
	TransferUSDMinor          int64  `json:"transfer_usd_minor,omitempty"`
	TransferCurrencyCode      string `json:"transfer_currency_code,omitempty"`
}

type giftWallData struct {
	Items             []giftWallItemData `json:"items"`
	GiftKindCount     int64              `json:"gift_kind_count"`
	GiftTotalCount    int64              `json:"gift_total_count"`
	TotalValue        int64              `json:"total_value"`
	TotalCoinValue    int64              `json:"total_coin_value"`
	TotalDiamondValue int64              `json:"total_diamond_value"`
	TotalGiftPoint    int64              `json:"total_gift_point"`
	TotalHeatValue    int64              `json:"total_heat_value"`
}

type giftWallItemData struct {
	GiftID               string        `json:"gift_id"`
	GiftName             string        `json:"gift_name"`
	ResourceID           int64         `json:"resource_id"`
	Resource             *resourceData `json:"resource,omitempty"`
	ResourceSnapshotJSON string        `json:"resource_snapshot_json"`
	GiftTypeCode         string        `json:"gift_type_code"`
	PresentationJSON     string        `json:"presentation_json"`
	GiftCount            int64         `json:"gift_count"`
	TotalValue           int64         `json:"total_value"`
	TotalCoinValue       int64         `json:"total_coin_value"`
	TotalDiamondValue    int64         `json:"total_diamond_value"`
	TotalGiftPoint       int64         `json:"total_gift_point"`
	TotalHeatValue       int64         `json:"total_heat_value"`
	ChargeAssetType      string        `json:"charge_asset_type"`
	LastPriceVersion     string        `json:"last_price_version"`
	FirstReceivedAtMS    int64         `json:"first_received_at_ms"`
	LastReceivedAtMS     int64         `json:"last_received_at_ms"`
	SortOrder            int32         `json:"sort_order"`
}

type googlePaymentConfirmRequestBody struct {
	CommandID       string `json:"command_id"`
	CommandIDAlt    string `json:"commandId"`
	ProductID       int64  `json:"product_id"`
	ProductIDAlt    int64  `json:"productId"`
	ProductCode     string `json:"product_code"`
	ProductCodeAlt  string `json:"productCode"`
	PackageName     string `json:"package_name"`
	PackageNameAlt  string `json:"packageName"`
	PurchaseToken   string `json:"purchase_token"`
	PurchaseAlt     string `json:"purchaseToken"`
	OrderID         string `json:"order_id"`
	OrderIDAlt      string `json:"orderId"`
	PurchaseTimeMS  int64  `json:"purchase_time_ms"`
	PurchaseTimeAlt int64  `json:"purchaseTimeMs"`
}

type googlePaymentConfirmData struct {
	PaymentOrderID   string           `json:"payment_order_id"`
	TransactionID    string           `json:"transaction_id"`
	Status           string           `json:"status"`
	ProductID        int64            `json:"product_id"`
	ProductCode      string           `json:"product_code"`
	CoinAmount       int64            `json:"coin_amount"`
	Balance          assetBalanceData `json:"balance"`
	IdempotentReplay bool             `json:"idempotent_replay"`
	ConsumeState     string           `json:"consume_state"`
}

type vipPurchaseRequestBody struct {
	CommandID    string `json:"command_id"`
	CommandIDAlt string `json:"commandId"`
	Level        int32  `json:"level"`
}

// vipMeData 匿名嵌入旧 DTO 以保留 data.level/name/active 等历史字段，新增结构化
// state 后客户端可以逐版本迁移，不会因一次协议升级失去当前会员展示。
type vipMeData struct {
	OverviewVIPData overviewVIPData      `json:"-"`
	ProgramConfig   vipProgramConfigData `json:"program_config"`
	State           vipStateData         `json:"state"`
}

// MarshalJSON 明确把旧字段保持在 data 根层，避免嵌套成 overview_vip_data 而破坏
// 已发布 Flutter 解析；新字段只做向后兼容的追加。
func (data vipMeData) MarshalJSON() ([]byte, error) {
	type wire struct {
		Level         int32                `json:"level"`
		Name          string               `json:"name"`
		Active        bool                 `json:"active"`
		StartedAtMS   int64                `json:"started_at_ms"`
		ExpiresAtMS   int64                `json:"expires_at_ms"`
		ProgramType   string               `json:"program_type,omitempty"`
		ConfigVersion int64                `json:"config_version,omitempty"`
		Unavailable   bool                 `json:"unavailable,omitempty"`
		ProgramConfig vipProgramConfigData `json:"program_config"`
		State         vipStateData         `json:"state"`
	}
	return json.Marshal(wire{
		Level:         data.OverviewVIPData.Level,
		Name:          data.OverviewVIPData.Name,
		Active:        data.OverviewVIPData.Active,
		StartedAtMS:   data.OverviewVIPData.StartedAtMS,
		ExpiresAtMS:   data.OverviewVIPData.ExpiresAtMS,
		ProgramType:   data.OverviewVIPData.ProgramType,
		ConfigVersion: data.OverviewVIPData.ConfigVersion,
		Unavailable:   data.OverviewVIPData.Unavailable,
		ProgramConfig: data.ProgramConfig,
		State:         data.State,
	})
}

type vipPackageData struct {
	Level                      int32               `json:"level"`
	Name                       string              `json:"name"`
	Status                     string              `json:"status"`
	PriceCoin                  int64               `json:"price_coin"`
	DurationMS                 int64               `json:"duration_ms"`
	RewardResourceGroupID      int64               `json:"reward_resource_group_id"`
	RewardItems                []vipRewardItemData `json:"reward_items"`
	CanPurchase                bool                `json:"can_purchase"`
	SortOrder                  int32               `json:"sort_order"`
	RechargeGateRequired       bool                `json:"recharge_gate_required"`
	RequiredRechargeCoinAmount int64               `json:"required_recharge_coin_amount"`
	UserRechargeCoinAmount     int64               `json:"user_recharge_coin_amount"`
	PurchaseLockedReason       string              `json:"purchase_locked_reason"`
	Benefits                   []vipBenefitData    `json:"benefits"`
	ConfigVersion              int64               `json:"config_version"`
}

// vipProgramConfigData 原样暴露 wallet-service 已判定的 App 级策略；客户端只据此展示，
// 购买有效期、降级限制和体验卡开关始终由 wallet-service 在事务内执行。
type vipProgramConfigData struct {
	AppCode                  string `json:"app_code"`
	ProgramType              string `json:"program_type"`
	LevelCount               int32  `json:"level_count"`
	SameLevelExpiryPolicy    string `json:"same_level_expiry_policy"`
	UpgradeExpiryPolicy      string `json:"upgrade_expiry_policy"`
	DowngradePurchasePolicy  string `json:"downgrade_purchase_policy"`
	BenefitInheritancePolicy string `json:"benefit_inheritance_policy"`
	GrantMode                string `json:"grant_mode"`
	TrialCardEnabled         bool   `json:"trial_card_enabled"`
	Status                   string `json:"status"`
	ConfigVersion            int64  `json:"config_version"`
	UpdatedByAdminID         int64  `json:"updated_by_admin_id"`
	CreatedAtMS              int64  `json:"created_at_ms"`
	UpdatedAtMS              int64  `json:"updated_at_ms"`
}

// vipBenefitData 是最终生效权益的资格描述。客户端不得根据 unlock_level 自行继承；
// 对可关闭通知还需结合 state.user_settings，真正执行前以 CheckVipBenefit 为准。
type vipBenefitData struct {
	BenefitCode    string                      `json:"benefit_code"`
	Name           string                      `json:"name"`
	BenefitType    string                      `json:"benefit_type"`
	UnlockLevel    int32                       `json:"unlock_level"`
	Status         string                      `json:"status"`
	TrialEnabled   bool                        `json:"trial_enabled"`
	ResourceID     int64                       `json:"resource_id"`
	ResourceType   string                      `json:"resource_type"`
	ExecutionScope string                      `json:"execution_scope"`
	AutoEquip      bool                        `json:"auto_equip"`
	SortOrder      int32                       `json:"sort_order"`
	MetadataJSON   string                      `json:"metadata_json"`
	CreatedAtMS    int64                       `json:"created_at_ms"`
	UpdatedAtMS    int64                       `json:"updated_at_ms"`
	Presentation   *vipBenefitPresentationData `json:"presentation"`
	// Resource 只有在 wallet 已确认同 App、active 且类型匹配时才非空。
	Resource *resourceData `json:"resource,omitempty"`
}

type vipBenefitPresentationData struct {
	Description   string                       `json:"description"`
	PreviewItems  []vipBenefitPreviewItemData  `json:"preview_items"`
	NumericReward *vipBenefitNumericRewardData `json:"numeric_reward"`
}

type vipBenefitPreviewItemData struct {
	PreviewID       string `json:"preview_id"`
	Title           string `json:"title"`
	MediaType       string `json:"media_type"`
	AssetURL        string `json:"asset_url"`
	PreviewURL      string `json:"preview_url"`
	AnimationURL    string `json:"animation_url"`
	CompositionType string `json:"composition_type"`
	SortOrder       int32  `json:"sort_order"`
}

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

type vipTrialCardData struct {
	TrialCardID         string `json:"trial_card_id"`
	EntitlementID       string `json:"entitlement_id"`
	ResourceID          int64  `json:"resource_id"`
	UserID              int64  `json:"user_id"`
	Level               int32  `json:"level"`
	Name                string `json:"name"`
	Status              string `json:"status"`
	Equipped            bool   `json:"equipped"`
	DurationMS          int64  `json:"duration_ms"`
	EffectiveAtMS       int64  `json:"effective_at_ms"`
	ExpiresAtMS         int64  `json:"expires_at_ms"`
	RemainingDurationMS int64  `json:"remaining_duration_ms"`
	GrantSource         string `json:"grant_source"`
	SourceGrantID       string `json:"source_grant_id"`
	CreatedAtMS         int64  `json:"created_at_ms"`
	UpdatedAtMS         int64  `json:"updated_at_ms"`
}

type vipStateData struct {
	PaidVIP           overviewVIPData      `json:"paid_vip"`
	EquippedTrialCard *vipTrialCardData    `json:"equipped_trial_card"`
	EffectiveVIP      overviewVIPData      `json:"effective_vip"`
	EffectiveSource   string               `json:"effective_source"`
	EffectiveBenefits []vipBenefitData     `json:"effective_benefits"`
	EvaluatedAtMS     int64                `json:"evaluated_at_ms"`
	ProgramConfig     vipProgramConfigData `json:"program_config"`
	UserSettings      vipUserSettingsData  `json:"user_settings"`
}

type vipUserSettingsData struct {
	AppCode                      string `json:"app_code"`
	UserID                       int64  `json:"user_id"`
	RoomEntryNoticeEnabled       bool   `json:"room_entry_notice_enabled"`
	OnlineGlobalNoticeEnabled    bool   `json:"online_global_notice_enabled"`
	HideProfileDataEnabled       bool   `json:"hide_profile_data_enabled"`
	AnonymousProfileVisitEnabled bool   `json:"anonymous_profile_visit_enabled"`
	LeaderboardInvisibleEnabled  bool   `json:"leaderboard_invisible_enabled"`
	UpdatedAtMS                  int64  `json:"updated_at_ms"`
}

type vipRewardItemData struct {
	ResourceID   int64  `json:"resource_id"`
	ResourceCode string `json:"resource_code"`
	ResourceType string `json:"resource_type"`
	Name         string `json:"name"`
	Quantity     int64  `json:"quantity"`
	ExpiresAtMS  int64  `json:"expires_at_ms"`
	CoverURL     string `json:"cover_url"`
	AssetURL     string `json:"asset_url"`
	PreviewURL   string `json:"preview_url"`
	AnimationURL string `json:"animation_url"`
}

// getWalletOverview 返回钱包二级页摘要；我的页首屏使用更轻的 WalletValueSummary。
func (h *Handler) getWalletOverview(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.GetWalletOverview(request.Context(), &walletv1.GetWalletOverviewRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, walletOverviewFromProto(resp))
}

// listRechargeProducts 返回用户当前区域可用的充值档位。
func (h *Handler) listRechargeProducts(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	userID := auth.UserIDFromContext(request.Context())
	profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	resp, err := h.walletClient.ListRechargeProducts(request.Context(), &walletv1.ListRechargeProductsRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		UserId:       userID,
		RegionId:     profileResp.GetUser().GetRegionId(),
		Platform:     httpkit.FirstQueryOrHeader(request, "platform", "X-App-Platform", "X-Platform"),
		AudienceType: "normal",
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	products := make([]rechargeProductData, 0, len(resp.GetProducts()))
	for _, product := range resp.GetProducts() {
		products = append(products, rechargeProductFromProto(product))
	}
	httpkit.WriteOK(writer, request, map[string]any{"channels": resp.GetChannels(), "products": products})
}

// confirmGooglePayment 接收 App 支付成功后的 Google purchase_token，转交 wallet-service 做服务端校验和入账。
func (h *Handler) confirmGooglePayment(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body googlePaymentConfirmRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := strings.TrimSpace(body.CommandID)
	if commandID == "" {
		commandID = strings.TrimSpace(body.CommandIDAlt)
	}
	productID := body.ProductID
	if productID <= 0 {
		productID = body.ProductIDAlt
	}
	productCode := strings.TrimSpace(body.ProductCode)
	if productCode == "" {
		productCode = strings.TrimSpace(body.ProductCodeAlt)
	}
	packageName := strings.TrimSpace(body.PackageName)
	if packageName == "" {
		packageName = strings.TrimSpace(body.PackageNameAlt)
	}
	purchaseToken := strings.TrimSpace(body.PurchaseToken)
	if purchaseToken == "" {
		purchaseToken = strings.TrimSpace(body.PurchaseAlt)
	}
	orderID := strings.TrimSpace(body.OrderID)
	if orderID == "" {
		orderID = strings.TrimSpace(body.OrderIDAlt)
	}
	purchaseTimeMS := body.PurchaseTimeMS
	if purchaseTimeMS <= 0 {
		purchaseTimeMS = body.PurchaseTimeAlt
	}
	// 普通充值页继续传 product_id；首充弹窗只持有 Google 商品 ID，
	// 因此允许 product_id 为空，但必须把 google_product_id 放进 product_code 交给 wallet-service 回查内部商品。
	if commandID == "" || packageName == "" || purchaseToken == "" || (productID <= 0 && productCode == "") {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	userID := auth.UserIDFromContext(request.Context())
	profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	resp, err := h.walletClient.ConfirmGooglePayment(request.Context(), &walletv1.ConfirmGooglePaymentRequest{
		RequestId:      httpkit.RequestIDFromContext(request.Context()),
		AppCode:        appcode.FromContext(request.Context()),
		CommandId:      commandID,
		UserId:         userID,
		RegionId:       profileResp.GetUser().GetRegionId(),
		CountryId:      profileResp.GetUser().GetCountryId(),
		ProductId:      productID,
		ProductCode:    productCode,
		PackageName:    packageName,
		PurchaseToken:  purchaseToken,
		OrderId:        orderID,
		PurchaseTimeMs: purchaseTimeMS,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, googlePaymentConfirmFromProto(resp))
}

// getDiamondExchangeConfig 返回钻石兑换配置。
func (h *Handler) getDiamondExchangeConfig(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.GetDiamondExchangeConfig(request.Context(), &walletv1.GetDiamondExchangeConfigRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	rules := make([]diamondExchangeRuleData, 0, len(resp.GetRules()))
	for _, rule := range resp.GetRules() {
		rules = append(rules, diamondExchangeRuleFromProto(rule))
	}
	httpkit.WriteOK(writer, request, map[string]any{"rules": rules})
}

// getMyGiftWall 返回当前登录用户已经收到的礼物墙；“我的”入口只信任 token 里的 user_id，避免客户端用参数伪造成他人。
func (h *Handler) getMyGiftWall(writer http.ResponseWriter, request *http.Request) {
	h.writeGiftWall(writer, request, auth.UserIDFromContext(request.Context()))
}

// getUserGiftWall 返回资料卡目标用户已经收到的礼物墙；资料卡本来就是公开展示面，路径 user_id 只决定展示对象，不参与鉴权身份。
func (h *Handler) getUserGiftWall(writer http.ResponseWriter, request *http.Request) {
	userID, ok := httpkit.PositiveInt64PathValue(request, "user_id")
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	h.writeGiftWall(writer, request, userID)
}

// writeGiftWall 统一当前用户入口和资料卡入口的响应格式；wallet-service 持有最终一致的收礼投影，gateway 只传稳定 user_id。
func (h *Handler) writeGiftWall(writer http.ResponseWriter, request *http.Request, userID int64) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	if userID <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetUserGiftWall(request.Context(), &walletv1.GetUserGiftWallRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, giftWallFromProto(resp))
}

// listWalletTransactions 返回当前用户钱包流水分页。
func (h *Handler) listWalletTransactions(writer http.ResponseWriter, request *http.Request) {
	h.listWalletTransactionsByAsset(writer, request, strings.TrimSpace(request.URL.Query().Get("asset_type")), 20)
}

// listCoinTransactions 返回当前用户金币流水分页；默认 page_size=30 对齐 App 最近 30 条展示。
func (h *Handler) listCoinTransactions(writer http.ResponseWriter, request *http.Request) {
	h.listWalletTransactionsByAsset(writer, request, "COIN", 30)
}

// listDiamondTransactions 查询主播周期钻石收入；钻石不是通用钱包资产，必须走 wallet-service 独立读模型。
func (h *Handler) listDiamondTransactions(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, pageSize, startAtMS, endAtMS, ok := walletTransactionQuery(request, 30)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListHostPeriodDiamondTransactions(request.Context(), &walletv1.ListWalletTransactionsRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
		Page:      page,
		PageSize:  pageSize,
		StartAtMs: startAtMS,
		EndAtMs:   endAtMS,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	h.writeWalletTransactions(writer, request, resp, page, pageSize)
}

func (h *Handler) listWalletTransactionsByAsset(writer http.ResponseWriter, request *http.Request, assetType string, defaultPageSize int32) {
	if h.walletClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	page, pageSize, startAtMS, endAtMS, ok := walletTransactionQuery(request, defaultPageSize)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.ListWalletTransactions(request.Context(), &walletv1.ListWalletTransactionsRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    auth.UserIDFromContext(request.Context()),
		AssetType: assetType,
		Page:      page,
		PageSize:  pageSize,
		StartAtMs: startAtMS,
		EndAtMs:   endAtMS,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	h.writeWalletTransactions(writer, request, resp, page, pageSize)
}

func walletTransactionQuery(request *http.Request, defaultPageSize int32) (int32, int32, int64, int64, bool) {
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		return 0, 0, 0, 0, false
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", defaultPageSize)
	if !ok {
		return 0, 0, 0, 0, false
	}
	startRaw := strings.TrimSpace(request.URL.Query().Get("start_at_ms"))
	endRaw := strings.TrimSpace(request.URL.Query().Get("end_at_ms"))
	if startRaw == "" && endRaw == "" {
		return page, pageSize, 0, 0, true
	}
	if startRaw == "" || endRaw == "" {
		return 0, 0, 0, 0, false
	}
	startAtMS, startErr := strconv.ParseInt(startRaw, 10, 64)
	endAtMS, endErr := strconv.ParseInt(endRaw, 10, 64)
	if startErr != nil || endErr != nil || startAtMS <= 0 || endAtMS <= startAtMS {
		return 0, 0, 0, 0, false
	}
	return page, pageSize, startAtMS, endAtMS, true
}

func (h *Handler) writeWalletTransactions(writer http.ResponseWriter, request *http.Request, resp *walletv1.ListWalletTransactionsResponse, page int32, pageSize int32) {
	profiles, err := h.walletTransactionCounterpartyProfiles(request, resp.GetTransactions())
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]walletTransactionData, 0, len(resp.GetTransactions()))
	for _, item := range resp.GetTransactions() {
		items = append(items, walletTransactionFromProto(item, profiles))
	}
	httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": resp.GetTotal(), "page": page, "page_size": pageSize})
}

func (h *Handler) walletTransactionCounterpartyProfiles(request *http.Request, transactions []*walletv1.WalletTransaction) (map[int64]*userv1.User, error) {
	if h.userProfileClient == nil {
		return map[int64]*userv1.User{}, nil
	}
	userIDs := make([]int64, 0, len(transactions))
	seen := make(map[int64]struct{}, len(transactions))
	for _, transaction := range transactions {
		userID := transaction.GetCounterpartyUserId()
		if userID <= 0 {
			continue
		}
		if _, exists := seen[userID]; exists {
			continue
		}
		seen[userID] = struct{}{}
		userIDs = append(userIDs, userID)
	}
	if len(userIDs) == 0 {
		return map[int64]*userv1.User{}, nil
	}
	// 钱包流水只保存稳定 user_id；展示昵称、短号和头像必须回 user-service 批量补齐，避免 H5 把长 user_id 当短号展示。
	resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
		Meta:    httpkit.UserMeta(request, ""),
		UserIds: userIDs,
	})
	if err != nil {
		return nil, err
	}
	return resp.GetUsers(), nil
}

func rechargeProductFromProto(product *walletv1.RechargeProduct) rechargeProductData {
	if product == nil {
		return rechargeProductData{}
	}
	return rechargeProductData{
		ProductID:         product.GetProductId(),
		ProductCode:       product.GetProductCode(),
		ProductName:       product.GetProductName(),
		Description:       product.GetDescription(),
		AudienceType:      product.GetAudienceType(),
		Platform:          product.GetPlatform(),
		Channel:           product.GetChannel(),
		CurrencyCode:      product.GetCurrencyCode(),
		CoinAmount:        product.GetCoinAmount(),
		AmountMinor:       product.GetAmountMinor(),
		AmountMicro:       product.GetAmountMicro(),
		PolicyVersion:     product.GetPolicyVersion(),
		RegionIDs:         product.GetRegionIds(),
		ResourceAssetType: product.GetResourceAssetType(),
		Status:            product.GetStatus(),
		Enabled:           product.GetEnabled(),
		AppVisible:        product.GetAppVisible(),
		SortOrder:         product.GetSortOrder(),
	}
}

func googlePaymentConfirmFromProto(resp *walletv1.ConfirmGooglePaymentResponse) googlePaymentConfirmData {
	if resp == nil {
		return googlePaymentConfirmData{}
	}
	return googlePaymentConfirmData{
		PaymentOrderID:   resp.GetPaymentOrderId(),
		TransactionID:    resp.GetTransactionId(),
		Status:           resp.GetStatus(),
		ProductID:        resp.GetProductId(),
		ProductCode:      resp.GetProductCode(),
		CoinAmount:       resp.GetCoinAmount(),
		Balance:          balanceFromProto(resp.GetBalance()),
		IdempotentReplay: resp.GetIdempotentReplay(),
		ConsumeState:     resp.GetConsumeState(),
	}
}

func diamondExchangeRuleFromProto(rule *walletv1.DiamondExchangeRule) diamondExchangeRuleData {
	if rule == nil {
		return diamondExchangeRuleData{}
	}
	return diamondExchangeRuleData{
		ExchangeType:  rule.GetExchangeType(),
		FromAssetType: rule.GetFromAssetType(),
		ToAssetType:   rule.GetToAssetType(),
		FromAmount:    rule.GetFromAmount(),
		ToAmount:      rule.GetToAmount(),
		Enabled:       rule.GetEnabled(),
	}
}

func walletOverviewFromProto(resp *walletv1.GetWalletOverviewResponse) walletOverviewData {
	if resp == nil {
		return walletOverviewData{}
	}
	balances := make([]assetBalanceData, 0, len(resp.GetBalances()))
	for _, balance := range resp.GetBalances() {
		balances = append(balances, balanceFromProto(balance))
	}
	flags := resp.GetFeatureFlags()
	return walletOverviewData{
		Balances: balances,
		FeatureFlags: walletFeatureFlagsData{
			RechargeEnabled:        flags.GetRechargeEnabled(),
			DiamondExchangeEnabled: flags.GetDiamondExchangeEnabled(),
		},
	}
}

func walletTransactionFromProto(item *walletv1.WalletTransaction, profiles map[int64]*userv1.User) walletTransactionData {
	if item == nil {
		return walletTransactionData{}
	}
	data := walletTransactionData{
		EntryID:              item.GetEntryId(),
		TransactionID:        item.GetTransactionId(),
		BizType:              item.GetBizType(),
		AssetType:            item.GetAssetType(),
		AvailableDelta:       item.GetAvailableDelta(),
		FrozenDelta:          item.GetFrozenDelta(),
		AvailableAfter:       item.GetAvailableAfter(),
		FrozenAfter:          item.GetFrozenAfter(),
		CounterpartyUserID:   item.GetCounterpartyUserId(),
		RoomID:               item.GetRoomId(),
		CreatedAtMS:          item.GetCreatedAtMs(),
		TransferUSDMinor:     item.GetTransferUsdMinor(),
		TransferCurrencyCode: item.GetTransferCurrencyCode(),
	}
	if profile := profiles[item.GetCounterpartyUserId()]; profile != nil {
		data.CounterpartyDisplayUserID = profile.GetDisplayUserId()
		data.CounterpartyUsername = profile.GetUsername()
		data.CounterpartyAvatar = profile.GetAvatar()
	}
	return data
}

func giftWallFromProto(resp *walletv1.GetUserGiftWallResponse) giftWallData {
	if resp == nil {
		return giftWallData{}
	}
	items := make([]giftWallItemData, 0, len(resp.GetItems()))
	for _, item := range resp.GetItems() {
		items = append(items, giftWallItemFromProto(item))
	}
	return giftWallData{
		Items:             items,
		GiftKindCount:     resp.GetGiftKindCount(),
		GiftTotalCount:    resp.GetGiftTotalCount(),
		TotalValue:        resp.GetTotalValue(),
		TotalCoinValue:    resp.GetTotalCoinValue(),
		TotalDiamondValue: resp.GetTotalDiamondValue(),
		TotalGiftPoint:    resp.GetTotalGiftPoint(),
		TotalHeatValue:    resp.GetTotalHeatValue(),
	}
}

func giftWallItemFromProto(item *walletv1.GiftWallItem) giftWallItemData {
	if item == nil {
		return giftWallItemData{}
	}
	return giftWallItemData{
		GiftID:               item.GetGiftId(),
		GiftName:             item.GetGiftName(),
		ResourceID:           item.GetResourceId(),
		Resource:             resourcePointerFromProto(item.GetResource()),
		ResourceSnapshotJSON: item.GetResourceSnapshotJson(),
		GiftTypeCode:         item.GetGiftTypeCode(),
		PresentationJSON:     item.GetPresentationJson(),
		GiftCount:            item.GetGiftCount(),
		TotalValue:           item.GetTotalValue(),
		TotalCoinValue:       item.GetTotalCoinValue(),
		TotalDiamondValue:    item.GetTotalDiamondValue(),
		TotalGiftPoint:       item.GetTotalGiftPoint(),
		TotalHeatValue:       item.GetTotalHeatValue(),
		ChargeAssetType:      item.GetChargeAssetType(),
		LastPriceVersion:     item.GetLastPriceVersion(),
		FirstReceivedAtMS:    item.GetFirstReceivedAtMs(),
		LastReceivedAtMS:     item.GetLastReceivedAtMs(),
		SortOrder:            item.GetSortOrder(),
	}
}

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