package payment

import (
	"context"
	"database/sql"
	"errors"
	"math"
	"strconv"
	"strings"
	"time"

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

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const usdtMicroUnit int64 = 1_000_000

type Handler struct {
	wallet             walletclient.Client
	userDB             *sql.DB
	walletDB           *sql.DB
	store              *repository.Store
	audit              shared.OperationLogger
	exchangeRates      exchangeRateClient
	moneyRegionSources []MoneyRegionSource
	billSources        map[string]RechargeBillSource
}

type HandlerOption func(*Handler)

func WithMoneyRegionSources(sources ...MoneyRegionSource) HandlerOption {
	return func(h *Handler) {
		for _, source := range sources {
			if source != nil {
				h.moneyRegionSources = append(h.moneyRegionSources, source)
			}
		}
	}
}

// WithRechargeBillSources 注册 legacy App（Yumi 等）的外部账单源；命中 app_code 的充值明细和汇总直接读外部源。
func WithRechargeBillSources(sources ...RechargeBillSource) HandlerOption {
	return func(h *Handler) {
		for _, source := range sources {
			if source == nil {
				continue
			}
			if h.billSources == nil {
				h.billSources = map[string]RechargeBillSource{}
			}
			h.billSources[appctx.Normalize(source.AppCode())] = source
		}
	}
}

// New 组装支付后台 handler；wallet 负责账单和商品事实，userDB 只补后台列表需要的用户展示资料。
func New(wallet walletclient.Client, userDB *sql.DB, walletDB *sql.DB, store *repository.Store, audit shared.OperationLogger, options ...HandlerOption) *Handler {
	handler := &Handler{wallet: wallet, userDB: userDB, walletDB: walletDB, store: store, audit: audit, exchangeRates: newDefaultExchangeRateClient()}
	// Handler option 让 legacy likei 区域源保持可选；本地/测试未配置线上 Mongo 时，payment 仍只读本地 hyapp_user 主数据。
	for _, option := range options {
		if option != nil {
			option(handler)
		}
	}
	return handler
}

func (h *Handler) ListRechargeBills(c *gin.Context) {
	options := shared.ListOptions(c)
	appCode := appctx.FromContext(c.Request.Context())
	rechargeType := strings.TrimSpace(firstQuery(c, "recharge_type", "rechargeType"))
	userFilter := shared.UserIdentityFilterFromQuery(c, "")
	userID, userMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, userFilter, "user_id 参数不正确", "user_id", "userId")
	if !ok {
		return
	}
	sellerFilter := shared.UserIdentityFilterFromQuery(c, "seller")
	sellerUserID, sellerMatched, ok := h.resolveRechargeBillUserFilter(c, appCode, sellerFilter, "seller_user_id 参数不正确", "seller_user_id", "sellerUserId")
	if !ok {
		return
	}
	if (!userFilter.IsEmpty() && !userMatched) || (!sellerFilter.IsEmpty() && !sellerMatched) {
		response.OK(c, response.Page{Items: []rechargeBillDTO{}, Page: options.Page, PageSize: options.PageSize, Total: 0})
		return
	}
	source, hasSource := h.billSources[appCode]
	normalizedType := normalizedRechargeType(rechargeType)
	if normalizedType == "" || isFinanceCoinSellerRechargeType(normalizedType) {
		query := financeCoinSellerRechargeQuery{
			RechargeType:      rechargeType,
			Status:            options.Status,
			PaidState:         strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
			Keyword:           options.Keyword,
			RegionID:          queryInt64(c, "region_id", "regionId"),
			StartAtMS:         queryInt64(c, "start_at_ms", "startAtMs"),
			EndAtMS:           queryInt64(c, "end_at_ms", "endAtMs"),
			TargetUserID:      sellerUserID,
			HasUserOnlyFilter: userID > 0 && sellerUserID == 0,
			Page:              options.Page,
			PageSize:          options.PageSize,
			RequestID:         middleware.CurrentRequestID(c),
			UserID:            userID,
		}
		var items []rechargeBillDTO
		var total int64
		var err error
		if isFinanceCoinSellerRechargeType(normalizedType) {
			items, total, err = h.listFinanceCoinSellerRechargeBills(c.Request.Context(), appCode, query)
		} else {
			var legacySource RechargeBillSource
			if hasSource {
				legacySource = source
			}
			items, total, err = h.listFinanceAllRechargeBills(c.Request.Context(), appCode, legacySource, query)
		}
		if err != nil {
			response.ServerError(c, "获取账单列表失败")
			return
		}
		if err := h.fillRechargeBillUsers(c.Request.Context(), appCode, items); err != nil {
			response.ServerError(c, "获取账单用户资料失败")
			return
		}
		response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total})
		return
	}
	if hasSource {
		h.listLegacyRechargeBills(c, source)
		return
	}
	resp, err := h.wallet.ListRechargeBills(c.Request.Context(), &walletv1.ListRechargeBillsRequest{
		RequestId:    middleware.CurrentRequestID(c),
		AppCode:      appCode,
		UserId:       userID,
		SellerUserId: sellerUserID,
		RegionId:     queryInt64(c, "region_id", "regionId"),
		RechargeType: rechargeType,
		PaidState:    strings.TrimSpace(firstQuery(c, "paid_state", "paidState")),
		Status:       options.Status,
		Keyword:      options.Keyword,
		StartAtMs:    queryInt64(c, "start_at_ms", "startAtMs"),
		EndAtMs:      queryInt64(c, "end_at_ms", "endAtMs"),
		Page:         int32(options.Page),
		PageSize:     int32(options.PageSize),
	})
	if err != nil {
		response.ServerError(c, "获取账单列表失败")
		return
	}

	items := make([]rechargeBillDTO, 0, len(resp.GetBills()))
	for _, item := range resp.GetBills() {
		items = append(items, rechargeBillFromProto(item))
	}
	// 账单事实仍由 wallet-service 返回；这里只按本页出现的付款用户和币商用户批量补展示资料，避免列表渲染时逐行查用户。
	if err := h.fillRechargeBillUsers(c.Request.Context(), appCode, items); err != nil {
		response.ServerError(c, "获取账单用户资料失败")
		return
	}
	response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}

