package financeorder

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"math"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

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

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const (
	permissionCoinSellerRechargeView   = "finance-order:coin-seller-recharge:view"
	permissionCoinSellerRechargeCreate = "finance-order:coin-seller-recharge:create"
	permissionCoinSellerRechargeVerify = "finance-order:coin-seller-recharge:verify"
	permissionCoinSellerRechargeGrant  = "finance-order:coin-seller-recharge:grant"
	permissionUSDTAddressUpdate        = "finance-order:usdt-address:update"
	permissionCoinSellerExchangeRate   = "coin-seller:exchange-rate"

	coinSellerRechargeAssetType = "COIN_SELLER_COIN"
	coinSellerRechargeStockType = "usdt_purchase"

	financeOrderInternalUserIDMinDigits = 15
)

var (
	errInvalidCoinSellerRechargeOrderInput = errors.New("币商充值订单参数不正确")
	errCoinSellerRechargeForbidden         = errors.New("没有操作权限")
)

type Service struct {
	store               *repository.Store
	wallet              walletclient.Client
	user                userclient.Client
	now                 func() time.Time
	statsRecorder       financeCoinSellerRechargeStatsRecorder
	legacyWriters       map[string]LegacyCoinSellerRechargeWriter
	legacyRegionResolve LegacyCoinSellerRegionResolver
}

type Option func(*Service)

// LegacyCoinSellerRegionResolver 把 legacy 用户的国家码解析成财务/BI 通用的区域 ID
// （与 admin_user_money_scopes、账单区域分布同一套合成口径）。
// 没有它 Aslan/Yumi 的币商充值订单只有国家快照，target_region_id 恒为 0，
// 区域分布和分区域授权账号都无法归因这笔充值。
type LegacyCoinSellerRegionResolver func(ctx context.Context, appCode string, countryCode string) (int64, bool, error)

func WithLegacyCoinSellerRegionResolver(resolver LegacyCoinSellerRegionResolver) Option {
	return func(service *Service) {
		service.legacyRegionResolve = resolver
	}
}

type financeCoinSellerRechargeStatsRecorder interface {
	RecordFinanceCoinSellerRechargeOrder(ctx context.Context, order model.CoinSellerRechargeOrder) error
}

func WithFinanceCoinSellerRechargeStatsRecorder(recorder financeCoinSellerRechargeStatsRecorder) Option {
	return func(service *Service) {
		service.statsRecorder = recorder
	}
}

func WithLegacyCoinSellerRechargeWriters(writers ...LegacyCoinSellerRechargeWriter) Option {
	return func(service *Service) {
		if service.legacyWriters == nil {
			service.legacyWriters = map[string]LegacyCoinSellerRechargeWriter{}
		}
		for _, writer := range writers {
			if writer == nil {
				continue
			}
			appCode := appctx.Normalize(writer.AppCode())
			if appCode != "" {
				service.legacyWriters[appCode] = writer
			}
		}
	}
}

func NewService(store *repository.Store, wallet walletclient.Client, user userclient.Client, options ...Option) *Service {
	service := &Service{
		store:         store,
		wallet:        wallet,
		user:          user,
		now:           func() time.Time { return time.Now().UTC() },
		legacyWriters: map[string]LegacyCoinSellerRechargeWriter{},
	}
	for _, option := range options {
		if option != nil {
			option(service)
		}
	}
	return service
}

func (s *Service) CreateOrder(ctx context.Context, actor shared.Actor, req createCoinSellerRechargeOrderRequest, requestID string) (*coinSellerRechargeOrderDTO, error) {
	if s == nil || s.store == nil {
		return nil, errors.New("admin store is not configured")
	}
	if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionCoinSellerRechargeCreate) {
		return nil, errCoinSellerRechargeForbidden
	}
	input, err := normalizeCreateOrderRequest(req)
	if err != nil {
		return nil, err
	}
	nowMS := s.now().UnixMilli()
	input.OrderDateMS, err = normalizeCreateOrderBusinessTime(input.OrderDateMS, nowMS)
	if err != nil {
		return nil, err
	}
	// 金币数只由当前 APP 汇率配置生成；即使调用方绕过前端直接提交 coinAmount，也不会进入请求结构或账务结果。
	target, err := s.resolveCoinSellerTarget(ctx, requestID, input.AppCode, input.TargetUserID)
	if err != nil {
		return nil, err
	}
	coinAmount, err := s.createOrderCoinAmount(input, target)
	if err != nil {
		return nil, err
	}
	input.CoinAmount = coinAmount
	// 前端“校验通过”只能改善操作体验，不能作为资金事实；创建时必须复用同一套服务端校验，避免篡改请求直接落库。
	receipt, err := s.verifyReceiptInput(ctx, input.receiptVerificationInput(), strings.TrimSpace(requestID))
	if err != nil {
		return nil, err
	}
	if !receipt.Verified {
		return nil, errors.New(firstNonEmpty(receipt.FailureReason, "充值凭证未支付成功"))
	}
	if err := verifyReceiptMatchesInput(input, receipt); err != nil {
		return nil, err
	}
	verifiedAtMS := receipt.VerifiedAtMS
	if verifiedAtMS <= 0 {
		verifiedAtMS = nowMS
	}
	order := &model.CoinSellerRechargeOrder{
		AppCode:              input.AppCode,
		TargetUserID:         target.UserID,
		TargetDisplayUserID:  firstNonEmpty(target.DisplayUserID, formatInt64(target.UserID)),
		TargetCountryID:      target.CountryID,
		TargetRegionID:       target.RegionID,
		USDAmount:            input.USDAmount,
		USDMinorAmount:       input.USDMinorAmount,
		CoinAmount:           input.CoinAmount,
		ProviderCode:         input.ProviderCode,
		Chain:                input.Chain,
		ExternalOrderNo:      input.ExternalOrderNo,
		ProviderOrderID:      receipt.ProviderOrderID,
		ProviderStatus:       receipt.Status,
		ProviderCountryCode:  input.ProviderCountryCode,
		ProviderCurrencyCode: firstNonEmpty(receipt.CurrencyCode, input.ProviderCurrencyCode),
		ProviderAmountMinor:  receipt.ProviderAmountMinor,
		ProviderPayType:      input.ProviderPayType,
		ReceiveAddress:       receipt.ReceiveAddress,
		OrderDateMS:          input.OrderDateMS,
		// 新建订单已经通过外部凭证校验，因此进入待发放状态；历史 pending 订单仍可走列表行校验按钮重试。
		Status:              model.CoinSellerRechargeOrderStatusVerified,
		VerifyStatus:        model.CoinSellerRechargeVerifyStatusVerified,
		GrantStatus:         model.CoinSellerRechargeGrantStatusPending,
		Remark:              input.Remark,
		ProviderPayloadJSON: receipt.RawJSON,
		OperatorUserID:      actor.UserID,
		OperatorName:        actor.Username,
		VerifiedByUserID:    &actor.UserID,
		VerifiedByName:      actor.Username,
		VerifiedAtMS:        &verifiedAtMS,
		CreatedAtMS:         nowMS,
		UpdatedAtMS:         nowMS,
	}
	// admin_coin_seller_recharge_orders 是所有 APP 的订单/币商绑定台账；即使 Aslan/Yumi 最终写 legacy 钱包，也必须先在这里占用外部订单号。
	if err := s.store.CreateCoinSellerRechargeOrder(order); err != nil {
		if isDuplicateKeyError(err) {
			return nil, errors.New("订单号已绑定，不能重复创建")
		}
		return nil, err
	}
	item, err := s.store.GetCoinSellerRechargeOrder(order.ID)
	if err != nil {
		return nil, err
	}
	s.recordVerifiedCoinSellerRechargeOrder(ctx, *item)
	dto := coinSellerRechargeOrderDTOFromModel(*item)
	return &dto, nil
}

