package walletapi

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strconv"
	"strings"

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

const (
	h5RechargeAudienceNormal     = "normal"
	h5RechargeAudienceCoinSeller = "coin_seller"
	h5PaymentProviderMifaPay     = "mifapay"
	h5PaymentProviderV5Pay       = "v5pay"
	h5PaymentProviderUSDTTRC20   = "usdt_trc20"
)

type h5RechargeTargetContext struct {
	UserID           int64
	DisplayUserID    string
	Username         string
	Avatar           string
	CountryID        int64
	CountryCode      string
	CountryName      string
	RegionID         int64
	RegionCode       string
	RegionName       string
	AudienceType     string
	IsCoinSeller     bool
	CoinSellerStatus string
	ResolvedByToken  bool
}

type h5RechargeContextData struct {
	Account  h5RechargeAccountData `json:"account"`
	Role     string                `json:"role"`
	CanPay   bool                  `json:"can_pay"`
	AuthMode string                `json:"auth_mode"`
}

type h5RechargeAccountData struct {
	DisplayUserID    string `json:"display_user_id"`
	Username         string `json:"username"`
	Avatar           string `json:"avatar,omitempty"`
	CountryCode      string `json:"country_code,omitempty"`
	CountryName      string `json:"country_name,omitempty"`
	RegionID         int64  `json:"region_id"`
	RegionCode       string `json:"region_code,omitempty"`
	RegionName       string `json:"region_name,omitempty"`
	IsCoinSeller     bool   `json:"is_coin_seller"`
	CoinSellerStatus string `json:"coin_seller_status,omitempty"`
	AudienceType     string `json:"audience_type"`
}

type h5RechargeOptionsData struct {
	Account          h5RechargeAccountData         `json:"account"`
	Products         []rechargeProductData         `json:"products"`
	PaymentMethods   []thirdPartyPaymentMethodData `json:"payment_methods"`
	USDTTRC20Enabled bool                          `json:"usdt_trc20_enabled"`
	USDTTRC20Address string                        `json:"usdt_trc20_address,omitempty"`
}

type thirdPartyPaymentMethodData struct {
	MethodID          int64  `json:"method_id"`
	ProviderCode      string `json:"provider_code"`
	ProviderName      string `json:"provider_name"`
	CountryCode       string `json:"country_code"`
	CountryName       string `json:"country_name"`
	CurrencyCode      string `json:"currency_code"`
	PayWay            string `json:"pay_way"`
	PayType           string `json:"pay_type"`
	MethodName        string `json:"method_name"`
	LogoURL           string `json:"logo_url,omitempty"`
	Status            string `json:"status"`
	USDToCurrencyRate string `json:"usd_to_currency_rate"`
	SortOrder         int32  `json:"sort_order"`
}

type h5RechargeOrderRequestBody struct {
	DisplayUserID      string `json:"display_user_id"`
	ProductID          int64  `json:"product_id"`
	ProductIDAlt       int64  `json:"productId"`
	ProviderCode       string `json:"provider_code"`
	ProviderCodeAlt    string `json:"providerCode"`
	PaymentMethod      string `json:"payment_method"`
	PaymentMethodID    int64  `json:"payment_method_id"`
	PaymentMethodIDAlt int64  `json:"paymentMethodId"`
	CommandID          string `json:"command_id"`
	CommandIDAlt       string `json:"commandId"`
	ReturnURL          string `json:"return_url"`
	ReturnURLAlt       string `json:"returnUrl"`
}

type temporaryPayLinkRequestBody struct {
	CoinAmount        int64   `json:"coin_amount"`
	CoinAmountAlt     int64   `json:"coinAmount"`
	USDMinorAmount    int64   `json:"usd_minor_amount"`
	USDMinorAmountAlt int64   `json:"usdMinorAmount"`
	USDAmount         float64 `json:"usd_amount"`
	USDAmountAlt      float64 `json:"usdAmount"`
	ProviderCode      string  `json:"provider_code"`
	ProviderCodeAlt   string  `json:"providerCode"`
	PaymentMethodID   int64   `json:"payment_method_id"`
	PaymentMethodAlt  int64   `json:"paymentMethodId"`
	CommandID         string  `json:"command_id"`
	CommandIDAlt      string  `json:"commandId"`
	ReturnURL         string  `json:"return_url"`
	ReturnURLAlt      string  `json:"returnUrl"`
}

type h5RechargeTxRequestBody struct {
	DisplayUserID string `json:"display_user_id"`
	TxHash        string `json:"tx_hash"`
	TxHashAlt     string `json:"txHash"`
}