func (h *Handler) resolveRechargeBillUserFilter(c *gin.Context, appCode string, filter shared.UserIdentityFilter, invalidMessage string, keys ...string) (int64, bool, bool) {
	keyword := strings.TrimSpace(firstQuery(c, keys...))
	if filter.IsEmpty() && keyword == "" {
		return 0, false, true
	}
	if !filter.IsEmpty() {
		userID, matched, err := shared.ResolveUserIdentityFilter(c.Request.Context(), h.userDB, appCode, filter, time.Now().UnixMilli())
		if err != nil {
			response.ServerError(c, "查询账单用户信息失败")
			return 0, false, false
		}
		return userID, matched, true
	}
	// 充值账单事实在 wallet-service，筛选字段只能传内部 user_id；后台先把长 ID、默认短号、当前展示号和 active 靓号解析成同一个稳定 ID。
	userID, _, err := shared.ResolveExactUserIDOrNumericFallback(c.Request.Context(), h.userDB, appCode, keyword, time.Now().UnixMilli())
	if err != nil {
		if errors.Is(err, shared.ErrInvalidUserIdentityFilter) {
			response.BadRequest(c, invalidMessage)
			return 0, false, false
		}
		response.ServerError(c, "查询账单用户信息失败")
		return 0, false, false
	}
	return userID, true, true
}

// fillRechargeBillUsers 将用户资料写回当前页账单 DTO；缺失资料会在 DTO 层保留长 ID，避免影响账单事实展示。
func (h *Handler) fillRechargeBillUsers(ctx context.Context, appCode string, items []rechargeBillDTO) error {
	userIDs := collectRechargeBillUserIDs(items)
	if len(userIDs) == 0 {
		return nil
	}
	profiles, err := h.loadRechargeBillUsers(ctx, appCode, userIDs)
	if err != nil {
		return err
	}
	for index := range items {
		items[index].User = rechargeBillUserOrFallback(profiles[items[index].UserID], items[index].UserID)
		items[index].Seller = rechargeBillUserOrFallback(profiles[items[index].SellerUserID], items[index].SellerUserID)
	}
	return nil
}