func (s *Service) VerifyReceipt(ctx context.Context, actor shared.Actor, req verifyCoinSellerRechargeReceiptRequest, requestID string) (*coinSellerRechargeReceiptVerificationDTO, error) {
	if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionCoinSellerRechargeVerify) {
		return nil, errCoinSellerRechargeForbidden
	}
	// 该接口只返回三方/链上凭证快照，不创建订单、不占用外部订单号；真正绑定发生在 CreateOrder 的事务路径。
	input, err := normalizeReceiptVerificationRequest(req)
	if err != nil {
		return nil, err
	}
	receipt, err := s.verifyReceiptInput(ctx, input, strings.TrimSpace(requestID))
	if err != nil {
		return nil, err
	}
	return &receipt, nil
}

func (s *Service) ListOrders(options repository.CoinSellerRechargeOrderListOptions) ([]coinSellerRechargeOrderDTO, int64, error) {
	if s == nil || s.store == nil {
		return nil, 0, errors.New("admin store is not configured")
	}
	if appCode := strings.TrimSpace(options.AppCode); appCode != "" && appCode != "all" {
		options.AppCode = appctx.Normalize(appCode)
	} else {
		options.AppCode = ""
	}
	items, total, err := s.store.ListCoinSellerRechargeOrders(options)
	if err != nil {
		return nil, 0, err
	}
	return coinSellerRechargeOrderDTOsFromModel(items), total, nil
}

func (s *Service) GetOrder(id uint) (*coinSellerRechargeOrderDTO, error) {
	if s == nil || s.store == nil {
		return nil, errors.New("admin store is not configured")
	}
	item, err := s.store.GetCoinSellerRechargeOrder(id)
	if err != nil {
		return nil, err
	}
	dto := coinSellerRechargeOrderDTOFromModel(*item)
	return &dto, nil
}

func (s *Service) VerifyOrder(ctx context.Context, actor shared.Actor, id uint, requestID string) (*coinSellerRechargeOrderDTO, error) {
	if s == nil || s.store == nil {
		return nil, errors.New("admin store is not configured")
	}
	if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionCoinSellerRechargeVerify) {
		return nil, errCoinSellerRechargeForbidden
	}
	if s.wallet == nil {
		return nil, errors.New("finance order wallet verifier is not configured")
	}
	order, err := s.store.GetCoinSellerRechargeOrder(id)
	if err != nil {
		return nil, err
	}
	if order.GrantStatus == model.CoinSellerRechargeGrantStatusGranted {
		return nil, repository.ErrCoinSellerRechargeOrderGranted
	}
	receiveAddress, err := s.financeUSDTReceiveAddress(order.AppCode, order.Chain)
	if err != nil {
		return nil, err
	}
	resp, err := s.wallet.VerifyCoinSellerRechargeReceipt(appctx.WithContext(ctx, order.AppCode), &walletv1.VerifyCoinSellerRechargeReceiptRequest{
		RequestId:            strings.TrimSpace(requestID),
		AppCode:              order.AppCode,
		ProviderCode:         receiptProviderCode(*order),
		ExternalOrderNo:      order.ExternalOrderNo,
		Chain:                receiptChain(*order),
		UsdMinorAmount:       order.USDMinorAmount,
		OrderDateMs:          order.OrderDateMS,
		ProviderCountryCode:  order.ProviderCountryCode,
		ProviderCurrencyCode: order.ProviderCurrencyCode,
		ProviderAmountMinor:  order.ProviderAmountMinor,
		ProviderPayType:      order.ProviderPayType,
		ReceiveAddress:       receiveAddress,
	})
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, errors.New("充值凭证校验返回为空")
	}
	failureReason := strings.TrimSpace(resp.GetFailureReason())
	if resp.GetVerified() {
		if matchErr := verifyReceiptMatchesOrder(*order, resp); matchErr != nil {
			failureReason = matchErr.Error()
		}
	}
	verifyInput := coinSellerRechargeVerifyInput(actor, resp, failureReason, s.now().UnixMilli())
	var updated *model.CoinSellerRechargeOrder
	if resp.GetVerified() && failureReason == "" {
		updated, err = s.store.MarkCoinSellerRechargeOrderVerified(id, verifyInput)
	} else {
		if failureReason == "" {
			failureReason = "充值凭证未通过校验"
			verifyInput.FailureReason = failureReason
		}
		updated, err = s.store.MarkCoinSellerRechargeOrderVerifyFailed(id, verifyInput)
	}
	if err != nil {
		return nil, err
	}
	if updated != nil && updated.VerifyStatus == model.CoinSellerRechargeVerifyStatusVerified {
		s.recordVerifiedCoinSellerRechargeOrder(ctx, *updated)
	}
	dto := coinSellerRechargeOrderDTOFromModel(*updated)
	return &dto, nil
}