type h5RechargeOrderData struct {
	OrderID             string `json:"order_id"`
	ProductID           int64  `json:"product_id"`
	ProductName         string `json:"product_name"`
	CoinAmount          int64  `json:"coin_amount"`
	USDMinorAmount      int64  `json:"usd_minor_amount"`
	AudienceType        string `json:"audience_type"`
	ProviderCode        string `json:"provider_code"`
	PaymentMethodID     int64  `json:"payment_method_id"`
	CountryCode         string `json:"country_code"`
	CurrencyCode        string `json:"currency_code"`
	ProviderAmountMinor int64  `json:"provider_amount_minor"`
	PayWay              string `json:"pay_way"`
	PayType             string `json:"pay_type"`
	PayURL              string `json:"pay_url,omitempty"`
	TxHash              string `json:"tx_hash,omitempty"`
	ReceiveAddress      string `json:"receive_address,omitempty"`
	Status              string `json:"status"`
	FailureReason       string `json:"failure_reason,omitempty"`
	TransactionID       string `json:"transaction_id,omitempty"`
	IdempotentReplay    bool   `json:"idempotent_replay"`
	CreatedAtMS         int64  `json:"created_at_ms"`
	UpdatedAtMS         int64  `json:"updated_at_ms"`
}

type temporaryPayLinkData struct {
	Order       h5RechargeOrderData `json:"order"`
	PaymentURL  string              `json:"payment_link"`
	Wrapped     bool                `json:"wrapped"`
	RawPayURL   string              `json:"raw_pay_url,omitempty"`
	WrappedNote string              `json:"wrapped_note,omitempty"`
}

type mifapayNotifyBody struct {
	MerAccount string `json:"merAccount"`
	Data       string `json:"data"`
	Sign       string `json:"sign"`
}

// getH5RechargeContext 确认 H5 充值目标账号；带 token 时只认 token 用户，不接受前端指定别人。
func (h *Handler) getH5RechargeContext(writer http.ResponseWriter, request *http.Request) {
	target, ok := h.resolveH5RechargeTarget(writer, request, request.URL.Query().Get("display_user_id"))
	if !ok {
		return
	}
	httpkit.WriteOK(writer, request, h5RechargeContextData{
		Account:  h5RechargeAccountFromTarget(target),
		Role:     target.AudienceType,
		CanPay:   target.RegionID > 0,
		AuthMode: h5RechargeAuthMode(target.ResolvedByToken),
	})
}