// loadRechargeBillUsers 按 app_code 和用户 ID 批量读取展示资料；国家只服务后台账单展示，账单金额和区域事实仍以 wallet-service 为准。
func (h *Handler) loadRechargeBillUsers(ctx context.Context, appCode string, userIDs []int64) (map[int64]rechargeBillUserDTO, error) {
	if h == nil || h.userDB == nil {
		return nil, errors.New("user mysql is not configured")
	}
	// 本查询只读取 users/countries 表里的展示字段，按照 app_code 和 user_id 精确命中；区域和账单筛选仍以 wallet-service 返回为准。
	rows, err := h.userDB.QueryContext(ctx, `
		SELECT u.user_id, COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''), COALESCE(u.avatar, ''),
		       COALESCE(u.country, ''), COALESCE(c.country_name, ''), COALESCE(c.country_display_name, '')
		FROM users u
		LEFT JOIN countries c ON c.app_code = u.app_code AND c.country_code = u.country
		WHERE u.app_code = ? AND u.user_id IN (`+placeholders(len(userIDs))+`)`,
		append([]any{appCode}, int64Args(userIDs)...)...,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	profiles := make(map[int64]rechargeBillUserDTO, len(userIDs))
	for rows.Next() {
		var profile rechargeBillUserDTO
		var userID int64
		if err := rows.Scan(
			&userID,
			&profile.DisplayUserID,
			&profile.Username,
			&profile.Avatar,
			&profile.CountryCode,
			&profile.CountryName,
			&profile.CountryDisplayName,
		); err != nil {
			return nil, err
		}
		profile.UserID = strconv.FormatInt(userID, 10)
		profiles[userID] = profile
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return profiles, nil
}

func (h *Handler) ListRechargeProducts(c *gin.Context) {
	options := shared.ListOptions(c)
	resp, err := h.wallet.ListAdminRechargeProducts(c.Request.Context(), &walletv1.ListAdminRechargeProductsRequest{
		RequestId:    middleware.CurrentRequestID(c),
		AppCode:      appctx.FromContext(c.Request.Context()),
		Status:       strings.TrimSpace(firstQuery(c, "status")),
		Platform:     strings.TrimSpace(firstQuery(c, "platform")),
		RegionId:     queryInt64(c, "region_id", "regionId"),
		AudienceType: strings.TrimSpace(firstQuery(c, "audience_type", "audienceType")),
		Keyword:      options.Keyword,
		Page:         int32(options.Page),
		PageSize:     int32(options.PageSize),
	})
	if err != nil {
		writeWalletError(c, err, "获取内购配置失败")
		return
	}
	items := make([]rechargeProductDTO, 0, len(resp.GetProducts()))
	for _, item := range resp.GetProducts() {
		items = append(items, rechargeProductFromProto(item))
	}
	response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}

func (h *Handler) CreateRechargeProduct(c *gin.Context) {
	var request rechargeProductRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "内购配置参数不正确")
		return
	}
	amountMicro, err := parseUSDTMicro(request.AmountUSDT, request.AmountUSDTMicro)
	if err != nil {
		response.BadRequest(c, "金额USDT不正确")
		return
	}
	req := &walletv1.CreateRechargeProductRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		AmountMicro:    amountMicro,
		CoinAmount:     request.CoinAmount,
		ProductName:    strings.TrimSpace(request.ProductName),
		Description:    strings.TrimSpace(request.Description),
		Platform:       strings.TrimSpace(request.Platform),
		AudienceType:   strings.TrimSpace(request.AudienceType),
		RegionIds:      request.RegionIDs,
		Enabled:        request.Enabled,
		OperatorUserId: actorID(c),
	}
	if request.AppVisible != nil {
		req.AppVisible = request.AppVisible
	}
	resp, err := h.wallet.CreateRechargeProduct(c.Request.Context(), req)
	if err != nil {
		writeWalletError(c, err, "创建内购配置失败")
		return
	}
	item := rechargeProductFromProto(resp.GetProduct())
	shared.OperationLogWithResourceID(c, h.audit, "create-recharge-product", "wallet_recharge_products", strconv.FormatInt(item.ProductID, 10), "success", item.ProductCode)
	response.Created(c, item)
}

