package walletapi

import (
	"log/slog"
	"net/http"
	"strconv"
	"strings"
	"time"

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

const (
	pointDiamondWalletAssetType = "POINT_DIAMOND"
	pointDiamondPolicyType      = "POINT_DIAMOND"
)

// pointDiamondWalletCommandBody 只接收用户的稳定业务意图。积分换算、费率和币商到账数量
// 必须由 wallet-service 在锁账事务内读取已发布政策，HTTP 层不接受客户端自报结果。
type pointDiamondWalletCommandBody struct {
	CommandID                string            `json:"command_id"`
	CommandIDCamel           string            `json:"commandId"`
	PointAmount              flexibleJSONInt64 `json:"point_amount"`
	PointAmountCamel         flexibleJSONInt64 `json:"pointAmount"`
	GrossUSDMinor            flexibleJSONInt64 `json:"gross_usd_minor"`
	GrossUSDMinorCamel       flexibleJSONInt64 `json:"grossUsdMinor"`
	TargetDisplayUserID      string            `json:"target_display_user_id"`
	TargetDisplayUserIDCamel string            `json:"targetDisplayUserId"`

	WithdrawUSDTTRC20Address  string `json:"withdraw_usdt_trc20_address"`
	WithdrawUSDTTRC20AddressC string `json:"withdrawUsdtTrc20Address"`
	USDTTRC20Address          string `json:"usdt_trc20_address"`
	USDTTRC20AddressC         string `json:"usdtTrc20Address"`
	PaymentAddress            string `json:"payment_address"`
	PaymentAddressC           string `json:"paymentAddress"`
	Address                   string `json:"address"`
}

func (b pointDiamondWalletCommandBody) commandID() string {
	if value := strings.TrimSpace(b.CommandID); value != "" {
		return value
	}
	return strings.TrimSpace(b.CommandIDCamel)
}

func (b pointDiamondWalletCommandBody) pointAmount() int64 {
	if value := b.PointAmount.Int64(); value > 0 {
		return value
	}
	return b.PointAmountCamel.Int64()
}

func (b pointDiamondWalletCommandBody) grossUSDMinor() int64 {
	if value := b.GrossUSDMinor.Int64(); value > 0 {
		return value
	}
	return b.GrossUSDMinorCamel.Int64()
}

func (b pointDiamondWalletCommandBody) targetDisplayUserID() string {
	if value := strings.TrimSpace(b.TargetDisplayUserID); value != "" {
		return value
	}
	return strings.TrimSpace(b.TargetDisplayUserIDCamel)
}

func (b pointDiamondWalletCommandBody) usdtTRC20Address() string {
	return salaryWalletWithdrawAddressBody{
		WithdrawUSDTTRC20Address:  b.WithdrawUSDTTRC20Address,
		WithdrawUSDTTRC20AddressC: b.WithdrawUSDTTRC20AddressC,
		USDTTRC20Address:          b.USDTTRC20Address,
		USDTTRC20AddressC:         b.USDTTRC20AddressC,
		PaymentAddress:            b.PaymentAddress,
		PaymentAddressC:           b.PaymentAddressC,
		Address:                   b.Address,
	}.usdtTRC20Address()
}

type pointDiamondBalanceData struct {
	AssetType       string `json:"asset_type"`
	AvailablePoints string `json:"available_points"`
	AvailableAmount string `json:"available_amount"`
	FrozenAmount    string `json:"frozen_amount"`
	Version         string `json:"version"`
}

type pointDiamondPolicyData struct {
	PointDiamondsPerUSD               string `json:"point_diamonds_per_usd"`
	CoinsPerUSD                       string `json:"coins_per_usd"`
	AgencyPointShareBPS               string `json:"agency_point_share_bps"`
	MinimumWithdrawUSDMinor           string `json:"minimum_withdraw_usd_minor"`
	CoinSellerMinimumWithdrawUSDMinor string `json:"coin_seller_minimum_withdraw_usd_minor"`
	PlatformMinimumWithdrawUSDMinor   string `json:"platform_minimum_withdraw_usd_minor"`
	DiamondExchangeEnabled            bool   `json:"diamond_exchange_enabled"`
	WithdrawFeeBPS                    string `json:"withdraw_fee_bps"`
	PolicyID                          string `json:"policy_id"`
	PolicyVersion                     string `json:"policy_version"`
	PolicyInstanceCode                string `json:"policy_instance_code"`
}

type pointDiamondActionsData struct {
	ExchangeToCoins      bool `json:"exchange_to_coins"`
	Withdraw             bool `json:"withdraw"`
	TransferToCoinSeller bool `json:"transfer_to_coin_seller"`
}

type pointDiamondActionAvailabilityData struct {
	Allowed        bool   `json:"allowed"`
	LimitPeriod    string `json:"limit_period"`
	LimitCount     string `json:"limit_count"`
	UsedCount      string `json:"used_count"`
	RemainingCount string `json:"remaining_count"`
	AllowedDays    string `json:"allowed_days"`
	BlockReason    string `json:"block_reason"`
}

type pointDiamondCoinSellerData struct {
	UserID              string   `json:"user_id"`
	DisplayUserID       string   `json:"display_user_id"`
	Nickname            string   `json:"nickname"`
	Avatar              string   `json:"avatar"`
	SortOrder           int32    `json:"sort_order"`
	ServiceRegionIDs    []int64  `json:"service_region_ids"`
	ServiceCountryCodes []string `json:"service_country_codes"`
}

func (h *Handler) getPointDiamondWalletOverview(writer http.ResponseWriter, request *http.Request) {
	pointWallet, profile, config, ok := h.resolvePointDiamondWalletContext(writer, request)
	if !ok {
		return
	}
	balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
		UserId: auth.UserIDFromContext(request.Context()), AssetTypes: []string{pointDiamondWalletAssetType},
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	balance := pointDiamondBalanceFromProto(firstBalance(balanceResp.GetBalances()))
	policy := pointDiamondPolicyFromProto(config)
	enabled := config.GetFound() && config.GetPolicyType() == pointDiamondPolicyType
	sellers := []pointDiamondCoinSellerData{}
	if enabled {
		// POINT_DIAMOND 只能展示 wallet 按 App 与用户国家过滤后的币商白名单；通用币商目录
		// 不是资金资格事实，否则页面会展示最终被 wallet 拒绝的币商。
		sellerResp, err := pointWallet.ListPointWithdrawalCoinSellers(request.Context(), &walletv1.ListPointWithdrawalCoinSellersRequest{
			RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
			RegionId: profile.GetRegionId(), CountryCode: pointWalletCountryCode(profile),
		})
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return
		}
		legacySellers, err := h.pointWalletCoinSellerData(request, sellerResp.GetSellers())
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return
		}
		sellers = make([]pointDiamondCoinSellerData, 0, len(legacySellers))
		for _, seller := range legacySellers {
			sellers = append(sellers, pointDiamondCoinSellerData{
				UserID: seller.UserID, DisplayUserID: seller.DisplayUserID, Nickname: seller.Nickname,
				Avatar: seller.Avatar, SortOrder: seller.SortOrder,
				ServiceRegionIDs:    append([]int64(nil), seller.ServiceRegionIDs...),
				ServiceCountryCodes: append([]string(nil), seller.ServiceCountryCodes...),
			})
		}
	}
	availabilityEvaluated := config.GetAvailabilityEvaluated()
	diamondExchangeEnabled := pointDiamondExchangeEnabled(config)
	actions := pointDiamondActionsData{
		ExchangeToCoins:      enabled && diamondExchangeEnabled,
		Withdraw:             enabled && availabilityEvaluated && config.GetPlatformAvailability().GetAllowed(),
		TransferToCoinSeller: enabled && availabilityEvaluated && config.GetCoinSellerAvailability().GetAllowed() && len(sellers) > 0,
	}
	channels := []string{}
	if enabled {
		if actions.ExchangeToCoins {
			channels = append(channels, "COIN")
		}
		if actions.Withdraw {
			channels = append(channels, "PLATFORM_USDT")
		}
		if actions.TransferToCoinSeller {
			channels = append(channels, "COIN_SELLER_USDT")
		}
	}
	// 根级兼容字段和结构化字段来自同一份 wallet 快照，避免不同客户端各自推导比例后产生口径分叉。
	httpkit.WriteOK(writer, request, map[string]any{
		"enabled": enabled, "policy_type": pointDiamondPolicyType, "asset_type": pointDiamondWalletAssetType,
		"balance": balance, "policy": policy, "actions": actions, "channels": channels,
		"coin_sellers":           sellers,
		"availability_evaluated": availabilityEvaluated,
		"action_limits": map[string]pointDiamondActionAvailabilityData{
			"withdraw":                pointDiamondActionAvailabilityFromProto(config.GetPlatformAvailability()),
			"transfer_to_coin_seller": pointDiamondActionAvailabilityFromProto(config.GetCoinSellerAvailability()),
		},
		"available_points": balance.AvailablePoints, "available_amount": balance.AvailableAmount,
		"point_diamonds_per_usd": policy.PointDiamondsPerUSD, "coins_per_usd": policy.CoinsPerUSD,
		"agency_point_share_bps":                 policy.AgencyPointShareBPS,
		"minimum_withdraw_usd_minor":             policy.MinimumWithdrawUSDMinor,
		"coin_seller_minimum_withdraw_usd_minor": policy.CoinSellerMinimumWithdrawUSDMinor,
		"platform_minimum_withdraw_usd_minor":    policy.PlatformMinimumWithdrawUSDMinor,
		"diamond_exchange_enabled":               policy.DiamondExchangeEnabled,
		"withdraw_fee_bps":                       policy.WithdrawFeeBPS,
		"withdraw_address":                       salaryWithdrawAddressFromProto(profile),
	})
}

