package walletapi

import (
	"context"
	"fmt"
	"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/client"
	"hyapp/services/gateway-service/internal/financewithdrawal"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

const (
	salaryIdentityHost     = "HOST"
	salaryIdentityAgency   = "AGENCY"
	salaryIdentityBD       = "BD"
	salaryIdentityBDLeader = "BD_LEADER"

	salaryAssetHost     = "HOST_SALARY_USD"
	salaryAssetAgency   = "AGENCY_SALARY_USD"
	salaryAssetBD       = "BD_SALARY_USD"
	salaryAssetBDLeader = "ADMIN_SALARY_USD"

	salaryWalletMinimumWithdrawUSDMinor = 5000
)

type salaryWalletIdentityContext struct {
	Identity        string
	SalaryAssetType string
	UserID          int64
	RegionID        int64
}

type salaryWalletOverviewData struct {
	Identity          string                             `json:"identity"`
	SalaryAssetType   string                             `json:"salary_asset_type"`
	RegionID          int64                              `json:"region_id"`
	Salary            salaryWalletBalanceData            `json:"salary"`
	WithdrawAddress   salaryWalletWithdrawAddressData    `json:"withdraw_address"`
	ExchangeRate      int64                              `json:"exchange_rate"`
	TransferRateTiers []salaryWalletExchangeRateTierData `json:"transfer_rate_tiers"`
}

type salaryWalletBalanceData struct {
	AssetType       string `json:"asset_type"`
	AvailableAmount int64  `json:"available_amount"`
	FrozenAmount    int64  `json:"frozen_amount"`
	DisplayAmount   string `json:"display_amount"`
}

type salaryWalletExchangeRateTierData struct {
	RegionID    int64  `json:"region_id"`
	MinUSDMinor int64  `json:"min_usd_minor"`
	MaxUSDMinor int64  `json:"max_usd_minor"`
	CoinPerUSD  int64  `json:"coin_per_usd"`
	Status      string `json:"status"`
	SortOrder   int32  `json:"sort_order"`
	UpdatedAtMS int64  `json:"updated_at_ms"`
}

type salaryWalletWithdrawAddressData struct {
	WithdrawUSDTTRC20Address    string `json:"withdraw_usdt_trc20_address"`
	USDTTRC20Address            string `json:"usdt_trc20_address"`
	HasWithdrawUSDTTRC20Address bool   `json:"has_withdraw_usdt_trc20_address"`
}

type salaryWalletAmountBody struct {
	CommandID       string `json:"command_id"`
	CommandIDCamel  string `json:"commandId"`
	Identity        string `json:"identity"`
	AmountUSD       string `json:"amount_usd"`
	AmountUSDCamel  string `json:"amountUsd"`
	AmountUSDMinor  int64  `json:"amount_usd_minor"`
	AmountUSDMinorC int64  `json:"amountUsdMinor"`
	Reason          string `json:"reason"`
}

type salaryWalletTransferBody struct {
	CommandID            string `json:"command_id"`
	CommandIDCamel       string `json:"commandId"`
	Identity             string `json:"identity"`
	TargetDisplayUserID  string `json:"target_display_user_id"`
	TargetDisplayUserIDC string `json:"targetDisplayUserId"`
	AmountUSD            string `json:"amount_usd"`
	AmountUSDCamel       string `json:"amountUsd"`
	AmountUSDMinor       int64  `json:"amount_usd_minor"`
	AmountUSDMinorC      int64  `json:"amountUsdMinor"`
	Reason               string `json:"reason"`
}

type salaryWalletWithdrawAddressBody struct {
	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"`
}

type salaryWalletWithdrawBody struct {
	CommandID       string `json:"command_id"`
	CommandIDCamel  string `json:"commandId"`
	Identity        string `json:"identity"`
	AmountUSD       string `json:"amount_usd"`
	AmountUSDCamel  string `json:"amountUsd"`
	AmountUSDMinor  int64  `json:"amount_usd_minor"`
	AmountUSDMinorC int64  `json:"amountUsdMinor"`

	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 salaryWalletAmountBody) commandID() string {
	if value := strings.TrimSpace(b.CommandID); value != "" {
		return value
	}
	return strings.TrimSpace(b.CommandIDCamel)
}

func (b salaryWalletAmountBody) amountUSDMinor() (int64, error) {
	if b.AmountUSDMinor > 0 {
		return b.AmountUSDMinor, nil
	}
	if b.AmountUSDMinorC > 0 {
		return b.AmountUSDMinorC, nil
	}
	if value := strings.TrimSpace(b.AmountUSD); value != "" {
		return parseUSDMinor(value)
	}
	return parseUSDMinor(b.AmountUSDCamel)
}

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

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

func (b salaryWalletTransferBody) amountUSDMinor() (int64, error) {
	if b.AmountUSDMinor > 0 {
		return b.AmountUSDMinor, nil
	}
	if b.AmountUSDMinorC > 0 {
		return b.AmountUSDMinorC, nil
	}
	if value := strings.TrimSpace(b.AmountUSD); value != "" {
		return parseUSDMinor(value)
	}
	return parseUSDMinor(b.AmountUSDCamel)
}

func (b salaryWalletWithdrawAddressBody) usdtTRC20Address() string {
	for _, value := range []string{
		b.WithdrawUSDTTRC20Address,
		b.WithdrawUSDTTRC20AddressC,
		b.USDTTRC20Address,
		b.USDTTRC20AddressC,
		b.PaymentAddress,
		b.PaymentAddressC,
		b.Address,
	} {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func (b salaryWalletWithdrawBody) amountUSDMinor() (int64, error) {
	if b.AmountUSDMinor > 0 {
		return b.AmountUSDMinor, nil
	}
	if b.AmountUSDMinorC > 0 {
		return b.AmountUSDMinorC, nil
	}
	if value := strings.TrimSpace(b.AmountUSD); value != "" {
		return parseUSDMinor(value)
	}
	return parseUSDMinor(b.AmountUSDCamel)
}

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

func (b salaryWalletWithdrawBody) 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()
}

func (h *Handler) getSalaryWalletOverview(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	identity, ok := h.resolveSalaryWalletIdentity(writer, request, request.URL.Query().Get("identity"))
	if !ok {
		return
	}

	balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
		RequestId:  httpkit.RequestIDFromContext(request.Context()),
		UserId:     identity.UserID,
		AssetTypes: []string{identity.SalaryAssetType},
		AppCode:    appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	ratesResp, err := h.walletClient.ListCoinSellerSalaryExchangeRateTiers(request.Context(), &walletv1.ListCoinSellerSalaryExchangeRateTiersRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		RegionId:  identity.RegionID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: identity.UserID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, salaryWalletOverviewData{
		Identity:          identity.Identity,
		SalaryAssetType:   identity.SalaryAssetType,
		RegionID:          identity.RegionID,
		Salary:            salaryBalanceFromProto(identity.SalaryAssetType, firstBalance(balanceResp.GetBalances())),
		WithdrawAddress:   salaryWithdrawAddressFromProto(profileResp.GetUser()),
		ExchangeRate:      80000,
		TransferRateTiers: salaryRateTiersFromProto(ratesResp.GetTiers()),
	})
}

func (h *Handler) handleSalaryWalletWithdrawAddress(writer http.ResponseWriter, request *http.Request) {
	switch request.Method {
	case http.MethodGet:
		h.getSalaryWalletWithdrawAddress(writer, request)
	case http.MethodPost:
		h.updateSalaryWalletWithdrawAddress(writer, request)
	default:
		httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
	}
}

func (h *Handler) getSalaryWalletWithdrawAddress(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	resp, 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
	}

	httpkit.WriteOK(writer, request, salaryWithdrawAddressFromProto(resp.GetUser()))
}

func (h *Handler) updateSalaryWalletWithdrawAddress(writer http.ResponseWriter, request *http.Request) {
	if h.userProfileClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body salaryWalletWithdrawAddressBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	address := body.usdtTRC20Address()
	if address == "" {
		// H5 保存动作必须携带明确地址；清空默认提现地址需要单独产品规则，不能用空提交误清。
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	resp, err := h.userProfileClient.UpdateUserWithdrawAddress(request.Context(), &userv1.UpdateUserWithdrawAddressRequest{
		Meta:             httpkit.UserMeta(request, ""),
		UserId:           auth.UserIDFromContext(request.Context()),
		UsdtTrc20Address: address,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}

	httpkit.WriteOK(writer, request, salaryWithdrawAddressFromProto(resp.GetUser()))
}

func (h *Handler) withdrawSalaryWallet(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil || h.userProfileClient == nil || h.withdrawalWriter == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body salaryWalletWithdrawBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	amountUSDMinor, err := body.amountUSDMinor()
	if err != nil || amountUSDMinor < salaryWalletMinimumWithdrawUSDMinor {
		// H5 页面展示 Minimum 50 USD；服务端同步兜底，避免绕过前端直接提交小额申请。
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	identity, ok := h.resolveSalaryWalletIdentity(writer, request, body.Identity)
	if !ok {
		return
	}
	balanceResp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
		RequestId:  httpkit.RequestIDFromContext(request.Context()),
		UserId:     identity.UserID,
		AssetTypes: []string{identity.SalaryAssetType},
		AppCode:    appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	balance := firstBalance(balanceResp.GetBalances())
	if balance == nil || balance.GetAvailableAmount() < amountUSDMinor {
		// 冻结前先读余额，可以把明显不可出款的请求挡在地址保存和账务冻结之前。
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "insufficient balance")
		return
	}

	address, ok := h.resolveSalaryWalletWithdrawAddress(writer, request, identity.UserID, body.usdtTRC20Address())
	if !ok {
		return
	}
	nowMS := time.Now().UTC().UnixMilli()
	freezeCommandID := salaryWithdrawalFreezeCommandID(body.commandID(), httpkit.RequestIDFromContext(request.Context()), nowMS)
	freezeResp, err := h.walletClient.FreezeSalaryWithdrawal(request.Context(), &walletv1.FreezeSalaryWithdrawalRequest{
		CommandId:       freezeCommandID,
		UserId:          identity.UserID,
		SalaryAssetType: identity.SalaryAssetType,
		SalaryUsdMinor:  amountUSDMinor,
		Reason:          "salary withdrawal submitted",
		AppCode:         appcode.FromContext(request.Context()),
		WithdrawalRef:   httpkit.RequestIDFromContext(request.Context()),
		RegionId:        identity.RegionID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	application, err := h.withdrawalWriter.CreateApplication(request.Context(), financewithdrawal.CreateApplicationCommand{
		AppCode:             appcode.FromContext(request.Context()),
		UserID:              identity.UserID,
		SalaryAssetType:     identity.SalaryAssetType,
		WithdrawAmount:      formatUSDMinor(amountUSDMinor),
		WithdrawAmountMinor: amountUSDMinor,
		WithdrawMethod:      financewithdrawal.MethodUSDTTRC20,
		WithdrawAddress:     address,
		FreezeCommandID:     freezeCommandID,
		FreezeTransactionID: freezeResp.GetTransactionId(),
		CreatedAtMS:         nowMS,
	})
	if err != nil {
		rollbackSalaryWithdrawalFreeze(request, h.walletClient, freezeCommandID, identity, amountUSDMinor)
		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("salary_withdrawal_dingtalk_notify_failed", "application_id", application.ID, "user_id", identity.UserID, "app_code", application.AppCode, "error", err)
		}
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"application_id":        strconv.FormatInt(application.ID, 10),
		"status":                application.Status,
		"operations_status":     application.OperationsStatus,
		"identity":              identity.Identity,
		"salary_asset_type":     identity.SalaryAssetType,
		"salary_usd_minor":      amountUSDMinor,
		"salary_balance_after":  salaryBalanceFromProto(identity.SalaryAssetType, freezeResp.GetBalance()),
		"withdraw_amount":       application.WithdrawAmount,
		"withdraw_method":       application.WithdrawMethod,
		"withdraw_address":      application.WithdrawAddress,
		"freeze_transaction_id": freezeResp.GetTransactionId(),
		"created_at_ms":         application.CreatedAtMS,
	})
}

func salaryWithdrawalFreezeCommandID(submittedCommandID string, requestID string, nowMS int64) string {
	if value := strings.TrimSpace(submittedCommandID); value != "" {
		return value
	}
	requestID = strings.TrimSpace(requestID)
	if requestID != "" {
		return "salary-withdrawal:" + requestID
	}
	return fmt.Sprintf("salary-withdrawal:%d", nowMS)
}

func rollbackSalaryWithdrawalFreeze(request *http.Request, walletClient client.WalletClient, freezeCommandID string, identity salaryWalletIdentityContext, amountUSDMinor int64) {
	if walletClient == nil {
		return
	}
	rollbackCommandID := strings.TrimSpace(freezeCommandID) + ":rollback"
	if len(rollbackCommandID) > 128 {
		rollbackCommandID = fmt.Sprintf("salary-withdrawal-rollback:%s", httpkit.RequestIDFromContext(request.Context()))
	}
	rollbackCtx, cancel := context.WithTimeout(appcode.WithContext(context.Background(), appcode.FromContext(request.Context())), 5*time.Second)
	defer cancel()
	_, err := walletClient.ReleaseSalaryWithdrawal(rollbackCtx, &walletv1.ReleaseSalaryWithdrawalRequest{
		CommandId:       rollbackCommandID,
		UserId:          identity.UserID,
		SalaryAssetType: identity.SalaryAssetType,
		SalaryUsdMinor:  amountUSDMinor,
		Reason:          "salary withdrawal application create failed",
		AppCode:         appcode.FromContext(request.Context()),
	})
	if err != nil {
		slog.Error("salary_withdrawal_freeze_rollback_failed", "user_id", identity.UserID, "salary_asset_type", identity.SalaryAssetType, "amount_usd_minor", amountUSDMinor, "freeze_command_id", freezeCommandID, "error", err)
	}
}

func (h *Handler) resolveSalaryWalletWithdrawAddress(writer http.ResponseWriter, request *http.Request, userID int64, submittedAddress string) (string, bool) {
	if submittedAddress != "" {
		// 提现时携带新地址时复用 user-service 的地址校验和持久化规则，确保下次进入 H5 不再重复填写。
		resp, err := h.userProfileClient.UpdateUserWithdrawAddress(request.Context(), &userv1.UpdateUserWithdrawAddressRequest{
			Meta:             httpkit.UserMeta(request, ""),
			UserId:           userID,
			UsdtTrc20Address: submittedAddress,
		})
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return "", false
		}
		address := strings.TrimSpace(resp.GetUser().GetWithdrawUsdtTrc20Address())
		if address == "" {
			httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
			return "", false
		}
		return address, true
	}
	resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return "", false
	}
	address := strings.TrimSpace(resp.GetUser().GetWithdrawUsdtTrc20Address())
	if address == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "withdraw address is required")
		return "", false
	}
	return address, true
}