func (h *Handler) UpdateRechargeProduct(c *gin.Context) {
	productID, ok := productID(c)
	if !ok {
		return
	}
	var request rechargeProductRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "内购配置参数不正确")
		return
	}
	amountMicro, err := parseUSDTMicro(request.AmountUSDT, request.AmountUSDTMicro)
	if err != nil {
		response.BadRequest(c, "金额USDT不正确")
		return
	}
	req := &walletv1.UpdateRechargeProductRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		ProductId:      productID,
		AmountMicro:    amountMicro,
		CoinAmount:     request.CoinAmount,
		ProductName:    strings.TrimSpace(request.ProductName),
		Description:    strings.TrimSpace(request.Description),
		Platform:       strings.TrimSpace(request.Platform),
		AudienceType:   strings.TrimSpace(request.AudienceType),
		RegionIds:      request.RegionIDs,
		Enabled:        request.Enabled,
		OperatorUserId: actorID(c),
	}
	if request.AppVisible != nil {
		req.AppVisible = request.AppVisible
	}
	resp, err := h.wallet.UpdateRechargeProduct(c.Request.Context(), req)
	if err != nil {
		writeWalletError(c, err, "修改内购配置失败")
		return
	}
	item := rechargeProductFromProto(resp.GetProduct())
	shared.OperationLogWithResourceID(c, h.audit, "update-recharge-product", "wallet_recharge_products", strconv.FormatInt(item.ProductID, 10), "success", item.ProductCode)
	response.OK(c, item)
}

func (h *Handler) DeleteRechargeProduct(c *gin.Context) {
	productID, ok := productID(c)
	if !ok {
		return
	}
	_, err := h.wallet.DeleteRechargeProduct(c.Request.Context(), &walletv1.DeleteRechargeProductRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		ProductId:      productID,
		OperatorUserId: actorID(c),
	})
	if err != nil {
		writeWalletError(c, err, "删除内购配置失败")
		return
	}
	shared.OperationLogWithResourceID(c, h.audit, "delete-recharge-product", "wallet_recharge_products", strconv.FormatInt(productID, 10), "success", "")
	response.OK(c, gin.H{"deleted": true})
}

func (h *Handler) ListThirdPartyPaymentChannels(c *gin.Context) {
	resp, err := h.wallet.ListThirdPartyPaymentChannels(c.Request.Context(), &walletv1.ListThirdPartyPaymentChannelsRequest{
		RequestId:              middleware.CurrentRequestID(c),
		AppCode:                appctx.FromContext(c.Request.Context()),
		ProviderCode:           strings.TrimSpace(firstQuery(c, "provider_code", "providerCode")),
		Status:                 strings.TrimSpace(firstQuery(c, "status")),
		IncludeDisabledMethods: true,
	})
	if err != nil {
		writeWalletError(c, err, "获取三方支付渠道失败")
		return
	}
	items := make([]thirdPartyPaymentChannelDTO, 0, len(resp.GetChannels()))
	for _, item := range resp.GetChannels() {
		items = append(items, thirdPartyPaymentChannelFromProto(item))
	}
	response.OK(c, gin.H{"items": items, "total": len(items)})
}