// listH5RechargeOptions 返回已确认账号的商品档位、USDT 和可下单的本地三方支付方式。
func (h *Handler) listH5RechargeOptions(writer http.ResponseWriter, request *http.Request) {
	target, ok := h.resolveH5RechargeTarget(writer, request, request.URL.Query().Get("display_user_id"))
	if !ok {
		return
	}
	resp, err := h.walletClient.ListH5RechargeOptions(request.Context(), &walletv1.H5RechargeOptionsRequest{
		RequestId:         httpkit.RequestIDFromContext(request.Context()),
		AppCode:           appcode.FromContext(request.Context()),
		TargetUserId:      target.UserID,
		TargetRegionId:    target.RegionID,
		TargetCountryCode: target.CountryCode,
		AudienceType:      target.AudienceType,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, h5RechargeOptionsFromProto(target, resp))
}

// createH5RechargeOrder 创建本地三方支付或 USDT 外部充值订单；目标账号始终由 gateway 重新解析。
func (h *Handler) createH5RechargeOrder(writer http.ResponseWriter, request *http.Request) {
	var body h5RechargeOrderRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	target, ok := h.resolveH5RechargeTarget(writer, request, body.DisplayUserID)
	if !ok {
		return
	}
	productID := firstPositiveInt64(body.ProductID, body.ProductIDAlt)
	paymentMethodID := firstPositiveInt64(body.PaymentMethodID, body.PaymentMethodIDAlt)
	providerCode := normalizeH5PaymentProvider(firstNonEmptyString(body.ProviderCode, body.ProviderCodeAlt, body.PaymentMethod), paymentMethodID)
	commandID := strings.TrimSpace(firstNonEmptyString(body.CommandID, body.CommandIDAlt))
	if commandID == "" {
		commandID = fmt.Sprintf("h5_recharge:%d:%s", target.UserID, httpkit.RequestIDFromContext(request.Context()))
	}
	if productID <= 0 || providerCode == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.CreateH5RechargeOrder(request.Context(), &walletv1.CreateH5RechargeOrderRequest{
		RequestId:         httpkit.RequestIDFromContext(request.Context()),
		AppCode:           appcode.FromContext(request.Context()),
		CommandId:         commandID,
		TargetUserId:      target.UserID,
		TargetRegionId:    target.RegionID,
		TargetCountryId:   target.CountryID,
		TargetCountryCode: target.CountryCode,
		AudienceType:      target.AudienceType,
		ProductId:         productID,
		ProviderCode:      providerCode,
		PaymentMethodId:   paymentMethodID,
		ReturnUrl:         firstNonEmptyString(body.ReturnURL, body.ReturnURLAlt),
		ClientIp:          httpkit.ClientIP(request),
		Language:          h5RechargePaymentLanguage(h5RechargePaymentLanguageQuery(request)),
		PayerName:         target.Username,
		PayerAccount:      target.DisplayUserID,
		PayerEmail:        h5RechargePayerEmail(target.DisplayUserID, appcode.FromContext(request.Context())),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{"order": h5RechargeOrderFromProto(resp.GetOrder())})
}

// listTemporaryPayMethods 返回运营临时支付链接可选的三方支付方式；不需要用户国家和商品档位。
func (h *Handler) listTemporaryPayMethods(writer http.ResponseWriter, request *http.Request) {
	resp, err := h.walletClient.ListThirdPartyPaymentChannels(request.Context(), &walletv1.ListThirdPartyPaymentChannelsRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		Status:    "active",
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	providerFilter := normalizeH5PaymentProvider(request.URL.Query().Get("provider_code"), 0)
	countryFilter := strings.ToUpper(strings.TrimSpace(request.URL.Query().Get("country_code")))
	methods := make([]thirdPartyPaymentMethodData, 0)
	for _, channel := range resp.GetChannels() {
		for _, method := range channel.GetMethods() {
			if providerFilter != "" && method.GetProviderCode() != providerFilter {
				continue
			}
			if countryFilter != "" && method.GetCountryCode() != countryFilter {
				continue
			}
			methods = append(methods, thirdPartyPaymentMethodFromProto(method))
		}
	}
	httpkit.WriteOK(writer, request, map[string]any{"methods": methods})
}

// createTemporaryPayLink 创建不绑定用户的临时收款链接；支付完成后只用于人工核销。
func (h *Handler) createTemporaryPayLink(writer http.ResponseWriter, request *http.Request) {
	var body temporaryPayLinkRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	coinAmount := firstPositiveInt64(body.CoinAmount, body.CoinAmountAlt)
	usdMinorAmount := temporaryUSDMinorAmount(body)
	paymentMethodID := firstPositiveInt64(body.PaymentMethodID, body.PaymentMethodAlt)
	providerCode := normalizeH5PaymentProvider(firstNonEmptyString(body.ProviderCode, body.ProviderCodeAlt), paymentMethodID)
	// 临时链接只代表一笔收款，不绑定用户和发币动作；coin_amount 仅保留协议兼容，页面不再提交正数。
	if body.CoinAmount < 0 || body.CoinAmountAlt < 0 || usdMinorAmount <= 0 || paymentMethodID <= 0 || providerCode == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	commandID := strings.TrimSpace(firstNonEmptyString(body.CommandID, body.CommandIDAlt))
	if commandID == "" {
		commandID = fmt.Sprintf("h5_temporary_recharge:%s", httpkit.RequestIDFromContext(request.Context()))
	}
	resp, err := h.walletClient.CreateTemporaryRechargeOrder(request.Context(), &walletv1.CreateTemporaryRechargeOrderRequest{
		RequestId:       httpkit.RequestIDFromContext(request.Context()),
		AppCode:         appcode.FromContext(request.Context()),
		CommandId:       commandID,
		CoinAmount:      coinAmount,
		UsdMinorAmount:  usdMinorAmount,
		ProviderCode:    providerCode,
		PaymentMethodId: paymentMethodID,
		ReturnUrl:       firstNonEmptyString(body.ReturnURL, body.ReturnURLAlt),
		ClientIp:        httpkit.ClientIP(request),
		Language:        h5RechargePaymentLanguage(h5RechargePaymentLanguageQuery(request)),
		PayerName:       "temporary",
		PayerAccount:    "temporary",
		PayerEmail:      temporaryPayPayerEmail(commandID, appcode.FromContext(request.Context())),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	data, ok := h.temporaryPayLinkData(writer, request, resp.GetOrder())
	if !ok {
		return
	}
	httpkit.WriteOK(writer, request, data)
}

// getTemporaryPayLink 验证某个临时支付链接状态；wallet-service 会在查询时刷新三方支付状态。
func (h *Handler) getTemporaryPayLink(writer http.ResponseWriter, request *http.Request) {
	orderID := strings.TrimSpace(firstNonEmptyString(request.PathValue("order_id"), request.URL.Query().Get("order_id")))
	if orderID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetTemporaryRechargeOrder(request.Context(), &walletv1.GetTemporaryRechargeOrderRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		OrderId:   orderID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	data, ok := h.temporaryPayLinkData(writer, request, resp.GetOrder())
	if !ok {
		return
	}
	httpkit.WriteOK(writer, request, data)
}

// listTemporaryPayLinks 返回后台管理用的临时支付链接列表；列表不批量刷新上游，单笔验证再触发查单。
func (h *Handler) listTemporaryPayLinks(writer http.ResponseWriter, request *http.Request) {
	query := request.URL.Query()
	resp, err := h.walletClient.ListTemporaryRechargeOrders(request.Context(), &walletv1.ListTemporaryRechargeOrdersRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		Status:       strings.TrimSpace(query.Get("status")),
		ProviderCode: normalizeH5PaymentProvider(query.Get("provider_code"), 0),
		Keyword:      strings.TrimSpace(query.Get("keyword")),
		Page:         int32Query(query, "page", 1),
		PageSize:     int32Query(query, "page_size", 20),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]temporaryPayLinkData, 0, len(resp.GetOrders()))
	for _, order := range resp.GetOrders() {
		data, ok := h.temporaryPayLinkData(writer, request, order)
		if !ok {
			return
		}
		items = append(items, data)
	}
	httpkit.WriteOK(writer, request, map[string]any{"orders": items, "total": resp.GetTotal()})
}

// submitH5RechargeTx 提交 USDT tx_hash；无 token 时仍用 display_user_id 校验订单归属。
func (h *Handler) submitH5RechargeTx(writer http.ResponseWriter, request *http.Request) {
	var body h5RechargeTxRequestBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	target, ok := h.resolveH5RechargeTarget(writer, request, body.DisplayUserID)
	if !ok {
		return
	}
	orderID := strings.TrimSpace(request.PathValue("order_id"))
	txHash := strings.TrimSpace(firstNonEmptyString(body.TxHash, body.TxHashAlt))
	if orderID == "" || txHash == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.SubmitH5RechargeTx(request.Context(), &walletv1.SubmitH5RechargeTxRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		OrderId:      orderID,
		TargetUserId: target.UserID,
		TxHash:       txHash,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{"order": h5RechargeOrderFromProto(resp.GetOrder())})
}

// getH5RechargeOrder 给 H5 轮询订单状态；gateway 每次重新解析账号，避免前端伪造 target_user_id。
func (h *Handler) getH5RechargeOrder(writer http.ResponseWriter, request *http.Request) {
	target, ok := h.resolveH5RechargeTarget(writer, request, request.URL.Query().Get("display_user_id"))
	if !ok {
		return
	}
	orderID := strings.TrimSpace(request.PathValue("order_id"))
	if orderID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.walletClient.GetH5RechargeOrder(request.Context(), &walletv1.GetH5RechargeOrderRequest{
		RequestId:    httpkit.RequestIDFromContext(request.Context()),
		AppCode:      appcode.FromContext(request.Context()),
		OrderId:      orderID,
		TargetUserId: target.UserID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{"order": h5RechargeOrderFromProto(resp.GetOrder())})
}

// handleMifapayNotify 按 MiFaPay 要求返回纯文本 SUCCESS；失败不套 gateway JSON envelope。
func (h *Handler) handleMifapayNotify(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		writeMifaPayNotifyText(writer, http.StatusBadGateway, "FAIL")
		return
	}
	body := mifapayNotifyBody{
		MerAccount: strings.TrimSpace(request.FormValue("merAccount")),
		Data:       strings.TrimSpace(request.FormValue("data")),
		Sign:       strings.TrimSpace(request.FormValue("sign")),
	}
	if body.MerAccount == "" && body.Data == "" && body.Sign == "" {
		if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
			writeMifaPayNotifyText(writer, http.StatusBadRequest, "FAIL")
			return
		}
	}
	resp, err := h.walletClient.HandleMifapayNotify(request.Context(), &walletv1.HandleMifapayNotifyRequest{
		RequestId:  httpkit.RequestIDFromContext(request.Context()),
		AppCode:    appcode.FromContext(request.Context()),
		MerAccount: body.MerAccount,
		Data:       body.Data,
		Sign:       body.Sign,
	})
	if err != nil || !resp.GetAccepted() {
		writeMifaPayNotifyText(writer, http.StatusBadRequest, "FAIL")
		return
	}
	writeMifaPayNotifyText(writer, http.StatusOK, "SUCCESS")
}

// handleV5PayNotify 按 V5Pay 要求返回小写纯文本 success；验签和订单校验都下沉到 wallet-service。
func (h *Handler) handleV5PayNotify(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil {
		writeV5PayNotifyText(writer, http.StatusBadGateway, "fail")
		return
	}
	raw, err := io.ReadAll(io.LimitReader(request.Body, 2<<20))
	if err != nil {
		writeV5PayNotifyText(writer, http.StatusBadRequest, "fail")
		return
	}
	fields := map[string]string{}
	if len(raw) > 0 {
		var payload map[string]any
		if err := json.Unmarshal(raw, &payload); err == nil {
			fields = v5PayNotifyFields(payload)
		} else if values, formErr := url.ParseQuery(string(raw)); formErr == nil {
			for key, candidates := range values {
				if len(candidates) > 0 {
					fields[key] = strings.TrimSpace(candidates[0])
				}
			}
		} else {
			writeV5PayNotifyText(writer, http.StatusBadRequest, "fail")
			return
		}
	}
	if len(fields) == 0 {
		if err := request.ParseForm(); err != nil {
			writeV5PayNotifyText(writer, http.StatusBadRequest, "fail")
			return
		}
		for key, values := range request.Form {
			if len(values) > 0 {
				fields[key] = strings.TrimSpace(values[0])
			}
		}
	}
	resp, err := h.walletClient.HandleV5PayNotify(request.Context(), &walletv1.HandleV5PayNotifyRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		RawJson:   strings.TrimSpace(string(raw)),
		Fields:    fields,
	})
	if err != nil || !resp.GetAccepted() {
		logV5PayNotifyRejected(request, err, resp, fields)
		writeV5PayNotifyText(writer, http.StatusBadRequest, "fail")
		return
	}
	writeV5PayNotifyText(writer, http.StatusOK, firstNonEmptyString(resp.GetResponseText(), "success"))
}

func (h *Handler) resolveH5RechargeTarget(writer http.ResponseWriter, request *http.Request, displayUserID string) (h5RechargeTargetContext, bool) {
	if h.walletClient == nil || h.userProfileClient == nil || h.userIdentityClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return h5RechargeTargetContext{}, false
	}
	userID := auth.UserIDFromContext(request.Context())
	resolvedByToken := userID > 0
	if !resolvedByToken {
		if strings.TrimSpace(displayUserID) == "" {
			httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
			return h5RechargeTargetContext{}, false
		}
		// 无 token 入口的输入是“用户标识”，可能是 active 靓号、当前短号、默认数字 ID 或真实 user_id。
		// 所有传给 wallet-service 的目标都必须先在 gateway 统一收敛成内部 user_id，避免各业务入口自行解析导致靓号报错。
		resolvedUserID, err := useridinput.Resolve(request.Context(), h.userIdentityClient, httpkit.UserMeta(request, ""), displayUserID)
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return h5RechargeTargetContext{}, false
		}
		userID = resolvedUserID
	}
	if userID <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return h5RechargeTargetContext{}, false
	}
	profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return h5RechargeTargetContext{}, false
	}
	user := profileResp.GetUser()
	if user == nil || user.GetUserId() <= 0 || user.GetStatus() != userv1.UserStatus_USER_STATUS_ACTIVE || user.GetRegionId() <= 0 {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return h5RechargeTargetContext{}, false
	}
	audienceType, isCoinSeller, coinSellerStatus, ok := h.resolveH5RechargeAudience(writer, request, userID)
	if !ok {
		return h5RechargeTargetContext{}, false
	}
	return h5RechargeTargetContext{
		UserID:           user.GetUserId(),
		DisplayUserID:    user.GetDisplayUserId(),
		Username:         user.GetUsername(),
		Avatar:           user.GetAvatar(),
		CountryID:        user.GetCountryId(),
		CountryCode:      h5RechargeCountryCodeFromUser(user),
		CountryName:      firstNonEmptyString(user.GetCountryDisplayName(), user.GetCountryName()),
		RegionID:         user.GetRegionId(),
		RegionCode:       user.GetRegionCode(),
		RegionName:       user.GetRegionName(),
		AudienceType:     audienceType,
		IsCoinSeller:     isCoinSeller,
		CoinSellerStatus: coinSellerStatus,
		ResolvedByToken:  resolvedByToken,
	}, true
}