func (s *Service) GrantOrder(ctx context.Context, actor shared.Actor, id uint, requestID string) (*coinSellerRechargeOrderDTO, error) {
	if s == nil || s.store == nil {
		return nil, errors.New("admin store is not configured")
	}
	if actor.UserID == 0 || !hasPermission(actor.Permissions, permissionCoinSellerRechargeGrant) {
		return nil, errCoinSellerRechargeForbidden
	}
	nowMS := s.now().UnixMilli()
	claimed, err := s.store.ClaimCoinSellerRechargeOrderGrant(id, actor.UserID, actor.Username, nowMS)
	if err != nil {
		return nil, err
	}
	if claimed.GrantStatus == model.CoinSellerRechargeGrantStatusGranted {
		dto := coinSellerRechargeOrderDTOFromModel(*claimed)
		return &dto, nil
	}
	execution, err := s.executeGrant(ctx, actor, *claimed, requestID)
	if err != nil {
		failed, markErr := s.store.MarkCoinSellerRechargeOrderGrantFailed(id, repository.CoinSellerRechargeOrderGrantInput{
			GrantedByUserID: actor.UserID,
			GrantedByName:   actor.Username,
			GrantedAtMS:     s.now().UnixMilli(),
			FailureReason:   err.Error(),
		})
		if markErr != nil {
			return nil, markErr
		}
		dto := coinSellerRechargeOrderDTOFromModel(*failed)
		return &dto, err
	}
	updated, err := s.store.MarkCoinSellerRechargeOrderGranted(id, repository.CoinSellerRechargeOrderGrantInput{
		GrantedByUserID:     actor.UserID,
		GrantedByName:       actor.Username,
		GrantedAtMS:         s.now().UnixMilli(),
		WalletCommandID:     execution.CommandID,
		WalletTransactionID: execution.TransactionID,
		WalletAssetType:     execution.AssetType,
		WalletAmountDelta:   execution.AmountDelta,
		WalletBalanceAfter:  execution.BalanceAfter,
	})
	if err != nil {
		return nil, err
	}
	dto := coinSellerRechargeOrderDTOFromModel(*updated)
	return &dto, nil
}

func (s *Service) recordVerifiedCoinSellerRechargeOrder(ctx context.Context, order model.CoinSellerRechargeOrder) {
	if s == nil || s.statsRecorder == nil || order.ID == 0 || order.VerifyStatus != model.CoinSellerRechargeVerifyStatusVerified {
		return
	}
	// 统计投影是幂等的派生读模型；创建/校验主链路不能因为 statistics-service 短暂不可用而回滚已校验财务工单。
	if err := s.statsRecorder.RecordFinanceCoinSellerRechargeOrder(ctx, order); err != nil {
		slog.WarnContext(ctx, "finance_coin_seller_recharge_stats_record_failed", "order_id", order.ID, "app_code", order.AppCode, "error", err)
	}
}

type normalizedCreateOrderInput struct {
	AppCode              string
	TargetUserID         string
	USDAmount            string
	USDMinorAmount       int64
	CoinAmount           int64
	IsMakeup             bool
	ProviderCode         string
	Chain                string
	ExternalOrderNo      string
	ProviderCountryCode  string
	ProviderCurrencyCode string
	ProviderAmountMinor  int64
	ProviderPayType      string
	OrderDateMS          int64
	Remark               string
}

type normalizedReceiptVerificationInput struct {
	AppCode              string
	USDMinorAmount       int64
	ProviderCode         string
	Chain                string
	ExternalOrderNo      string
	ProviderCountryCode  string
	ProviderCurrencyCode string
	ProviderAmountMinor  int64
	ProviderPayType      string
	OrderDateMS          int64
}

func (input normalizedCreateOrderInput) receiptVerificationInput() normalizedReceiptVerificationInput {
	return normalizedReceiptVerificationInput{
		AppCode:              input.AppCode,
		USDMinorAmount:       input.USDMinorAmount,
		ProviderCode:         input.ProviderCode,
		Chain:                input.Chain,
		ExternalOrderNo:      input.ExternalOrderNo,
		ProviderCountryCode:  input.ProviderCountryCode,
		ProviderCurrencyCode: input.ProviderCurrencyCode,
		ProviderAmountMinor:  input.ProviderAmountMinor,
		ProviderPayType:      input.ProviderPayType,
		OrderDateMS:          input.OrderDateMS,
	}
}