func (h *Handler) ListTemporaryPaymentLinks(c *gin.Context) {
	options := shared.ListOptions(c)
	appCode := appctx.Normalize(firstNonEmptyString(firstQuery(c, "app_code", "appCode"), appctx.FromContext(c.Request.Context())))
	access, ok := h.moneyAccess(c)
	if !ok {
		return
	}
	if h.store != nil && !moneyAccessAllowsApp(access, appCode) {
		response.Forbidden(c, "没有该 App 的财务范围")
		return
	}
	resp, err := h.wallet.ListTemporaryRechargeOrders(c.Request.Context(), &walletv1.ListTemporaryRechargeOrdersRequest{
		RequestId:    middleware.CurrentRequestID(c),
		AppCode:      appCode,
		Status:       options.Status,
		ProviderCode: strings.TrimSpace(firstQuery(c, "provider_code", "providerCode")),
		Keyword:      options.Keyword,
		Page:         int32(options.Page),
		PageSize:     int32(options.PageSize),
	})
	if err != nil {
		writeWalletError(c, err, "获取三方临时支付链接失败")
		return
	}
	items := make([]temporaryPaymentLinkDTO, 0, len(resp.GetOrders()))
	for _, item := range resp.GetOrders() {
		items = append(items, temporaryPaymentLinkFromProto(item))
	}
	if h.store != nil {
		items, err = h.enrichAndFilterTemporaryLinks(appCode, items, access, queryInt64(c, "region_id", "regionId"), queryUint(c, "operator_user_id", "operatorUserId"))
		if err != nil {
			response.ServerError(c, "获取支付链接归属失败")
			return
		}
		response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: int64(len(items))})
		return
	}
	response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
}

func (h *Handler) GetTemporaryPaymentLink(c *gin.Context) {
	orderID := strings.TrimSpace(c.Param("order_id"))
	if orderID == "" {
		response.BadRequest(c, "支付链接订单号不能为空")
		return
	}
	appCode := appctx.Normalize(firstNonEmptyString(firstQuery(c, "app_code", "appCode"), appctx.FromContext(c.Request.Context())))
	access, ok := h.moneyAccess(c)
	if !ok {
		return
	}
	if h.store != nil && !moneyAccessAllowsApp(access, appCode) {
		response.Forbidden(c, "没有该 App 的财务范围")
		return
	}
	// 查询单条订单会进入 wallet-service 的支付状态刷新逻辑；admin 页面只发起核验，不在浏览器里拼三方查询参数。
	resp, err := h.wallet.GetTemporaryRechargeOrder(c.Request.Context(), &walletv1.GetTemporaryRechargeOrderRequest{
		RequestId: middleware.CurrentRequestID(c),
		AppCode:   appCode,
		OrderId:   orderID,
	})
	if err != nil {
		writeWalletError(c, err, "校验三方临时支付链接失败")
		return
	}
	item := temporaryPaymentLinkFromProto(resp.GetOrder())
	if h.store != nil {
		items, err := h.enrichAndFilterTemporaryLinks(appCode, []temporaryPaymentLinkDTO{item}, access, 0, 0)
		if err != nil {
			response.ServerError(c, "获取支付链接归属失败")
			return
		}
		if len(items) == 0 {
			response.Forbidden(c, "没有该支付链接的财务范围")
			return
		}
		item = items[0]
	}
	response.OK(c, item)
}

func (h *Handler) SetThirdPartyPaymentMethodStatus(c *gin.Context) {
	methodID := queryPathInt64(c, "method_id")
	if methodID <= 0 {
		response.BadRequest(c, "支付方式ID不正确")
		return
	}
	var request thirdPartyPaymentMethodStatusRequest
	if err := c.ShouldBindJSON(&request); err != nil {
		response.BadRequest(c, "支付方式状态参数不正确")
		return
	}
	resp, err := h.wallet.SetThirdPartyPaymentMethodStatus(c.Request.Context(), &walletv1.SetThirdPartyPaymentMethodStatusRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		MethodId:       methodID,
		Enabled:        request.Enabled,
		OperatorUserId: actorID(c),
	})
	if err != nil {
		writeWalletError(c, err, "修改支付方式状态失败")
		return
	}
	item := thirdPartyPaymentMethodFromProto(resp.GetMethod())
	shared.OperationLogWithResourceID(c, h.audit, "update-third-party-payment-method-status", "third_party_payment_methods", strconv.FormatInt(methodID, 10), "success", item.Status)
	response.OK(c, item)
}