func h5RechargeCountryCodeFromUser(user *userv1.User) string {
	if user == nil {
		return ""
	}
	for _, candidate := range []string{user.GetCountry(), user.GetCountryName(), user.GetCountryDisplayName()} {
		// H5 支付方式和下单都按 wallet-service 的国家码过滤；这里把历史资料里的英文名、中文名或 alpha-3 统一成 ISO2。
		// 如果用户资料本身已经是 SA 这类规范国家码，函数会直接返回，避免改变正常注册链路产生的 country 快照。
		if countryCode := normalizeH5RechargeCountryCode(candidate); countryCode != "" {
			return countryCode
		}
	}
	return ""
}

func h5RechargePayerEmail(account string, appCode string) string {
	localPart := h5RechargeEmailLocalPart(account)
	domainLabel := h5RechargeEmailDomainLabel(appcode.Normalize(appCode))
	// MiFaPay 要求 H5 下单传 email；产品口径是“用户账号 + @ + app_code + .com”。
	// 这里不接受前端自报邮箱，只用 gateway 已解析出的目标账号和当前租户，避免用户能篡改付款人身份快照。
	return localPart + "@" + domainLabel + ".com"
}

func h5RechargePaymentLanguage(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return "en"
	}
	if comma := strings.IndexByte(value, ','); comma >= 0 {
		value = value[:comma]
	}
	if semicolon := strings.IndexByte(value, ';'); semicolon >= 0 {
		value = value[:semicolon]
	}
	value = strings.TrimSpace(value)
	if !validH5RechargeLanguageTag(value) {
		return "en"
	}
	// MiFaPay language 只接受单个语言标签；这里保留显式 query 的首选标签，例如 zh-CN。
	// gateway 不再从 Header 兜底，避免浏览器 Accept-Language 在无 H5 query 时影响支付页语言或触发三方格式校验。
	return value
}