func (h *Handler) getSalaryWalletHistory(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	identity, ok := h.resolveSalaryWalletIdentity(writer, request, request.URL.Query().Get("identity"))
	if !ok {
		return
	}
	page, ok := httpkit.PositiveInt32Query(request, "page", 1)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
	if !ok {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	// H5 工资历史不能让前端自己传 asset_type；先用身份权限解析出对应工资钱包资产，
	// 再查询当前用户的该资产流水，确保主播/Agency/BD/Admin 只看到自己身份的钱包收支。
	resp, err := h.walletClient.ListWalletTransactions(request.Context(), &walletv1.ListWalletTransactionsRequest{
		RequestId: httpkit.RequestIDFromContext(request.Context()),
		AppCode:   appcode.FromContext(request.Context()),
		UserId:    identity.UserID,
		AssetType: identity.SalaryAssetType,
		Page:      page,
		PageSize:  pageSize,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	profiles, err := h.walletTransactionCounterpartyProfiles(request, resp.GetTransactions())
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	items := make([]walletTransactionData, 0, len(resp.GetTransactions()))
	for _, item := range resp.GetTransactions() {
		items = append(items, walletTransactionFromProto(item, profiles))
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"identity":          identity.Identity,
		"salary_asset_type": identity.SalaryAssetType,
		"items":             items,
		"total":             resp.GetTotal(),
		"page":              page,
		"page_size":         pageSize,
	})
}

func (h *Handler) searchSalaryWalletSeller(writer http.ResponseWriter, request *http.Request) {
	if h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	identity, ok := h.resolveSalaryWalletIdentity(writer, request, request.URL.Query().Get("identity"))
	if !ok {
		return
	}
	displayUserID := strings.TrimSpace(request.URL.Query().Get("display_user_id"))
	if displayUserID == "" {
		displayUserID = strings.TrimSpace(request.URL.Query().Get("displayUserId"))
	}
	if displayUserID == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}

	seller, found, ok := h.findSameRegionSalaryCoinSeller(writer, request, identity.RegionID, displayUserID)
	if !ok {
		return
	}
	if !found {
		httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{"seller": coinSellerFromProto(seller)})
}

func (h *Handler) exchangeSalaryToCoin(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body salaryWalletAmountBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := body.commandID()
	amountUSDMinor, err := body.amountUSDMinor()
	if err != nil || commandID == "" || amountUSDMinor <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	identity, ok := h.resolveSalaryWalletIdentity(writer, request, body.Identity)
	if !ok {
		return
	}
	reason := strings.TrimSpace(body.Reason)
	if reason == "" {
		reason = "salary exchange"
	}

	resp, err := h.walletClient.ExchangeSalaryToCoin(request.Context(), &walletv1.ExchangeSalaryToCoinRequest{
		CommandId:       commandID,
		UserId:          identity.UserID,
		SalaryAssetType: identity.SalaryAssetType,
		SalaryUsdMinor:  amountUSDMinor,
		Reason:          reason,
		AppCode:         appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"transaction_id":       resp.GetTransactionId(),
		"salary_balance_after": resp.GetSalaryBalanceAfter(),
		"coin_balance_after":   resp.GetCoinBalanceAfter(),
		"salary_usd_minor":     resp.GetSalaryUsdMinor(),
		"coin_amount":          resp.GetCoinAmount(),
		"coin_per_usd":         resp.GetCoinPerUsd(),
	})
}

func (h *Handler) transferSalaryToCoinSeller(writer http.ResponseWriter, request *http.Request) {
	if h.walletClient == nil || h.userHostClient == nil {
		httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
		return
	}
	var body salaryWalletTransferBody
	if !httpkit.Decode(writer, request, &body) {
		return
	}
	commandID := body.commandID()
	displayUserID := body.targetDisplayUserID()
	amountUSDMinor, err := body.amountUSDMinor()
	if err != nil || commandID == "" || displayUserID == "" || amountUSDMinor <= 0 {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return
	}
	identity, ok := h.resolveSalaryWalletIdentity(writer, request, body.Identity)
	if !ok {
		return
	}
	seller, found, ok := h.findSameRegionSalaryCoinSeller(writer, request, identity.RegionID, displayUserID)
	if !ok {
		return
	}
	if !found {
		httpkit.WriteError(writer, request, http.StatusNotFound, httpkit.CodeNotFound, "not found")
		return
	}
	reason := strings.TrimSpace(body.Reason)
	if reason == "" {
		reason = "salary transfer"
	}

	resp, err := h.walletClient.TransferSalaryToCoinSeller(request.Context(), &walletv1.TransferSalaryToCoinSellerRequest{
		CommandId:       commandID,
		SourceUserId:    identity.UserID,
		SellerUserId:    seller.GetUserId(),
		SalaryAssetType: identity.SalaryAssetType,
		SalaryUsdMinor:  amountUSDMinor,
		RegionId:        identity.RegionID,
		Reason:          reason,
		AppCode:         appcode.FromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return
	}
	httpkit.WriteOK(writer, request, map[string]any{
		"transaction_id":              resp.GetTransactionId(),
		"source_salary_balance_after": resp.GetSourceSalaryBalanceAfter(),
		"seller_balance_after":        resp.GetSellerBalanceAfter(),
		"salary_usd_minor":            resp.GetSalaryUsdMinor(),
		"coin_amount":                 resp.GetCoinAmount(),
		"coin_per_usd":                resp.GetCoinPerUsd(),
		"rate_min_usd_minor":          resp.GetRateMinUsdMinor(),
		"rate_max_usd_minor":          resp.GetRateMaxUsdMinor(),
	})
}

func (h *Handler) resolveSalaryWalletIdentity(writer http.ResponseWriter, request *http.Request, rawIdentity string) (salaryWalletIdentityContext, bool) {
	identity := normalizeSalaryWalletIdentity(rawIdentity)
	if identity == "" {
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return salaryWalletIdentityContext{}, false
	}
	userID := auth.UserIDFromContext(request.Context())
	switch identity {
	case salaryIdentityHost:
		resp, err := h.userHostClient.GetHostProfile(request.Context(), &userv1.GetHostProfileRequest{
			Meta:   httpkit.UserMeta(request, ""),
			UserId: userID,
		})
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return salaryWalletIdentityContext{}, false
		}
		profile := resp.GetHostProfile()
		if profile == nil || profile.GetUserId() != userID || profile.GetStatus() != "active" || profile.GetRegionId() <= 0 {
			httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
			return salaryWalletIdentityContext{}, false
		}
		return salaryWalletIdentityContext{Identity: identity, SalaryAssetType: salaryAssetHost, UserID: userID, RegionID: profile.GetRegionId()}, true
	case salaryIdentityAgency:
		return h.resolveAgencySalaryIdentity(writer, request, userID)
	case salaryIdentityBD, salaryIdentityBDLeader:
		return h.resolveBDSalaryIdentity(writer, request, userID, identity)
	default:
		httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
		return salaryWalletIdentityContext{}, false
	}
}

func (h *Handler) resolveAgencySalaryIdentity(writer http.ResponseWriter, request *http.Request, userID int64) (salaryWalletIdentityContext, bool) {
	roleResp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return salaryWalletIdentityContext{}, false
	}
	summary := roleResp.GetSummary()
	if summary == nil || summary.GetAgencyId() <= 0 || (!summary.GetIsAgency() && !summary.GetIsManager()) {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return salaryWalletIdentityContext{}, false
	}
	agencyResp, err := h.userHostClient.GetAgency(request.Context(), &userv1.GetAgencyRequest{
		Meta:     httpkit.UserMeta(request, ""),
		AgencyId: summary.GetAgencyId(),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return salaryWalletIdentityContext{}, false
	}
	agency := agencyResp.GetAgency()
	if agency == nil || agency.GetOwnerUserId() != userID || agency.GetStatus() != "active" || agency.GetRegionId() <= 0 {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return salaryWalletIdentityContext{}, false
	}
	return salaryWalletIdentityContext{Identity: salaryIdentityAgency, SalaryAssetType: salaryAssetAgency, UserID: userID, RegionID: agency.GetRegionId()}, true
}

func (h *Handler) resolveBDSalaryIdentity(writer http.ResponseWriter, request *http.Request, userID int64, identity string) (salaryWalletIdentityContext, bool) {
	role := "bd"
	if identity == salaryIdentityBDLeader {
		role = "bd_leader"
	}
	resp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: userID,
		Role:   role,
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return salaryWalletIdentityContext{}, false
	}
	profile := resp.GetBdProfile()
	if profile == nil || profile.GetUserId() != userID || profile.GetStatus() != "active" || profile.GetRegionId() <= 0 {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return salaryWalletIdentityContext{}, false
	}
	if identity == salaryIdentityBD && profile.GetRole() != "bd" {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return salaryWalletIdentityContext{}, false
	}
	if identity == salaryIdentityBDLeader && profile.GetRole() != "bd_leader" {
		httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
		return salaryWalletIdentityContext{}, false
	}
	assetType := salaryAssetBD
	if identity == salaryIdentityBDLeader {
		assetType = salaryAssetBDLeader
	}
	return salaryWalletIdentityContext{Identity: identity, SalaryAssetType: assetType, UserID: userID, RegionID: profile.GetRegionId()}, true
}