func (h *Handler) UpdateThirdPartyPaymentRate(c *gin.Context) {
	methodID := queryPathInt64(c, "method_id")
	if methodID <= 0 {
		response.BadRequest(c, "支付方式ID不正确")
		return
	}
	var request thirdPartyPaymentRateRequest
	if err := c.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.USDToCurrencyRate) == "" {
		response.BadRequest(c, "支付汇率参数不正确")
		return
	}
	resp, err := h.wallet.UpdateThirdPartyPaymentRate(c.Request.Context(), &walletv1.UpdateThirdPartyPaymentRateRequest{
		RequestId:         middleware.CurrentRequestID(c),
		AppCode:           appctx.FromContext(c.Request.Context()),
		MethodId:          methodID,
		UsdToCurrencyRate: strings.TrimSpace(request.USDToCurrencyRate),
		OperatorUserId:    actorID(c),
	})
	if err != nil {
		writeWalletError(c, err, "修改支付方式汇率失败")
		return
	}
	item := thirdPartyPaymentMethodFromProto(resp.GetMethod())
	shared.OperationLogWithResourceID(c, h.audit, "update-third-party-payment-rate", "third_party_payment_methods", strconv.FormatInt(methodID, 10), "success", item.USDToCurrencyRate)
	response.OK(c, item)
}

func (h *Handler) SyncThirdPartyPaymentMethods(c *gin.Context) {
	var request thirdPartyPaymentMethodSyncRequest
	if c.Request.Body != nil && c.Request.ContentLength != 0 {
		if err := c.ShouldBindJSON(&request); err != nil {
			response.BadRequest(c, "支付方式同步参数不正确")
			return
		}
	}
	providerCode := strings.TrimSpace(firstNonEmptyString(request.ProviderCode, firstQuery(c, "provider_code", "providerCode"), "v5pay"))
	resp, err := h.wallet.SyncThirdPartyPaymentMethods(c.Request.Context(), &walletv1.SyncThirdPartyPaymentMethodsRequest{
		RequestId:      middleware.CurrentRequestID(c),
		AppCode:        appctx.FromContext(c.Request.Context()),
		ProviderCode:   providerCode,
		OperatorUserId: actorID(c),
	})
	if err != nil {
		writeWalletError(c, err, "同步支付方式失败")
		return
	}
	dto := thirdPartyPaymentMethodSyncFromProto(resp)
	shared.OperationLog(c, h.audit, "sync-third-party-payment-methods", "third_party_payment_methods", "success", dto.ProviderCode)
	response.OK(c, dto)
}