func normalizeCreateOrderRequest(req createCoinSellerRechargeOrderRequest) (normalizedCreateOrderInput, error) {
	appCode := appctx.Normalize(req.AppCode)
	targetUserID := strings.TrimSpace(req.TargetUserID)
	providerCode, chain, err := normalizeRechargeProvider(req.ProviderCode, req.Chain)
	if err != nil {
		return normalizedCreateOrderInput{}, err
	}
	usdAmount, usdMinorAmount, err := normalizeUSD(req.USDAmount)
	if err != nil {
		return normalizedCreateOrderInput{}, err
	}
	externalOrderNo := strings.TrimSpace(req.ExternalOrderNo)
	if appCode == "" || targetUserID == "" || externalOrderNo == "" {
		return normalizedCreateOrderInput{}, errInvalidCoinSellerRechargeOrderInput
	}
	if len(externalOrderNo) > 128 {
		return normalizedCreateOrderInput{}, errors.New("订单号过长")
	}
	if req.ProviderAmountMinor < 0 {
		return normalizedCreateOrderInput{}, errors.New("三方实付金额不正确")
	}
	providerCountryCode := strings.ToUpper(strings.TrimSpace(req.ProviderCountryCode))
	providerCurrencyCode := strings.ToUpper(strings.TrimSpace(req.ProviderCurrencyCode))
	providerPayType := strings.TrimSpace(req.ProviderPayType)
	remark := strings.TrimSpace(req.Remark)
	if utf8.RuneCountInString(remark) > 512 {
		return normalizedCreateOrderInput{}, errors.New("备注不能超过 512 字")
	}
	orderDateMS, err := normalizeOrderDateMS(req.OrderDate, req.OrderDateMS)
	if err != nil {
		return normalizedCreateOrderInput{}, err
	}
	switch providerCode {
	case "usdt":
		if chain == "" {
			return normalizedCreateOrderInput{}, errors.New("USDT 充值必须选择链")
		}
	case "mifapay", "v5pay":
		// 三方实付币种和金额是人工订单绑定的校验条件；缺失时会退化成只按订单号查，容易把错误 APP/币种的流水绑定进币商台账。
		if providerCurrencyCode == "" {
			return normalizedCreateOrderInput{}, errors.New("三方币种不能为空")
		}
		if req.ProviderAmountMinor <= 0 {
			return normalizedCreateOrderInput{}, errors.New("三方实付金额不正确")
		}
	}
	return normalizedCreateOrderInput{
		AppCode:              appCode,
		TargetUserID:         targetUserID,
		USDAmount:            usdAmount,
		USDMinorAmount:       usdMinorAmount,
		CoinAmount:           0,
		IsMakeup:             req.IsMakeup,
		ProviderCode:         providerCode,
		Chain:                chain,
		ExternalOrderNo:      externalOrderNo,
		ProviderCountryCode:  providerCountryCode,
		ProviderCurrencyCode: providerCurrencyCode,
		ProviderAmountMinor:  req.ProviderAmountMinor,
		ProviderPayType:      providerPayType,
		OrderDateMS:          orderDateMS,
		Remark:               remark,
	}, nil
}

func normalizeReceiptVerificationRequest(req verifyCoinSellerRechargeReceiptRequest) (normalizedReceiptVerificationInput, error) {
	appCode := appctx.Normalize(req.AppCode)
	providerCode, chain, err := normalizeRechargeProvider(req.ProviderCode, req.Chain)
	if err != nil {
		return normalizedReceiptVerificationInput{}, err
	}
	externalOrderNo := strings.TrimSpace(req.ExternalOrderNo)
	if appCode == "" || externalOrderNo == "" {
		return normalizedReceiptVerificationInput{}, errInvalidCoinSellerRechargeOrderInput
	}
	if len(externalOrderNo) > 128 {
		return normalizedReceiptVerificationInput{}, errors.New("订单号过长")
	}
	usdMinorAmount := req.USDMinorAmount
	if usdMinorAmount <= 0 && req.USDAmount > 0 {
		_, cents, err := normalizeUSD(req.USDAmount)
		if err != nil {
			return normalizedReceiptVerificationInput{}, err
		}
		usdMinorAmount = cents
	}
	if usdMinorAmount < 0 {
		return normalizedReceiptVerificationInput{}, errors.New("充值美金数量不正确")
	}
	if req.ProviderAmountMinor < 0 {
		return normalizedReceiptVerificationInput{}, errors.New("三方实付金额不正确")
	}
	providerCountryCode := strings.ToUpper(strings.TrimSpace(req.ProviderCountryCode))
	providerCurrencyCode := strings.ToUpper(strings.TrimSpace(req.ProviderCurrencyCode))
	providerPayType := strings.TrimSpace(req.ProviderPayType)
	orderDateMS, err := normalizeOrderDateMS(req.OrderDate, req.OrderDateMS)
	if err != nil {
		return normalizedReceiptVerificationInput{}, err
	}
	if providerCode == "usdt" {
		if chain == "" {
			return normalizedReceiptVerificationInput{}, errors.New("USDT 充值必须选择链")
		}
		if usdMinorAmount <= 0 {
			return normalizedReceiptVerificationInput{}, errors.New("USDT 校验必须填写 USD 金额")
		}
	} else if requiresProviderCurrencyAmount(providerCode) {
		// 校验按钮是创建订单前的资金事实预检；MiFaPay/V5Pay 必须带运营填写的币种和实付金额，才能和 provider 回包做强比对。
		if providerCurrencyCode == "" {
			return normalizedReceiptVerificationInput{}, errors.New("三方币种不能为空")
		}
		if req.ProviderAmountMinor <= 0 {
			return normalizedReceiptVerificationInput{}, errors.New("三方实付金额不正确")
		}
	}
	return normalizedReceiptVerificationInput{
		AppCode:              appCode,
		USDMinorAmount:       usdMinorAmount,
		ProviderCode:         providerCode,
		Chain:                chain,
		ExternalOrderNo:      externalOrderNo,
		ProviderCountryCode:  providerCountryCode,
		ProviderCurrencyCode: providerCurrencyCode,
		ProviderAmountMinor:  req.ProviderAmountMinor,
		ProviderPayType:      providerPayType,
		OrderDateMS:          orderDateMS,
	}, nil
}