func (h *Handler) getPointDiamondWalletHistory(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, 20)
	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: pointDiamondWalletAssetType,
		Page: page, PageSize: pageSize, StartAtMs: startAtMS, EndAtMs: endAtMS,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]map[string]any, 0, len(resp.GetTransactions()))
	for _, item := range resp.GetTransactions() {
		if item == nil {
			continue
		}
		components := make([]map[string]string, 0, 2)
		if item.GetHostBaseAmount() > 0 {
			components = append(components, map[string]string{
				"entry_role": "host_gift_income", "amount": strconv.FormatInt(item.GetHostBaseAmount(), 10),
			})
		}
		if item.GetAgencyAmount() > 0 {
			components = append(components, map[string]string{
				"entry_role": "agency_gift_share", "amount": strconv.FormatInt(item.GetAgencyAmount(), 10),
			})
		}
		// 钱包流水可能超过 JavaScript 安全整数；所有 ID 与金额均返回十进制字符串。
		items = append(items, map[string]any{
			"entry_id": strconv.FormatInt(item.GetEntryId(), 10), "transaction_id": item.GetTransactionId(),
			"biz_type": item.GetBizType(), "entry_role": item.GetEntryRole(), "asset_type": item.GetAssetType(),
			"available_delta": strconv.FormatInt(item.GetAvailableDelta(), 10), "frozen_delta": strconv.FormatInt(item.GetFrozenDelta(), 10),
			"available_after": strconv.FormatInt(item.GetAvailableAfter(), 10), "frozen_after": strconv.FormatInt(item.GetFrozenAfter(), 10),
			"counterparty_user_id": strconv.FormatInt(item.GetCounterpartyUserId(), 10),
			"room_id":              item.GetRoomId(), "created_at_ms": strconv.FormatInt(item.GetCreatedAtMs(), 10),
			"host_base_amount":   strconv.FormatInt(item.GetHostBaseAmount(), 10),
			"agency_amount":      strconv.FormatInt(item.GetAgencyAmount(), 10),
			"policy_id":          strconv.FormatUint(item.GetPolicyId(), 10),
			"policy_version":     strconv.FormatUint(item.GetPolicyVersion(), 10),
			"ratio_point_amount": strconv.FormatInt(item.GetRatioPointAmount(), 10),
			"ratio_coin_amount":  strconv.FormatInt(item.GetRatioCoinAmount(), 10),
			"gross_usd_minor":    strconv.FormatInt(item.GetGrossUsdMinor(), 10),
			"fee_usd_minor":      strconv.FormatInt(item.GetFeeUsdMinor(), 10),
			"net_usd_minor":      strconv.FormatInt(item.GetNetUsdMinor(), 10),
			"withdraw_fee_bps":   strconv.FormatInt(item.GetWithdrawFeeBps(), 10),
			"components":         components,
		})
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"asset_type": pointDiamondWalletAssetType, "items": items, "total": strconv.FormatInt(resp.GetTotal(), 10),
		"page": page, "page_size": pageSize,
	})
}