func (h *Handler) findSameRegionSalaryCoinSeller(writer http.ResponseWriter, request *http.Request, regionID int64, displayUserID string) (*userv1.CoinSellerListItem, bool, bool) {
	resp, err := h.userHostClient.ListActiveCoinSellersInMyRegion(request.Context(), &userv1.ListActiveCoinSellersInMyRegionRequest{
		Meta:   httpkit.UserMeta(request, ""),
		UserId: auth.UserIDFromContext(request.Context()),
	})
	if err != nil {
		httpkit.WriteRPCError(writer, request, err)
		return nil, false, false
	}
	for _, seller := range resp.GetCoinSellers() {
		if seller.GetRegionId() == regionID && seller.GetMerchantAssetType() == "COIN_SELLER_COIN" && seller.GetStatus() == "active" && seller.GetDisplayUserId() == displayUserID {
			return seller, true, true
		}
	}
	return nil, false, true
}

func normalizeSalaryWalletIdentity(value string) string {
	value = strings.ToUpper(strings.TrimSpace(value))
	value = strings.ReplaceAll(value, "-", "_")
	switch value {
	case salaryIdentityHost, salaryIdentityAgency, salaryIdentityBD, salaryIdentityBDLeader:
		return value
	case "ADMIN":
		return salaryIdentityBDLeader
	default:
		return ""
	}
}