func (h *Handler) SyncThirdPartyPaymentRates(c *gin.Context) {
	var request thirdPartyPaymentRateSyncRequest
	if c.Request.Body != nil && c.Request.ContentLength != 0 {
		// 同步接口历史上允许空 body 直接触发；现在弹窗只新增上浮比例，空 body 仍按 0% 处理，避免旧后台或脚本调用被破坏。
		if err := c.ShouldBindJSON(&request); err != nil || !validThirdPartyPaymentRateMarkup(request.MarkupPercent) {
			response.BadRequest(c, "支付汇率同步参数不正确")
			return
		}
	}
	resp, err := h.wallet.ListThirdPartyPaymentChannels(c.Request.Context(), &walletv1.ListThirdPartyPaymentChannelsRequest{
		RequestId:              middleware.CurrentRequestID(c),
		AppCode:                appctx.FromContext(c.Request.Context()),
		ProviderCode:           strings.TrimSpace(firstQuery(c, "provider_code", "providerCode")),
		IncludeDisabledMethods: true,
	})
	if err != nil {
		writeWalletError(c, err, "获取三方支付方式失败")
		return
	}
	methods := collectThirdPartyPaymentMethods(resp.GetChannels())
	currencies := collectThirdPartyPaymentCurrencies(methods)
	if len(currencies) == 0 {
		response.OK(c, thirdPartyPaymentRateSyncDTO{UpdatedCount: 0})
		return
	}
	// 汇率同步必须以服务端实时拉取结果为准：前端只传运营确认的上浮比例，原始汇率、失败切换和最终写库都留在服务端收敛。
	rateClient := h.exchangeRates
	if rateClient == nil {
		rateClient = newDefaultExchangeRateClient()
	}
	rateResult, err := rateClient.LatestUSDRates(c.Request.Context(), currencies)
	if err != nil {
		response.ServerError(c, "同步支付汇率失败")
		return
	}
	updated := 0
	skipped := make(map[string]struct{})
	writtenRates := make(map[string]string, len(rateResult.Rates))
	for _, method := range methods {
		currencyCode := strings.ToUpper(strings.TrimSpace(method.GetCurrencyCode()))
		rate, ok := applyThirdPartyPaymentRateMarkup(rateResult.Rates[currencyCode], request.MarkupPercent)
		if !ok {
			skipped[currencyCode] = struct{}{}
			continue
		}
		if _, err := h.wallet.UpdateThirdPartyPaymentRate(c.Request.Context(), &walletv1.UpdateThirdPartyPaymentRateRequest{
			RequestId:         middleware.CurrentRequestID(c),
			AppCode:           appctx.FromContext(c.Request.Context()),
			MethodId:          method.GetMethodId(),
			UsdToCurrencyRate: rate,
			OperatorUserId:    actorID(c),
		}); err != nil {
			writeWalletError(c, err, "写入支付汇率失败")
			return
		}
		writtenRates[currencyCode] = rate
		updated++
	}
	skippedCurrencies := sortedMapKeys(skipped)
	shared.OperationLog(c, h.audit, "sync-third-party-payment-rates", "third_party_payment_methods", "success", strings.Join(rateResult.SourceNames, ","))
	response.OK(c, thirdPartyPaymentRateSyncDTO{
		UpdatedCount:      updated,
		SkippedCount:      len(skippedCurrencies),
		MissingCurrencies: skippedCurrencies,
		SourceNames:       rateResult.SourceNames,
		Sources:           rateResult.Sources,
		Rates:             writtenRates,
	})
}

type rechargeProductRequest struct {
	AmountUSDT      string  `json:"amountUsdt"`
	AmountUSDTMicro int64   `json:"amountUsdtMicro"`
	CoinAmount      int64   `json:"coinAmount"`
	ProductName     string  `json:"productName"`
	Description     string  `json:"description"`
	AudienceType    string  `json:"audienceType"`
	Platform        string  `json:"platform"`
	RegionIDs       []int64 `json:"regionIds"`
	Enabled         bool    `json:"enabled"`
	AppVisible      *bool   `json:"appVisible"`
}

type thirdPartyPaymentMethodStatusRequest struct {
	Enabled bool `json:"enabled"`
}

type thirdPartyPaymentRateRequest struct {
	USDToCurrencyRate string `json:"usdToCurrencyRate"`
}

type thirdPartyPaymentMethodSyncRequest struct {
	ProviderCode string `json:"providerCode"`
}

type thirdPartyPaymentRateSyncRequest struct {
	MarkupPercent float64 `json:"markupPercent"`
}

func queryInt64(c *gin.Context, keys ...string) int64 {
	value := strings.TrimSpace(firstQuery(c, keys...))
	if value == "" {
		return 0
	}
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed < 0 {
		return 0
	}
	return parsed
}

func firstQuery(c *gin.Context, keys ...string) string {
	for _, key := range keys {
		if value := c.Query(key); value != "" {
			return value
		}
	}
	return ""
}

func queryPathInt64(c *gin.Context, name string) int64 {
	value := strings.TrimSpace(c.Param(name))
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed <= 0 {
		return 0
	}
	return parsed
}

func productID(c *gin.Context) (int64, bool) {
	value := strings.TrimSpace(c.Param("product_id"))
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed <= 0 {
		response.BadRequest(c, "内购配置 ID 不正确")
		return 0, false
	}
	return parsed, true
}

func actorID(c *gin.Context) int64 {
	return int64(shared.ActorFromContext(c).UserID)
}