func (s *Service) verifyReceiptInput(ctx context.Context, input normalizedReceiptVerificationInput, requestID string) (coinSellerRechargeReceiptVerificationDTO, error) {
	if s == nil || s.wallet == nil {
		return coinSellerRechargeReceiptVerificationDTO{}, errors.New("finance order wallet verifier is not configured")
	}
	receiveAddress, err := s.financeUSDTReceiveAddress(input.AppCode, input.Chain)
	if err != nil {
		return coinSellerRechargeReceiptVerificationDTO{}, err
	}
	// admin-server 不直连三方支付和链节点；它只把当前 App 的 SQL 地址作为可信配置快照传给 wallet-service，
	// wallet-service 继续统一持有 provider 凭证和查单适配器，避免任一服务反向读取对方数据库。
	resp, err := s.wallet.VerifyCoinSellerRechargeReceipt(appctx.WithContext(ctx, input.AppCode), &walletv1.VerifyCoinSellerRechargeReceiptRequest{
		RequestId:            strings.TrimSpace(requestID),
		AppCode:              input.AppCode,
		ProviderCode:         receiptVerificationProviderCode(input),
		ExternalOrderNo:      input.ExternalOrderNo,
		Chain:                receiptVerificationChain(input),
		UsdMinorAmount:       input.USDMinorAmount,
		OrderDateMs:          input.OrderDateMS,
		ProviderCountryCode:  input.ProviderCountryCode,
		ProviderCurrencyCode: input.ProviderCurrencyCode,
		ProviderAmountMinor:  input.ProviderAmountMinor,
		ProviderPayType:      input.ProviderPayType,
		ReceiveAddress:       receiveAddress,
	})
	if err != nil {
		return coinSellerRechargeReceiptVerificationDTO{}, err
	}
	if resp == nil {
		return coinSellerRechargeReceiptVerificationDTO{}, errors.New("充值凭证校验返回为空")
	}
	return receiptVerificationDTOFromProto(resp), nil
}

func (s *Service) resolveCoinSellerTarget(ctx context.Context, requestID string, appCode string, rawTarget string) (coinSellerTarget, error) {
	if writer := s.legacyWriters[appCode]; writer != nil {
		target, err := writer.ResolveCoinSeller(ctx, rawTarget)
		if err != nil {
			return coinSellerTarget{}, err
		}
		return s.attachLegacyRegion(ctx, appCode, target)
	}
	if requiresLegacyWriter(appCode) {
		return coinSellerTarget{}, errors.New("legacy coin seller recharge writer is not configured")
	}
	if s.user == nil {
		return coinSellerTarget{}, errors.New("finance order user resolver is not configured")
	}
	ctx = appctx.WithContext(ctx, appCode)
	user, err := s.resolveWalletUser(ctx, strings.TrimSpace(requestID), rawTarget)
	if err != nil {
		return coinSellerTarget{}, err
	}
	if user.CountryID <= 0 || user.RegionID <= 0 {
		return coinSellerTarget{}, errors.New("币商国家或区域不完整")
	}
	summary, err := s.user.GetUserRoleSummary(ctx, userclient.GetUserRoleSummaryRequest{
		RequestID: strings.TrimSpace(requestID),
		Caller:    "admin-server",
		UserID:    user.UserID,
	})
	if err != nil {
		return coinSellerTarget{}, err
	}
	if summary == nil || !summary.IsCoinSeller {
		return coinSellerTarget{}, errors.New("目标用户不是启用中的币商")
	}
	return coinSellerTarget{
		UserID:        user.UserID,
		DisplayUserID: firstNonEmpty(user.PrettyDisplayUserID, user.DisplayUserID, user.DefaultDisplayUserID, formatInt64(user.UserID)),
		CountryID:     user.CountryID,
		RegionID:      user.RegionID,
	}, nil
}

// attachLegacyRegion 用国家码补齐 legacy 币商的区域归属。区域解析失败时创建必须失败：
// target_region_id=0 的订单会从所有区域分布和分区域授权账号的视图里消失，宁可让财务先补区域配置。
func (s *Service) attachLegacyRegion(ctx context.Context, appCode string, target coinSellerTarget) (coinSellerTarget, error) {
	if target.CountryCode == "" {
		return coinSellerTarget{}, errors.New("目标币商未配置国家，无法归因充值区域")
	}
	if s.legacyRegionResolve == nil {
		return coinSellerTarget{}, errors.New("legacy coin seller region resolver is not configured")
	}
	regionID, found, err := s.legacyRegionResolve(ctx, appCode, target.CountryCode)
	if err != nil {
		return coinSellerTarget{}, err
	}
	if !found || regionID <= 0 {
		return coinSellerTarget{}, fmt.Errorf("目标币商所在国家(%s)未划入任何区域，请先在区域配置中补齐", target.CountryCode)
	}
	target.RegionID = regionID
	return target, nil
}