func h5RechargePaymentLanguageQuery(request *http.Request) string {
	// 支付语言属于 H5 页面入口参数，而不是浏览器能力探测结果；只认页面 query，避免没有 App 语言参数时又把
	// Accept-Language 的 q 权重列表带进 MiFaPay，造成同一用户在不同系统浏览器里出现不可预期支付页语言。
	for _, key := range []string{"language", "lang", "locale", "app_lang", "appLanguage"} {
		if value := strings.TrimSpace(request.URL.Query().Get(key)); value != "" {
			return value
		}
	}
	return ""
}

func validH5RechargeLanguageTag(value string) bool {
	if value == "" {
		return false
	}
	parts := strings.Split(value, "-")
	if len(parts) == 0 || len(parts) > 4 || len(parts[0]) < 2 || len(parts[0]) > 3 || !allASCIILetters(parts[0]) {
		return false
	}
	for _, part := range parts[1:] {
		if len(part) < 2 || len(part) > 8 || !allASCIILettersOrDigits(part) {
			return false
		}
	}
	return true
}

func h5RechargeEmailLocalPart(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return "user"
	}
	var builder strings.Builder
	for _, char := range value {
		switch {
		case char >= 'a' && char <= 'z':
			builder.WriteRune(char)
		case char >= 'A' && char <= 'Z':
			builder.WriteRune(char)
		case char >= '0' && char <= '9':
			builder.WriteRune(char)
		case char == '.', char == '_', char == '-', char == '+':
			builder.WriteRune(char)
		default:
			builder.WriteByte('-')
		}
	}
	normalized := strings.Trim(builder.String(), ".-")
	if normalized == "" {
		return "user"
	}
	return normalized
}