func firstBalance(balances []*walletv1.AssetBalance) *walletv1.AssetBalance {
	if len(balances) == 0 {
		return nil
	}
	return balances[0]
}

func salaryBalanceFromProto(assetType string, balance *walletv1.AssetBalance) salaryWalletBalanceData {
	if balance == nil {
		return salaryWalletBalanceData{AssetType: assetType, DisplayAmount: "0.00"}
	}
	return salaryWalletBalanceData{
		AssetType:       balance.GetAssetType(),
		AvailableAmount: balance.GetAvailableAmount(),
		FrozenAmount:    balance.GetFrozenAmount(),
		DisplayAmount:   formatUSDMinor(balance.GetAvailableAmount()),
	}
}

func salaryRateTiersFromProto(tiers []*walletv1.CoinSellerSalaryExchangeRateTier) []salaryWalletExchangeRateTierData {
	items := make([]salaryWalletExchangeRateTierData, 0, len(tiers))
	for _, tier := range tiers {
		items = append(items, salaryWalletExchangeRateTierData{
			RegionID:    tier.GetRegionId(),
			MinUSDMinor: tier.GetMinUsdMinor(),
			MaxUSDMinor: tier.GetMaxUsdMinor(),
			CoinPerUSD:  tier.GetCoinPerUsd(),
			Status:      tier.GetStatus(),
			SortOrder:   tier.GetSortOrder(),
			UpdatedAtMS: tier.GetUpdatedAtMs(),
		})
	}
	return items
}