func (s *Service) resolveWalletUser(ctx context.Context, requestID string, rawTarget string) (*userclient.User, error) {
	keyword := strings.TrimSpace(rawTarget)
	if keyword == "" {
		return nil, errors.New("目标用户ID不正确")
	}
	numericID, numericOK := parsePositiveInt64(keyword)
	userIDChecked := false
	if numericOK && len(strings.TrimLeft(keyword, "0")) >= financeOrderInternalUserIDMinDigits {
		userIDChecked = true
		if user, found, err := s.getWalletUserByID(ctx, requestID, numericID); err != nil || found {
			return user, err
		}
	}
	if identity, err := s.user.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{
		RequestID:     requestID,
		Caller:        "admin-server",
		DisplayUserID: keyword,
	}); err != nil {
		if !isGRPCNotFound(err) {
			return nil, err
		}
	} else if identity != nil && identity.UserID > 0 {
		user, found, err := s.getWalletUserByID(ctx, requestID, identity.UserID)
		if err != nil {
			return nil, err
		}
		if found {
			return user, nil
		}
	}
	if numericOK && !userIDChecked {
		if user, found, err := s.getWalletUserByID(ctx, requestID, numericID); err != nil || found {
			return user, err
		}
	}
	return nil, errors.New("目标用户不存在")
}

func (s *Service) getWalletUserByID(ctx context.Context, requestID string, userID int64) (*userclient.User, bool, error) {
	user, err := s.user.GetUser(ctx, userclient.GetUserRequest{
		RequestID: requestID,
		Caller:    "admin-server",
		UserID:    userID,
	})
	if err != nil {
		if isGRPCNotFound(err) {
			return nil, false, nil
		}
		return nil, false, err
	}
	if user == nil || user.UserID <= 0 {
		return nil, false, nil
	}
	return user, true, nil
}

type grantExecutionResult struct {
	CommandID     string
	TransactionID string
	AssetType     string
	AmountDelta   int64
	BalanceAfter  int64
}

func (s *Service) executeGrant(ctx context.Context, actor shared.Actor, order model.CoinSellerRechargeOrder, requestID string) (grantExecutionResult, error) {
	commandID := coinSellerRechargeWalletCommandID(order.ID)
	if order.CoinAmount == 0 {
		// 金币数为 0 是补单：订单仍然绑定外部充值凭证并计入充值台账，但不能写 Lalu 钱包或 legacy 进货流水。
		return grantExecutionResult{
			CommandID:    commandID,
			AssetType:    coinSellerRechargeAssetType,
			AmountDelta:  0,
			BalanceAfter: 0,
		}, nil
	}
	if writer := s.legacyWriters[order.AppCode]; writer != nil {
		result, err := writer.GrantCoinSellerRecharge(ctx, legacyRechargeGrantInput{
			OrderID:         order.ID,
			UserID:          order.TargetUserID,
			ExternalOrderNo: order.ExternalOrderNo,
			ProviderCode:    order.ProviderCode,
			Chain:           order.Chain,
			USDAmount:       order.USDAmount,
			CoinAmount:      order.CoinAmount,
			OperatorUserID:  actor.UserID,
			GrantedAt:       s.now(),
		})
		if err != nil {
			return grantExecutionResult{}, err
		}
		return grantExecutionResult{
			CommandID:     commandID,
			TransactionID: result.TransactionID,
			AssetType:     coinSellerRechargeAssetType,
			AmountDelta:   order.CoinAmount,
			BalanceAfter:  result.BalanceAfter,
		}, nil
	}
	if requiresLegacyWriter(order.AppCode) {
		return grantExecutionResult{}, errors.New("legacy coin seller recharge writer is not configured")
	}
	if s.wallet == nil {
		return grantExecutionResult{}, errors.New("finance order wallet executor is not configured")
	}
	ctx = appctx.WithContext(ctx, order.AppCode)
	resp, err := s.wallet.AdminCreditCoinSellerStock(ctx, &walletv1.AdminCreditCoinSellerStockRequest{
		CommandId:        commandID,
		SellerUserId:     order.TargetUserID,
		StockType:        coinSellerRechargeStockType,
		CoinAmount:       order.CoinAmount,
		PaidCurrencyCode: "USDT",
		PaidAmountMicro:  order.USDMinorAmount * 10_000,
		PaymentRef:       coinSellerRechargePaymentRef(order),
		EvidenceRef:      coinSellerRechargeEvidenceRef(order.ID),
		OperatorUserId:   int64(actor.UserID),
		Reason:           coinSellerRechargeReason(order),
		AppCode:          order.AppCode,
		SellerCountryId:  order.TargetCountryID,
		SellerRegionId:   order.TargetRegionID,
	})
	if err != nil {
		return grantExecutionResult{}, err
	}
	return grantExecutionResult{
		CommandID:     commandID,
		TransactionID: resp.GetTransactionId(),
		AssetType:     coinSellerRechargeAssetType,
		AmountDelta:   resp.GetCoinAmount(),
		BalanceAfter:  resp.GetBalanceAfter(),
	}, nil
}

func coinSellerRechargeVerifyInput(actor shared.Actor, resp *walletv1.VerifyCoinSellerRechargeReceiptResponse, failureReason string, nowMS int64) repository.CoinSellerRechargeOrderVerifyInput {
	verifiedAtMS := resp.GetVerifiedAtMs()
	if verifiedAtMS <= 0 {
		verifiedAtMS = nowMS
	}
	return repository.CoinSellerRechargeOrderVerifyInput{
		VerifiedByUserID:    actor.UserID,
		VerifiedByName:      actor.Username,
		VerifiedAtMS:        verifiedAtMS,
		ProviderOrderID:     resp.GetProviderOrderId(),
		Status:              resp.GetStatus(),
		CurrencyCode:        resp.GetCurrencyCode(),
		ProviderAmountMinor: resp.GetProviderAmountMinor(),
		ProviderPayloadJSON: receiptPayloadJSON(resp),
		ReceiveAddress:      resp.GetReceiveAddress(),
		FailureReason:       failureReason,
	}
}