func h5RechargeEmailDomainLabel(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if value == "" {
		return appcode.Default
	}
	var builder strings.Builder
	for _, char := range value {
		switch {
		case char >= 'a' && char <= 'z':
			builder.WriteRune(char)
		case char >= '0' && char <= '9':
			builder.WriteRune(char)
		case char == '-':
			builder.WriteRune(char)
		default:
			builder.WriteByte('-')
		}
	}
	normalized := strings.Trim(builder.String(), "-")
	if normalized == "" {
		return appcode.Default
	}
	return normalized
}

func normalizeH5RechargeCountryCode(value string) string {
	normalized := strings.ToUpper(strings.TrimSpace(value))
	if normalized == "" {
		return ""
	}
	normalized = strings.NewReplacer("_", " ", "-", " ").Replace(normalized)
	normalized = strings.Join(strings.Fields(normalized), " ")
	if countryCode, ok := h5RechargeCountryAliases[normalized]; ok {
		return countryCode
	}
	if len(normalized) >= 2 && len(normalized) <= 3 && allASCIIUpperLetters(normalized) {
		return normalized
	}
	return ""
}

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

func allASCIILetters(value string) bool {
	for _, char := range value {
		if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') {
			return false
		}
	}
	return value != ""
}

func allASCIILettersOrDigits(value string) bool {
	for _, char := range value {
		if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') && (char < '0' || char > '9') {
			return false
		}
	}
	return value != ""
}

var h5RechargeCountryAliases = map[string]string{
	"ARE":                  "AE",
	"UNITED ARAB EMIRATES": "AE",
	"UAE":                  "AE",
	"阿拉伯联合酋长国":             "AE",
	"BANGLADESH":           "BD",
	"BGD":                  "BD",
	"孟加拉国":                 "BD",
	"BAHRAIN":              "BH",
	"BHR":                  "BH",
	"巴林":                   "BH",
	"EGY":                  "EG",
	"EGYPT":                "EG",
	"埃及":                   "EG",
	"IDN":                  "ID",
	"INDONESIA":            "ID",
	"印度尼西亚":                "ID",
	"IND":                  "IN",
	"INDIA":                "IN",
	"印度":                   "IN",
	"KWT":                  "KW",
	"KUWAIT":               "KW",
	"科威特":                  "KW",
	"OMAN":                 "OM",
	"OMN":                  "OM",
	"阿曼":                   "OM",
	"PAKISTAN":             "PK",
	"PAK":                  "PK",
	"巴基斯坦":                 "PK",
	"PHILIPPINES":          "PH",
	"PHL":                  "PH",
	"菲律宾":                  "PH",
	"QAT":                  "QA",
	"QATAR":                "QA",
	"卡塔尔":                  "QA",
	"SAU":                  "SA",
	"SAUDI ARABIA":         "SA",
	"沙特阿拉伯":                "SA",
	"THA":                  "TH",
	"THAILAND":             "TH",
	"泰国":                   "TH",
	"VNM":                  "VN",
	"VIETNAM":              "VN",
	"越南":                   "VN",
}

func (h *Handler) resolveH5RechargeAudience(writer http.ResponseWriter, request *http.Request, userID int64) (string, bool, string, bool) {
	resp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return "", false, "", false
	}
	summary := resp.GetSummary()
	if summary != nil && summary.GetIsCoinSeller() && strings.EqualFold(summary.GetCoinSellerStatus(), "active") {
		return h5RechargeAudienceCoinSeller, true, summary.GetCoinSellerStatus(), true
	}
	status := ""
	if summary != nil {
		status = summary.GetCoinSellerStatus()
	}
	return h5RechargeAudienceNormal, false, status, true
}