func (h *Handler) exchangePointDiamondWalletToCoins(writer http.ResponseWriter, request *http.Request) {
	pointWallet, profile, ok := h.resolvePointDiamondWalletProfile(writer, request)
	if !ok {
		return
	}
	var body pointDiamondWalletCommandBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID, pointAmount := body.commandID(), body.pointAmount()
	if commandID == "" || pointAmount <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	receipt, err := pointWallet.ExchangePointToCoin(request.Context(), &walletv1.ExchangePointToCoinRequest{
		CommandId: commandID, AppCode: appcode.FromContext(request.Context()), UserId: auth.UserIDFromContext(request.Context()),
		PointAmount: pointAmount, RegionId: profile.GetRegionId(), NowMs: time.Now().UTC().UnixMilli(), SourceAssetType: pointDiamondWalletAssetType,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	balanceAfter := map[string]string{
		"asset_type":       pointDiamondWalletAssetType,
		"available_amount": strconv.FormatInt(receipt.GetPointBalanceAfter(), 10),
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"transaction_id": receipt.GetTransactionId(), "asset_type": pointDiamondWalletAssetType,
		"point_amount": strconv.FormatInt(receipt.GetPointAmount(), 10), "coin_amount": strconv.FormatInt(receipt.GetCoinAmount(), 10),
		"point_diamond_balance_after": strconv.FormatInt(receipt.GetPointBalanceAfter(), 10),
		"coin_balance_after":          strconv.FormatInt(receipt.GetCoinBalanceAfter(), 10), "balance_after": balanceAfter,
		"ratio_point_amount": strconv.FormatInt(receipt.GetRatioPointAmount(), 10),
		"ratio_coin_amount":  strconv.FormatInt(receipt.GetRatioCoinAmount(), 10),
		"policy_id":          strconv.FormatUint(receipt.GetPolicyId(), 10),
		"policy_version":     strconv.FormatUint(receipt.GetPolicyVersion(), 10),
	})
}

func (h *Handler) transferPointDiamondWalletToCoinSeller(writer http.ResponseWriter, request *http.Request) {
	pointWallet, profile, ok := h.resolvePointDiamondWalletProfile(writer, request)
	if !ok {
		return
	}
	if h.userIdentityClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body pointDiamondWalletCommandBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID, grossUSDMinor, displayUserID := body.commandID(), body.grossUSDMinor(), body.targetDisplayUserID()
	if commandID == "" || grossUSDMinor <= 0 || displayUserID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	// 展示号只解析成稳定 user_id；币商 active/国家范围由 wallet 事务中的白名单再次校验。
	// 这样首次成功但响应丢失后，即使币商被停用或用户换区，同 command_id 仍能先命中原回执。
	sellerUserID, ok := h.resolveCoinSellerTransferTargetUserID(writer, request, displayUserID)
	if !ok {
		return
	}
	receipt, err := pointWallet.TransferPointToCoinSeller(request.Context(), &walletv1.TransferPointToCoinSellerRequest{
		CommandId: commandID, AppCode: appcode.FromContext(request.Context()), SourceUserId: auth.UserIDFromContext(request.Context()),
		SellerUserId: sellerUserID, SourceCountryCode: pointWalletCountryCode(profile), Reason: "point diamond transfer to coin seller",
		RegionId: profile.GetRegionId(), SourceAssetType: pointDiamondWalletAssetType, GrossUsdMinor: grossUSDMinor,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"transaction_id": receipt.GetTransactionId(), "asset_type": pointDiamondWalletAssetType,
		"target_display_user_id": displayUserID, "seller_user_id": strconv.FormatInt(sellerUserID, 10),
		"gross_usd_minor": strconv.FormatInt(receipt.GetGrossUsdMinor(), 10), "fee_usd_minor": strconv.FormatInt(receipt.GetFeeUsdMinor(), 10),
		"net_usd_minor": strconv.FormatInt(receipt.GetNetUsdMinor(), 10), "point_amount": strconv.FormatInt(receipt.GetPointAmount(), 10),
		"coin_amount":                 strconv.FormatInt(receipt.GetSellerCoinAmount(), 10),
		"point_diamond_balance_after": strconv.FormatInt(receipt.GetSourcePointBalanceAfter(), 10),
		"seller_balance_after":        strconv.FormatInt(receipt.GetSellerBalanceAfter(), 10),
		"balance_after":               map[string]string{"asset_type": pointDiamondWalletAssetType, "available_amount": strconv.FormatInt(receipt.GetSourcePointBalanceAfter(), 10)},
		"ratio_point_amount":          strconv.FormatInt(receipt.GetRatioPointAmount(), 10),
		"ratio_coin_amount":           strconv.FormatInt(receipt.GetRatioSellerCoinAmount(), 10),
		"withdraw_fee_bps":            strconv.FormatInt(int64(receipt.GetWithdrawFeeBps()), 10),
		"policy_id":                   strconv.FormatUint(receipt.GetPolicyId(), 10),
		"policy_version":              strconv.FormatUint(receipt.GetPolicyVersion(), 10),
	})
}

func (h *Handler) withdrawPointDiamondWallet(writer http.ResponseWriter, request *http.Request) {
	if h.withdrawalWriter == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	pointWallet, profile, ok := h.resolvePointDiamondWalletProfile(writer, request)
	if !ok {
		return
	}
	var body pointDiamondWalletCommandBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID, grossUSDMinor := body.commandID(), body.grossUSDMinor()
	if commandID == "" || grossUSDMinor <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	userID := auth.UserIDFromContext(request.Context())
	address, ok := h.resolveSalaryWalletWithdrawAddress(writer, request, userID, body.usdtTRC20Address())
	if !ok {
		return
	}
	withdrawalRef := pointWithdrawalReference(commandID)
	freezeResp, err := pointWallet.FreezePointWithdrawal(request.Context(), &walletv1.FreezePointWithdrawalRequest{
		CommandId: commandID, UserId: userID, AssetType: pointDiamondWalletAssetType,
		Reason: "point diamond withdrawal submitted", AppCode: appcode.FromContext(request.Context()),
		WithdrawalRef: withdrawalRef, RegionId: profile.GetRegionId(), GrossUsdMinor: grossUSDMinor,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	// 后续申请和补偿只使用 wallet 首次冻结回执；同 command_id 重放不能混入本次页面预览政策。
	frozenGross := freezeResp.GetGrossPointAmount()
	frozenFee := freezeResp.GetFeePointAmount()
	frozenNet := freezeResp.GetNetPointAmount()
	frozenPointsPerUSD := freezeResp.GetPointsPerUsd()
	frozenFeeBPS := freezeResp.GetFeeBps()
	policyInstance := strings.TrimSpace(freezeResp.GetPolicyInstanceCode())
	nowMS := time.Now().UTC().UnixMilli()
	application, err := h.withdrawalWriter.CreateApplication(request.Context(), financewithdrawal.CreateApplicationCommand{
		AppCode: appcode.FromContext(request.Context()), UserID: userID, SalaryAssetType: pointDiamondWalletAssetType,
		WithdrawAmount: formatUSDMinor(freezeResp.GetNetUsdMinor()), WithdrawAmountMinor: frozenGross,
		PointFeeAmount: frozenFee, PointNetAmount: frozenNet, PointsPerUSD: frozenPointsPerUSD, PointFeeBPS: frozenFeeBPS,
		PointPolicyInstance: policyInstance, WithdrawMethod: financewithdrawal.MethodUSDTTRC20, WithdrawAddress: address,
		FreezeCommandID: commandID, FreezeTransactionID: freezeResp.GetTransactionId(), CreatedAtMS: nowMS,
	})
	if err != nil {
		if financewithdrawal.IsApplicationOutcomeUnknown(err) {
			// 申请是否落库未知时保留冻结；相同 command_id 重试会命中原回执并收敛，不能冒险释放真实余额。
			slog.Error("point_diamond_withdrawal_application_outcome_unknown", "user_id", userID, "app_code", appcode.FromContext(request.Context()), "command_id", commandID, "error", err)
			httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
			return
		}
		rollbackPointWithdrawalFreezeForAsset(request, pointWallet, pointDiamondWalletAssetType, commandID, freezeResp.GetTransactionId(), userID, frozenGross, frozenFee, frozenNet, frozenPointsPerUSD, frozenFeeBPS)
		httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
		return
	}
	if h.withdrawalNotifier != nil {
		// 通知不是资金事实；失败只告警，不能回滚已冻结余额或让用户二次提交。
		if err := h.withdrawalNotifier.NotifyApplicationCreated(request.Context(), application); err != nil {
			slog.Warn("point_diamond_withdrawal_notify_failed", "application_id", application.ID, "user_id", userID, "error", err)
		}
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"application_id": strconv.FormatInt(application.ID, 10), "status": application.Status,
		"operations_status": application.OperationsStatus, "asset_type": pointDiamondWalletAssetType,
		"gross_usd_minor": strconv.FormatInt(freezeResp.GetGrossUsdMinor(), 10),
		"fee_usd_minor":   strconv.FormatInt(freezeResp.GetFeeUsdMinor(), 10), "net_usd_minor": strconv.FormatInt(freezeResp.GetNetUsdMinor(), 10),
		"point_amount": strconv.FormatInt(frozenGross, 10), "fee_point_amount": strconv.FormatInt(frozenFee, 10),
		"net_point_amount": strconv.FormatInt(frozenNet, 10), "point_diamonds_per_usd": strconv.FormatInt(frozenPointsPerUSD, 10),
		"withdraw_fee_bps": strconv.FormatInt(int64(frozenFeeBPS), 10), "policy_instance_code": policyInstance,
		"policy_id": strconv.FormatUint(freezeResp.GetPolicyId(), 10), "policy_version": strconv.FormatUint(freezeResp.GetPolicyVersion(), 10),
		"coins_per_usd":               strconv.FormatInt(freezeResp.GetCoinsPerUsd(), 10),
		"point_diamond_balance_after": strconv.FormatInt(freezeResp.GetBalance().GetAvailableAmount(), 10),
		"balance_after":               pointDiamondBalanceFromProto(freezeResp.GetBalance()),
		"withdraw_method":             application.WithdrawMethod, "withdraw_address": application.WithdrawAddress,
		"freeze_transaction_id": freezeResp.GetTransactionId(), "created_at_ms": strconv.FormatInt(application.CreatedAtMS, 10),
	})
}

func (h *Handler) resolvePointDiamondWalletContext(writer http.ResponseWriter, request *http.Request) (pointWithdrawalGatewayClient, *userv1.User, *walletv1.GetPointWithdrawalConfigResponse, bool) {
	pointWallet, profile, ok := h.resolvePointDiamondWalletProfile(writer, request)
	if !ok {
		return nil, nil, nil, false
	}
	config, err := pointWallet.GetPointWithdrawalConfig(request.Context(), &walletv1.GetPointWithdrawalConfigRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()), AppCode: appcode.FromContext(request.Context()),
		RegionId: profile.GetRegionId(), NowMs: time.Now().UTC().UnixMilli(), AssetType: pointDiamondWalletAssetType,
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return nil, nil, nil, false
	}
	return pointWallet, profile, config, true
}

func pointDiamondActionAvailabilityFromProto(value *walletv1.PointWithdrawalActionAvailability) pointDiamondActionAvailabilityData {
	if value == nil {
		return pointDiamondActionAvailabilityData{}
	}
	return pointDiamondActionAvailabilityData{
		Allowed: value.GetAllowed(), LimitPeriod: value.GetLimitPeriod(),
		LimitCount: strconv.FormatInt(value.GetLimitCount(), 10), UsedCount: strconv.FormatInt(value.GetUsedCount(), 10),
		RemainingCount: strconv.FormatInt(value.GetRemainingCount(), 10), AllowedDays: value.GetAllowedDays(), BlockReason: value.GetBlockReason(),
	}
}

// resolvePointDiamondWalletProfile 只解析动作所需的实时用户区域。当前政策、最低额和费率都由
// wallet 事务内决定；若先查当前政策，后台改政策后会在到达幂等账本前错误拦住成功命令重试。
func (h *Handler) resolvePointDiamondWalletProfile(writer http.ResponseWriter, request *http.Request) (pointWithdrawalGatewayClient, *userv1.User, bool) {
	pointWallet, ok := h.walletClient.(pointWithdrawalGatewayClient)
	if !ok || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return nil, nil, false
	}
	profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return nil, nil, false
	}
	profile := profileResp.GetUser()
	if profile == nil {
		httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "user not found")
		return nil, nil, false
	}
	return pointWallet, profile, true
}