func verifyReceiptMatchesOrder(order model.CoinSellerRechargeOrder, resp *walletv1.VerifyCoinSellerRechargeReceiptResponse) error {
	if !strings.EqualFold(resp.GetExternalOrderNo(), order.ExternalOrderNo) {
		return errors.New("校验返回订单号不匹配")
	}
	// USD/USDT 是后台账面金额，必须精确匹配；本地币种三方金额只在旧订单保存过期望值时才参与强校验。
	currency := strings.ToUpper(strings.TrimSpace(firstNonEmpty(resp.GetCurrencyCode(), order.ProviderCurrencyCode)))
	if order.ProviderCurrencyCode != "" && currency != "" && !strings.EqualFold(currency, order.ProviderCurrencyCode) {
		return errors.New("校验返回币种不匹配")
	}
	amount := resp.GetProviderAmountMinor()
	if amount <= 0 {
		return errors.New("校验返回金额为空")
	}
	if currency == "USD" || currency == "USDT" {
		if amount != order.USDMinorAmount {
			return errors.New("校验返回金额与订单美元金额不匹配")
		}
		return nil
	}
	if order.ProviderAmountMinor <= 0 {
		return nil
	}
	if amount != order.ProviderAmountMinor {
		return errors.New("校验返回金额与三方实付金额不匹配")
	}
	return nil
}

func verifyReceiptMatchesInput(input normalizedCreateOrderInput, receipt coinSellerRechargeReceiptVerificationDTO) error {
	if !strings.EqualFold(receipt.ExternalOrderNo, input.ExternalOrderNo) {
		return errors.New("校验返回订单号不匹配")
	}
	// USD/USDT 继续按后台美元金额核验；MiFaPay/V5Pay 的本地币种实付金额由表单强制填写，创建前再次和 provider 回包比对。
	currency := strings.ToUpper(strings.TrimSpace(firstNonEmpty(receipt.CurrencyCode, input.ProviderCurrencyCode)))
	if input.ProviderCurrencyCode != "" && currency != "" && !strings.EqualFold(currency, input.ProviderCurrencyCode) {
		return errors.New("校验返回币种不匹配")
	}
	if receipt.ProviderAmountMinor <= 0 {
		return errors.New("校验返回金额为空")
	}
	if currency == "USD" || currency == "USDT" {
		if receipt.ProviderAmountMinor != input.USDMinorAmount {
			return errors.New("校验返回金额与订单美元金额不匹配")
		}
		return nil
	}
	if input.ProviderAmountMinor > 0 && receipt.ProviderAmountMinor != input.ProviderAmountMinor {
		return errors.New("校验返回金额与三方实付金额不匹配")
	}
	return nil
}

func receiptPayloadJSON(resp *walletv1.VerifyCoinSellerRechargeReceiptResponse) string {
	if raw := strings.TrimSpace(resp.GetRawJson()); raw != "" {
		return raw
	}
	body, _ := json.Marshal(map[string]any{
		"verified":              resp.GetVerified(),
		"provider_code":         resp.GetProviderCode(),
		"external_order_no":     resp.GetExternalOrderNo(),
		"provider_order_id":     resp.GetProviderOrderId(),
		"status":                resp.GetStatus(),
		"currency_code":         resp.GetCurrencyCode(),
		"provider_amount_minor": resp.GetProviderAmountMinor(),
		"chain":                 resp.GetChain(),
		"receive_address":       resp.GetReceiveAddress(),
		"failure_reason":        resp.GetFailureReason(),
	})
	return string(body)
}

func receiptVerificationDTOFromProto(resp *walletv1.VerifyCoinSellerRechargeReceiptResponse) coinSellerRechargeReceiptVerificationDTO {
	if resp == nil {
		return coinSellerRechargeReceiptVerificationDTO{}
	}
	return coinSellerRechargeReceiptVerificationDTO{
		Verified:            resp.GetVerified(),
		ProviderCode:        resp.GetProviderCode(),
		ExternalOrderNo:     resp.GetExternalOrderNo(),
		ProviderOrderID:     resp.GetProviderOrderId(),
		Status:              resp.GetStatus(),
		CurrencyCode:        strings.ToUpper(strings.TrimSpace(resp.GetCurrencyCode())),
		ProviderAmountMinor: resp.GetProviderAmountMinor(),
		Chain:               resp.GetChain(),
		ReceiveAddress:      resp.GetReceiveAddress(),
		VerifiedAtMS:        resp.GetVerifiedAtMs(),
		FailureReason:       resp.GetFailureReason(),
		RawJSON:             receiptPayloadJSON(resp),
	}
}

func normalizeRechargeProvider(providerCode string, chain string) (string, string, error) {
	provider := strings.ToLower(strings.TrimSpace(providerCode))
	provider = strings.ReplaceAll(provider, "-", "_")
	normalizedChain := normalizeRechargeChain(chain)
	if strings.TrimSpace(chain) != "" && normalizedChain == "" {
		return "", "", errors.New("USDT 链不正确")
	}
	switch provider {
	case "mifapy", "mifapay":
		return "mifapay", "", nil
	case "v5pay":
		return "v5pay", "", nil
	case "usdt":
		return "usdt", normalizedChain, nil
	case "usdt_trc20", "trc20", "tron":
		return "usdt", "TRON", nil
	case "usdt_bep20", "bep20", "bsc", "bnb":
		return "usdt", "BSC", nil
	case "usdt_c2c", "c2c":
		return "usdt", "C2C", nil
	case "usdt_offchain", "offchain", "binance_offchain":
		return "usdt", "OFFCHAIN", nil
	default:
		return "", "", errors.New("充值方式不正确")
	}
}