func h5RechargeOptionsFromProto(target h5RechargeTargetContext, resp *walletv1.H5RechargeOptionsResponse) h5RechargeOptionsData {
	data := h5RechargeOptionsData{Account: h5RechargeAccountFromTarget(target)}
	if resp == nil {
		return data
	}
	data.Products = make([]rechargeProductData, 0, len(resp.GetProducts()))
	for _, product := range resp.GetProducts() {
		data.Products = append(data.Products, rechargeProductFromProto(product))
	}
	data.PaymentMethods = make([]thirdPartyPaymentMethodData, 0, len(resp.GetPaymentMethods()))
	for _, method := range resp.GetPaymentMethods() {
		data.PaymentMethods = append(data.PaymentMethods, thirdPartyPaymentMethodFromProto(method))
	}
	data.USDTTRC20Enabled = resp.GetUsdtTrc20Enabled()
	data.USDTTRC20Address = resp.GetUsdtTrc20Address()
	return data
}

func h5RechargeAccountFromTarget(target h5RechargeTargetContext) h5RechargeAccountData {
	return h5RechargeAccountData{
		DisplayUserID:    target.DisplayUserID,
		Username:         target.Username,
		Avatar:           target.Avatar,
		CountryCode:      target.CountryCode,
		CountryName:      target.CountryName,
		RegionID:         target.RegionID,
		RegionCode:       target.RegionCode,
		RegionName:       target.RegionName,
		IsCoinSeller:     target.IsCoinSeller,
		CoinSellerStatus: target.CoinSellerStatus,
		AudienceType:     target.AudienceType,
	}
}

func thirdPartyPaymentMethodFromProto(method *walletv1.ThirdPartyPaymentMethod) thirdPartyPaymentMethodData {
	if method == nil {
		return thirdPartyPaymentMethodData{}
	}
	return thirdPartyPaymentMethodData{
		MethodID:          method.GetMethodId(),
		ProviderCode:      method.GetProviderCode(),
		ProviderName:      method.GetProviderName(),
		CountryCode:       method.GetCountryCode(),
		CountryName:       method.GetCountryName(),
		CurrencyCode:      method.GetCurrencyCode(),
		PayWay:            method.GetPayWay(),
		PayType:           method.GetPayType(),
		MethodName:        method.GetMethodName(),
		LogoURL:           method.GetLogoUrl(),
		Status:            method.GetStatus(),
		USDToCurrencyRate: method.GetUsdToCurrencyRate(),
		SortOrder:         method.GetSortOrder(),
	}
}

func h5RechargeOrderFromProto(order *walletv1.ExternalRechargeOrder) h5RechargeOrderData {
	if order == nil {
		return h5RechargeOrderData{}
	}
	return h5RechargeOrderData{
		OrderID:             order.GetOrderId(),
		ProductID:           order.GetProductId(),
		ProductName:         order.GetProductName(),
		CoinAmount:          order.GetCoinAmount(),
		USDMinorAmount:      order.GetUsdMinorAmount(),
		AudienceType:        order.GetAudienceType(),
		ProviderCode:        order.GetProviderCode(),
		PaymentMethodID:     order.GetPaymentMethodId(),
		CountryCode:         order.GetCountryCode(),
		CurrencyCode:        order.GetCurrencyCode(),
		ProviderAmountMinor: order.GetProviderAmountMinor(),
		PayWay:              order.GetPayWay(),
		PayType:             order.GetPayType(),
		PayURL:              order.GetPayUrl(),
		TxHash:              order.GetTxHash(),
		ReceiveAddress:      order.GetReceiveAddress(),
		Status:              order.GetStatus(),
		FailureReason:       order.GetFailureReason(),
		TransactionID:       order.GetTransactionId(),
		IdempotentReplay:    order.GetIdempotentReplay(),
		CreatedAtMS:         order.GetCreatedAtMs(),
		UpdatedAtMS:         order.GetUpdatedAtMs(),
	}
}

func normalizeH5PaymentProvider(value string, paymentMethodID int64) string {
	value = strings.ToLower(strings.TrimSpace(value))
	switch value {
	case "usdt", "usdt-trc20", "usdt_trc20", "trc20":
		return h5PaymentProviderUSDTTRC20
	case "v5", "v5pay", "v5_pay":
		return h5PaymentProviderV5Pay
	case "mifa", "mifapay", "third_party":
		return h5PaymentProviderMifaPay
	case "":
		if paymentMethodID > 0 {
			return h5PaymentProviderMifaPay
		}
	}
	return value
}

func (h *Handler) temporaryPayLinkData(writer http.ResponseWriter, request *http.Request, order *walletv1.ExternalRechargeOrder) (temporaryPayLinkData, bool) {
	data := temporaryPayLinkData{Order: h5RechargeOrderFromProto(order), PaymentURL: strings.TrimSpace(order.GetPayUrl())}
	if data.PaymentURL == "" || isHTTPPayURL(data.PaymentURL) {
		return data, true
	}
	wrappedURL, err := h.uploadTemporaryPaySchemeWrapper(request, order.GetOrderId(), data.PaymentURL)
	if err != nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "temporary payment wrapper is not configured")
		return temporaryPayLinkData{}, false
	}
	data.RawPayURL = data.PaymentURL
	data.PaymentURL = wrappedURL
	data.Wrapped = true
	data.WrappedNote = "原始支付链接是 app scheme，已生成 COS 中转 HTML。"
	return data, true
}