func salaryWithdrawAddressFromProto(user *userv1.User) salaryWalletWithdrawAddressData {
	address := ""
	if user != nil {
		// 用户资料只保存默认 TRC20 提现地址；响应同时给语义字段和 H5 简写字段，降低前端接入歧义。
		address = strings.TrimSpace(user.GetWithdrawUsdtTrc20Address())
	}
	return salaryWalletWithdrawAddressData{
		WithdrawUSDTTRC20Address:    address,
		USDTTRC20Address:            address,
		HasWithdrawUSDTTRC20Address: address != "",
	}
}

func parseUSDMinor(value string) (int64, error) {
	value = strings.TrimSpace(value)
	if value == "" || strings.HasPrefix(value, "-") {
		return 0, fmt.Errorf("invalid amount")
	}
	parts := strings.Split(value, ".")
	if len(parts) > 2 || parts[0] == "" {
		return 0, fmt.Errorf("invalid amount")
	}
	dollars, err := strconv.ParseInt(parts[0], 10, 64)
	if err != nil {
		return 0, err
	}
	cents := int64(0)
	if len(parts) == 2 {
		fraction := parts[1]
		if len(fraction) > 2 {
			return 0, fmt.Errorf("invalid amount")
		}
		for len(fraction) < 2 {
			fraction += "0"
		}
		cents, err = strconv.ParseInt(fraction, 10, 64)
		if err != nil {
			return 0, err
		}
	}
	if dollars > (1<<63-1-cents)/100 {
		return 0, fmt.Errorf("amount overflow")
	}
	return dollars*100 + cents, nil
}

func formatUSDMinor(value int64) string {
	sign := ""
	if value < 0 {
		sign = "-"
		value = -value
	}
	return fmt.Sprintf("%s%d.%02d", sign, value/100, value%100)
}