func normalizeRechargeChain(chain string) string {
	chain = strings.ToLower(strings.TrimSpace(chain))
	chain = strings.ReplaceAll(chain, "-", "_")
	switch chain {
	case "", "none":
		return ""
	case "tron", "trx", "trc20", "usdt_trc20":
		return "TRON"
	case "bsc", "bnb", "bep20", "usdt_bep20":
		return "BSC"
	case "c2c", "usdt_c2c":
		return "C2C"
	case "offchain", "off_chain", "usdt_offchain", "binance_offchain":
		return "OFFCHAIN"
	default:
		return ""
	}
}

func receiptProviderCode(order model.CoinSellerRechargeOrder) string {
	if order.ProviderCode != "usdt" {
		return order.ProviderCode
	}
	switch strings.ToUpper(strings.TrimSpace(order.Chain)) {
	case "TRON":
		return "usdt_trc20"
	case "BSC":
		return "usdt_bep20"
	case "C2C":
		return "usdt_c2c"
	case "OFFCHAIN":
		return "usdt_offchain"
	default:
		return "usdt"
	}
}

func receiptVerificationProviderCode(input normalizedReceiptVerificationInput) string {
	return receiptProviderCode(model.CoinSellerRechargeOrder{ProviderCode: input.ProviderCode, Chain: input.Chain})
}

func requiresProviderCurrencyAmount(providerCode string) bool {
	switch strings.ToLower(strings.TrimSpace(providerCode)) {
	case "mifapay", "v5pay":
		return true
	default:
		return false
	}
}

func receiptChain(order model.CoinSellerRechargeOrder) string {
	switch strings.ToUpper(strings.TrimSpace(order.Chain)) {
	case "TRON":
		return "trc20"
	case "BSC":
		return "bep20"
	case "C2C":
		return "c2c"
	case "OFFCHAIN":
		return "offchain"
	default:
		return ""
	}
}

func receiptVerificationChain(input normalizedReceiptVerificationInput) string {
	return receiptChain(model.CoinSellerRechargeOrder{ProviderCode: input.ProviderCode, Chain: input.Chain})
}

func normalizeUSD(amount float64) (string, int64, error) {
	if math.IsNaN(amount) || math.IsInf(amount, 0) || amount <= 0 {
		return "", 0, errors.New("充值美金数量不正确")
	}
	cents := int64(math.Round(amount * 100))
	if cents <= 0 {
		return "", 0, errors.New("充值美金数量不正确")
	}
	return fmt.Sprintf("%d.%02d", cents/100, cents%100), cents, nil
}

func normalizeOrderDateMS(orderDate string, orderDateMS int64) (int64, error) {
	if orderDateMS > 0 {
		return orderDateMS, nil
	}
	text := strings.TrimSpace(orderDate)
	if text == "" {
		return 0, nil
	}
	for _, layout := range []string{"2006-01-02", "2006/01/02", time.RFC3339} {
		if parsed, err := time.ParseInLocation(layout, text, time.UTC); err == nil {
			return parsed.UTC().UnixMilli(), nil
		}
	}
	return 0, errors.New("订单日期不正确")
}

func normalizeCreateOrderBusinessTime(orderDateMS int64, nowMS int64) (int64, error) {
	if nowMS <= 0 {
		return 0, errors.New("当前时间不正确")
	}
	if orderDateMS <= 0 {
		return nowMS, nil
	}
	if orderDateMS > nowMS {
		return 0, errors.New("订单时间不能晚于当前时间")
	}
	return orderDateMS, nil
}

func requiresLegacyWriter(appCode string) bool {
	switch appctx.Normalize(appCode) {
	case "aslan", "yumi":
		return true
	default:
		return false
	}
}

func coinSellerRechargeWalletCommandID(orderID uint) string {
	return fmt.Sprintf("admin-coin-seller-recharge:%d", orderID)
}

func coinSellerRechargeEvidenceRef(orderID uint) string {
	return fmt.Sprintf("admin-coin-seller-recharge:%d", orderID)
}

func coinSellerRechargePaymentRef(order model.CoinSellerRechargeOrder) string {
	if order.ProviderCode == "usdt" {
		return "usdt:" + strings.ToLower(strings.TrimSpace(order.Chain)) + ":" + order.ExternalOrderNo
	}
	return order.ProviderCode + ":" + order.ExternalOrderNo
}

func coinSellerRechargeReason(order model.CoinSellerRechargeOrder) string {
	return fmt.Sprintf("coin seller recharge order %d %s", order.ID, coinSellerRechargePaymentRef(order))
}

func parsePositiveInt64(value string) (int64, bool) {
	parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
	return parsed, err == nil && parsed > 0
}

func formatInt64(value int64) string {
	return strconv.FormatInt(value, 10)
}

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

func hasPermission(permissions []string, code string) bool {
	for _, permission := range permissions {
		if permission == code {
			return true
		}
	}
	return false
}

func isGRPCNotFound(err error) bool {
	return status.Code(err) == codes.NotFound
}

func isDuplicateKeyError(err error) bool {
	text := strings.ToLower(strings.TrimSpace(err.Error()))
	return strings.Contains(text, "duplicate") || strings.Contains(text, "unique constraint")
}