func (h *Handler) uploadTemporaryPaySchemeWrapper(request *http.Request, orderID string, rawPayURL string) (string, error) {
	if h.objectUploader == nil {
		return "", fmt.Errorf("object uploader is not configured")
	}
	orderID = strings.TrimSpace(orderID)
	if orderID == "" {
		return "", fmt.Errorf("order_id is required")
	}
	body := temporaryPaySchemeHTML(rawPayURL)
	key := fmt.Sprintf("recharge/temporary-pay/%s.html", orderID)
	return h.objectUploader.PutObject(request.Context(), key, bytes.NewReader([]byte(body)), int64(len(body)), "text/html; charset=utf-8")
}

func temporaryPaySchemeHTML(rawPayURL string) string {
	rawPayURL = strings.TrimSpace(rawPayURL)
	payURLJSON, _ := json.Marshal(rawPayURL)
	return fmt.Sprintf(`<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>打开支付</title>
</head>
<body>
  <p>正在打开支付页面，如果没有自动跳转，请点击下面的链接。</p>
  <p><a href="%s">打开支付</a></p>
  <script>window.location.replace(%s);</script>
</body>
</html>`, html.EscapeString(rawPayURL), string(payURLJSON))
}

func isHTTPPayURL(value string) bool {
	parsed, err := url.Parse(strings.TrimSpace(value))
	if err != nil {
		return false
	}
	return parsed.Scheme == "http" || parsed.Scheme == "https"
}

func temporaryUSDMinorAmount(body temporaryPayLinkRequestBody) int64 {
	if value := firstPositiveInt64(body.USDMinorAmount, body.USDMinorAmountAlt); value > 0 {
		return value
	}
	for _, amount := range []float64{body.USDAmount, body.USDAmountAlt} {
		if amount > 0 {
			return int64(amount*100 + 0.5)
		}
	}
	return 0
}

func temporaryPayPayerEmail(commandID string, app string) string {
	localPart := h5RechargeEmailLocalPart(strings.TrimPrefix(commandID, "h5_temporary_recharge:"))
	return localPart + "@" + h5RechargeEmailDomainLabel(app) + ".temporary"
}

func int32Query(values url.Values, key string, fallback int32) int32 {
	parsed, err := strconv.Atoi(strings.TrimSpace(values.Get(key)))
	if err != nil || parsed <= 0 {
		return fallback
	}
	return int32(parsed)
}

func v5PayNotifyFields(payload map[string]any) map[string]string {
	fields := make(map[string]string, len(payload))
	for key, value := range payload {
		switch typed := value.(type) {
		case nil:
			fields[key] = ""
		case string:
			fields[key] = strings.TrimSpace(typed)
		default:
			fields[key] = strings.TrimSpace(fmt.Sprint(typed))
		}
	}
	return fields
}

func logV5PayNotifyRejected(request *http.Request, err error, resp *walletv1.HandleV5PayNotifyResponse, fields map[string]string) {
	attrs := []slog.Attr{
		slog.String("request_id", httpkit.RequestIDFromContext(request.Context())),
		slog.String("app_code", appcode.FromContext(request.Context())),
		slog.String("order_no", firstNonEmptyString(fields["orderNo"], fields["order_no"])),
		slog.String("product_type", firstNonEmptyString(fields["productType"], fields["product_type"])),
		slog.String("order_status", firstNonEmptyString(fields["orderStatus"], fields["order_status"])),
		slog.Int("field_count", len(fields)),
	}
	if resp != nil {
		attrs = append(attrs, slog.Bool("accepted", resp.GetAccepted()), slog.String("response_text", resp.GetResponseText()), slog.String("wallet_order_id", resp.GetOrderId()))
	}
	if err != nil {
		attrs = append(attrs, slog.String("error", err.Error()))
	}
	// V5Pay 回调失败必须有足够排障信息，但不能把完整 payload/sign 写进 gateway 日志，避免泄露签名材料。
	logx.Warn(request.Context(), "v5pay_notify_rejected", attrs...)
}

func h5RechargeAuthMode(resolvedByToken bool) string {
	if resolvedByToken {
		return "token"
	}
	return "display_user_id"
}

func firstPositiveInt64(values ...int64) int64 {
	for _, value := range values {
		if value > 0 {
			return value
		}
	}
	return 0
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if value = strings.TrimSpace(value); value != "" {
			return value
		}
	}
	return ""
}

func writeMifaPayNotifyText(writer http.ResponseWriter, statusCode int, body string) {
	writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
	writer.WriteHeader(statusCode)
	_, _ = writer.Write([]byte(body))
}

func writeV5PayNotifyText(writer http.ResponseWriter, statusCode int, body string) {
	writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
	writer.WriteHeader(statusCode)
	_, _ = writer.Write([]byte(body))
}