func parseUSDTMicro(amountText string, amountMicro int64) (int64, error) {
	if amountMicro > 0 {
		return amountMicro, nil
	}
	raw := strings.TrimSpace(amountText)
	if raw == "" {
		return 0, errors.New("amount is required")
	}
	if strings.HasPrefix(raw, "+") {
		raw = strings.TrimPrefix(raw, "+")
	}
	parts := strings.Split(raw, ".")
	if len(parts) > 2 || parts[0] == "" {
		return 0, errors.New("amount format is invalid")
	}
	if !allDigits(parts[0]) {
		return 0, errors.New("amount integer part is invalid")
	}
	whole, err := strconv.ParseInt(parts[0], 10, 64)
	if err != nil || whole > math.MaxInt64/usdtMicroUnit {
		return 0, errors.New("amount is too large")
	}
	var fraction int64
	if len(parts) == 2 {
		if parts[1] == "" || len(parts[1]) > 6 || !allDigits(parts[1]) {
			return 0, errors.New("amount fraction part is invalid")
		}
		padded := parts[1] + strings.Repeat("0", 6-len(parts[1]))
		fraction, err = strconv.ParseInt(padded, 10, 64)
		if err != nil {
			return 0, errors.New("amount fraction part is invalid")
		}
	}
	if whole > (math.MaxInt64-fraction)/usdtMicroUnit {
		return 0, errors.New("amount is too large")
	}
	value := whole*usdtMicroUnit + fraction
	if value <= 0 {
		return 0, errors.New("amount must be positive")
	}
	return value, nil
}

func allDigits(value string) bool {
	for _, char := range value {
		if char < '0' || char > '9' {
			return false
		}
	}
	return value != ""
}

func validThirdPartyPaymentRateMarkup(markupPercent float64) bool {
	// 上浮比例来自后台运营弹窗，只允许 0-100 的有限数值；负数会压低支付汇率，过大数值会直接放大用户本币支付金额。
	return !math.IsNaN(markupPercent) && !math.IsInf(markupPercent, 0) && markupPercent >= 0 && markupPercent <= 100
}

func applyThirdPartyPaymentRateMarkup(rateText string, markupPercent float64) (string, bool) {
	rawRate, err := strconv.ParseFloat(strings.TrimSpace(rateText), 64)
	if err != nil || rawRate <= 0 || !validThirdPartyPaymentRateMarkup(markupPercent) {
		return "", false
	}
	// 汇率源返回的是实时 USD->本币原值；写库前先叠加运营上浮比例，再按金额量级向上取整，保证 H5 下单汇率不低于实时汇率。
	markedRate := rawRate * (1 + markupPercent/100)
	if markedRate > 50 {
		// 高汇率币种（例如 IDR）展示和支付金额都不应出现小数；减去极小量只抵消 float 乘法毛刺，真实 18510.1 仍会进到 18511。
		return strconv.FormatFloat(math.Ceil(markedRate-1e-9), 'f', 0, 64), true
	}
	// 低汇率币种仍保留原有“最多 1 位小数且向上取”的策略，避免 SAR/BHD 这类币种因强行整数化放大支付金额。
	roundedRate := math.Ceil(markedRate*10-1e-9) / 10
	return trimOneDecimalRate(strconv.FormatFloat(roundedRate, 'f', 1, 64)), true
}

func trimOneDecimalRate(rateText string) string {
	// 产品要求“最多 1 位小数”，整数汇率不保留无意义的 .0，避免后台列表和人工核对时出现多余精度。
	return strings.TrimSuffix(rateText, ".0")
}

func writeWalletError(c *gin.Context, err error, fallback string) {
	message := fallback
	if st, ok := status.FromError(err); ok && strings.TrimSpace(st.Message()) != "" {
		message = st.Message()
	}
	switch status.Code(err) {
	case codes.InvalidArgument, codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted:
		response.BadRequest(c, message)
	case codes.NotFound:
		response.NotFound(c, message)
	default:
		response.ServerError(c, fallback)
	}
}