func pointDiamondBalanceFromProto(balance *walletv1.AssetBalance) pointDiamondBalanceData {
	if balance == nil {
		return pointDiamondBalanceData{AssetType: pointDiamondWalletAssetType, AvailablePoints: "0", AvailableAmount: "0", FrozenAmount: "0", Version: "0"}
	}
	available := strconv.FormatInt(balance.GetAvailableAmount(), 10)
	return pointDiamondBalanceData{
		AssetType: balance.GetAssetType(), AvailablePoints: available, AvailableAmount: available,
		FrozenAmount: strconv.FormatInt(balance.GetFrozenAmount(), 10), Version: strconv.FormatInt(balance.GetVersion(), 10),
	}
}

func pointDiamondPolicyFromProto(config *walletv1.GetPointWithdrawalConfigResponse) pointDiamondPolicyData {
	if config == nil {
		return pointDiamondPolicyData{}
	}
	return pointDiamondPolicyData{
		PointDiamondsPerUSD: strconv.FormatInt(config.GetPointsPerUsd(), 10), CoinsPerUSD: strconv.FormatInt(config.GetCoinsPerUsd(), 10),
		AgencyPointShareBPS:               strconv.FormatInt(int64(config.GetAgencyPointShareBps()), 10),
		MinimumWithdrawUSDMinor:           strconv.FormatInt(config.GetMinimumWithdrawUsdMinor(), 10),
		CoinSellerMinimumWithdrawUSDMinor: strconv.FormatInt(config.GetCoinSellerMinimumWithdrawUsdMinor(), 10),
		PlatformMinimumWithdrawUSDMinor:   strconv.FormatInt(config.GetPlatformMinimumWithdrawUsdMinor(), 10),
		DiamondExchangeEnabled:            pointDiamondExchangeEnabled(config),
		WithdrawFeeBPS:                    strconv.FormatInt(int64(config.GetFeeBps()), 10),
		PolicyID:                          strconv.FormatUint(config.GetPolicyId(), 10), PolicyVersion: strconv.FormatUint(config.GetPolicyVersion(), 10),
		PolicyInstanceCode: config.GetPolicyInstanceCode(),
	}
}

func pointDiamondExchangeEnabled(config *walletv1.GetPointWithdrawalConfigResponse) bool {
	if config == nil {
		return false
	}
	// optional 字段缺失只会来自旧 wallet-service；兼容窗口保持原有开启行为。
	// 新服务无论 true/false 都会显式设置指针，因此运营关闭不会被该回退覆盖。
	if config.DiamondExchangeEnabled == nil {
		return true
	}
	return config.GetDiamondExchangeEnabled()
}
