package payment

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/repository"
	walletv1 "hyapp.local/api/proto/wallet/v1"
)

type financeCoinSellerRechargeQuery struct {
	RechargeType      string
	Status            string
	PaidState         string
	Keyword           string
	RegionID          int64
	StartAtMS         int64
	EndAtMS           int64
	TzOffsetMinutes   int32
	TargetUserID      int64
	HasUserOnlyFilter bool
	Page              int
	PageSize          int
	MaxPageSize       int
	RequestID         string
	UserID            int64
}

type financeCoinSellerRechargeStats struct {
	Total   rechargeBillSummaryBucketDTO
	Daily   map[string]rechargeBillSummaryBucketDTO
	Regions []rechargeBillRegionBucketDTO
}

const (
	financeCoinSellerRechargeMaxPageSize = 100
	financeRechargeMergeMaxPrefetch      = 5000
	financeCoinSellerRechargeType        = "coin_seller"
	financeCoinSellerThirdPartyType      = "coin_seller_third_party"
	financeCoinSellerMiFaPayType         = "coin_seller_mifapay"
	financeCoinSellerV5PayType           = "coin_seller_v5pay"
	financeCoinSellerUSDTType            = "coin_seller_usdt"

	// 2^53 以内的 int64 在 float64 里精确可表示，不会被浏览器 JSON 圆整；
	// 只有 legacy 合成/雪花区域 ID(远大于 2^53)需要按目录归一后才能做 SQL 精确等值。
	maxExactFloat64RegionID = int64(1) << 53
)

// financeCoinSellerRechargeSource 把 API 的充值来源筛选收敛为台账查询分组。
// coin_seller 保持历史兼容并同时包含两类；细分值只影响币商充值，不改变普通三方/Google 的既有 recharge_type 契约。
func financeCoinSellerRechargeSource(rechargeType string) (string, bool) {
	switch normalizedRechargeType(rechargeType) {
	case "", financeCoinSellerRechargeType:
		return "", true
	case financeCoinSellerThirdPartyType:
		return repository.CoinSellerRechargeSourceThirdParty, true
	case financeCoinSellerMiFaPayType:
		return repository.CoinSellerRechargeSourceMiFaPay, true
	case financeCoinSellerV5PayType:
		return repository.CoinSellerRechargeSourceV5Pay, true
	case financeCoinSellerUSDTType:
		return repository.CoinSellerRechargeSourceUSDT, true
	default:
		return "", false
	}
}

func isFinanceCoinSellerRechargeType(rechargeType string) bool {
	_, ok := financeCoinSellerRechargeSource(rechargeType)
	return ok && normalizedRechargeType(rechargeType) != ""
}

func financeCoinSellerBillType(providerCode string) string {
	// Legacy Mongo 使用 MIFA_PAY/V5_PAY，新台账和 wallet 使用 mifapay/v5pay；去掉分隔符后才能得到同一财务厂商口径。
	normalized := strings.NewReplacer("_", "", "-", "", " ", "").Replace(strings.ToLower(strings.TrimSpace(providerCode)))
	switch normalized {
	case "mifapay":
		return financeCoinSellerMiFaPayType
	case "v5pay":
		return financeCoinSellerV5PayType
	case "usdt", "usdttrc20":
		return financeCoinSellerUSDTType
	default:
		// 未知历史三方不能被误标为任一厂商；保留聚合标签，仍可从 ProviderCode 审计原值。
		return financeCoinSellerThirdPartyType
	}
}

// normalizeFinanceCoinSellerRegionID 把请求里可能被前端 float64 圆整过的 legacy 区域 ID
// 还原成区域目录里的精确值；目录不可用或没有对应项时原样返回（与 databi normalizeRegionID 同语义）。
func (h *Handler) normalizeFinanceCoinSellerRegionID(ctx context.Context, appCode string, regionID int64) int64 {
	if h == nil || regionID <= maxExactFloat64RegionID {
		return regionID
	}
	regions, err := h.listRechargeBillRegions(ctx, appCode)
	if err != nil {
		return regionID
	}
	for _, region := range regions {
		if repository.RegionIDLooseEqual(region.RegionID, regionID) {
			return region.RegionID
		}
	}
	return regionID
}

func (h *Handler) applyFinanceCoinSellerSummary(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, summary *rechargeBillSummaryDTO) error {
	if summary == nil {
		return nil
	}
	rechargeType := normalizedRechargeType(query.RechargeType)
	_, coinSellerType := financeCoinSellerRechargeSource(rechargeType)
	if !coinSellerType {
		summary.CoinSeller = rechargeBillSummaryBucketDTO{}
		legacyRecomputeSummaryTotal(summary)
		return nil
	}
	stats, err := h.financeCoinSellerRechargeStats(ctx, appCode, query)
	if err != nil {
		return err
	}
	summary.CoinSeller = stats.Total
	if rechargeType != "" {
		summary.GooglePlay = rechargeBillSummaryBucketDTO{}
		summary.ThirdParty = rechargeBillSummaryBucketDTO{}
	}
	legacyRecomputeSummaryTotal(summary)
	return nil
}

func (h *Handler) applyFinanceCoinSellerOverview(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, overview *rechargeBillOverviewDTO) error {
	if overview == nil {
		return nil
	}
	rechargeType := normalizedRechargeType(query.RechargeType)
	_, coinSellerType := financeCoinSellerRechargeSource(rechargeType)
	if !coinSellerType {
		for i := range overview.Daily {
			overview.Daily[i].CoinSellerUsdMinor = 0
			overview.Daily[i].CoinSellerCoinAmount = 0
		}
		return nil
	}
	stats, err := h.financeCoinSellerRechargeStats(ctx, appCode, query)
	if err != nil {
		return err
	}
	byDate := map[string]int{}
	for i := range overview.Daily {
		byDate[overview.Daily[i].Date] = i
		// 财务工作台的币商充值只认后台币商订单 + H5 币商身份充值；wallet 库存流水和 legacy dashboard 旧聚合先清掉再重填。
		overview.Daily[i].CoinSellerUsdMinor = 0
		overview.Daily[i].CoinSellerCoinAmount = 0
	}
	for date, bucket := range stats.Daily {
		if date == "" {
			continue
		}
		index, ok := byDate[date]
		if !ok {
			overview.Daily = append(overview.Daily, rechargeBillDailyBucketDTO{Date: date})
			index = len(overview.Daily) - 1
			byDate[date] = index
		}
		overview.Daily[index].CoinSellerUsdMinor = bucket.USDMinorAmount
		overview.Daily[index].CoinSellerCoinAmount = bucket.CoinAmount
	}
	sort.Slice(overview.Daily, func(i, j int) bool {
		return overview.Daily[i].Date < overview.Daily[j].Date
	})
	if rechargeType != "" {
		overview.Regions = h.fillFinanceCoinSellerRegionNames(ctx, appCode, stats.Regions)
		return nil
	}
	if rechargeType == "" {
		overview.Regions = h.fillFinanceCoinSellerRegionNames(ctx, appCode, mergeRechargeRegionBuckets(overview.Regions, stats.Regions))
	}
	return nil
}

func (h *Handler) financeCoinSellerRechargeStats(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery) (financeCoinSellerRechargeStats, error) {
	stats := financeCoinSellerRechargeStats{Daily: map[string]rechargeBillSummaryBucketDTO{}}
	rechargeSource, coinSellerType := financeCoinSellerRechargeSource(query.RechargeType)
	if !coinSellerType {
		return stats, nil
	}
	if !financeCoinSellerSucceededStatus(query.Status) || strings.TrimSpace(query.PaidState) != "" || query.HasUserOnlyFilter {
		return stats, nil
	}
	query.RegionID = h.normalizeFinanceCoinSellerRegionID(ctx, appCode, query.RegionID)
	if rechargeSource != repository.CoinSellerRechargeSourceUSDT {
		paidSource := h.financePaidCoinSellerRechargeSource(appCode)
		if paidSource != nil {
			paidStats, err := paidSource.financePaidCoinSellerRechargeStats(ctx, query)
			if err != nil {
				return stats, err
			}
			// Aslan legacy H5 的 SUCCESS/FREIGHT_GOLD Mongo 订单是支付账单事实；
			// 这里只合并该事实，不读取 WebConsole 手填金额或钱包发货流水。
			stats.addBucket(paidStats.Total)
			for date, bucket := range paidStats.Daily {
				stats.addDaily(date, bucket)
			}
			for _, region := range paidStats.Regions {
				stats.addRegion(region.RegionID, region.BillCount, region.UsdMinorAmount)
			}
		}
	}
	if h != nil && h.store != nil {
		repoStats, err := h.store.CoinSellerRechargeOrderStats(repository.CoinSellerRechargeOrderStatsOptions{
			AppCode:         appCode,
			RechargeSource:  rechargeSource,
			RegionID:        query.RegionID,
			TargetUserID:    query.TargetUserID,
			Keyword:         query.Keyword,
			CreatedFromMS:   query.StartAtMS,
			CreatedToMS:     query.EndAtMS,
			TzOffsetMinutes: query.TzOffsetMinutes,
		})
		if err != nil {
			return stats, err
		}
		stats.addBucket(rechargeBillSummaryBucketDTO{
			BillCount:      repoStats.VerifiedCount,
			CoinAmount:     repoStats.VerifiedCoinAmount,
			USDMinorAmount: repoStats.VerifiedUSDMinor,
		})
		for date, bucket := range repoStats.VerifiedDaily {
			stats.addDaily(date, rechargeBillSummaryBucketDTO{BillCount: bucket.Count, CoinAmount: bucket.CoinAmount, USDMinorAmount: bucket.USDMinor})
		}
		for _, row := range repoStats.VerifiedRegionBuckets {
			stats.addRegion(row.RegionID, row.Count, row.USDMinor)
		}
	}
	if rechargeSource != repository.CoinSellerRechargeSourceUSDT && h != nil && h.walletDB != nil {
		if err := h.addH5CoinSellerRechargeStats(ctx, appCode, query, &stats); err != nil {
			return stats, err
		}
	}
	sort.Slice(stats.Regions, func(i, j int) bool {
		return stats.Regions[i].UsdMinorAmount > stats.Regions[j].UsdMinorAmount
	})
	return stats, nil
}

func (s *financeCoinSellerRechargeStats) addBucket(bucket rechargeBillSummaryBucketDTO) {
	if s == nil {
		return
	}
	s.Total.BillCount += bucket.BillCount
	s.Total.CoinAmount += bucket.CoinAmount
	s.Total.USDMinorAmount += bucket.USDMinorAmount
}

func (s *financeCoinSellerRechargeStats) addDaily(date string, bucket rechargeBillSummaryBucketDTO) {
	if s == nil || date == "" {
		return
	}
	if s.Daily == nil {
		s.Daily = map[string]rechargeBillSummaryBucketDTO{}
	}
	current := s.Daily[date]
	current.BillCount += bucket.BillCount
	current.CoinAmount += bucket.CoinAmount
	current.USDMinorAmount += bucket.USDMinorAmount
	s.Daily[date] = current
}

func (s *financeCoinSellerRechargeStats) addRegion(regionID int64, billCount int64, usdMinor int64) {
	if s == nil || regionID <= 0 {
		return
	}
	for i := range s.Regions {
		if s.Regions[i].RegionID == regionID {
			s.Regions[i].BillCount += billCount
			s.Regions[i].UsdMinorAmount += usdMinor
			return
		}
	}
	s.Regions = append(s.Regions, rechargeBillRegionBucketDTO{RegionID: regionID, BillCount: billCount, UsdMinorAmount: usdMinor})
}

func (h *Handler) addH5CoinSellerRechargeStats(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, stats *financeCoinSellerRechargeStats) error {
	where, args := h5CoinSellerRechargeWhere(appCode, query)
	var total rechargeBillSummaryBucketDTO
	if err := h.walletDB.QueryRowContext(ctx, `
		SELECT COUNT(*), COALESCE(SUM(eo.coin_amount), 0), COALESCE(SUM(eo.usd_minor_amount), 0)
		FROM external_recharge_orders eo
		JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
		`+where, args...).Scan(&total.BillCount, &total.CoinAmount, &total.USDMinorAmount); err != nil {
		return err
	}
	stats.addBucket(total)

	tzOffsetMS := int64(query.TzOffsetMinutes) * 60_000
	dailyArgs := append([]any{tzOffsetMS}, args...)
	rows, err := h.walletDB.QueryContext(ctx, `
		SELECT DATE_FORMAT(FROM_UNIXTIME((eo.updated_at_ms + ?) / 1000), '%Y-%m-%d') AS date,
		       COUNT(*), COALESCE(SUM(eo.coin_amount), 0), COALESCE(SUM(eo.usd_minor_amount), 0)
		FROM external_recharge_orders eo
		JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
		`+where+`
		GROUP BY date
		ORDER BY date ASC`, dailyArgs...)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var date string
		var bucket rechargeBillSummaryBucketDTO
		if err := rows.Scan(&date, &bucket.BillCount, &bucket.CoinAmount, &bucket.USDMinorAmount); err != nil {
			return err
		}
		stats.addDaily(date, bucket)
	}
	if err := rows.Err(); err != nil {
		return err
	}

	regionRows, err := h.walletDB.QueryContext(ctx, `
		SELECT eo.target_region_id, COUNT(*), COALESCE(SUM(eo.usd_minor_amount), 0)
		FROM external_recharge_orders eo
		JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
		`+where+`
		GROUP BY eo.target_region_id
		ORDER BY 3 DESC`, args...)
	if err != nil {
		return err
	}
	defer regionRows.Close()
	for regionRows.Next() {
		var regionID, billCount, usdMinor int64
		if err := regionRows.Scan(&regionID, &billCount, &usdMinor); err != nil {
			return err
		}
		stats.addRegion(regionID, billCount, usdMinor)
	}
	return regionRows.Err()
}

func (h *Handler) listFinanceCoinSellerRechargeBills(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery) ([]rechargeBillDTO, int64, error) {
	rechargeSource, coinSellerType := financeCoinSellerRechargeSource(query.RechargeType)
	if !coinSellerType {
		return []rechargeBillDTO{}, 0, nil
	}
	if !financeCoinSellerSucceededStatus(query.Status) || strings.TrimSpace(query.PaidState) != "" || query.HasUserOnlyFilter {
		return []rechargeBillDTO{}, 0, nil
	}
	query.RegionID = h.normalizeFinanceCoinSellerRegionID(ctx, appCode, query.RegionID)
	page, pageSize := financeCoinSellerPage(query.Page, query.PageSize, query.MaxPageSize)
	prefetch := financeRechargePrefetch(page, pageSize, query.MaxPageSize)
	items := make([]rechargeBillDTO, 0, prefetch*2)
	var total int64
	if rechargeSource != repository.CoinSellerRechargeSourceUSDT {
		paidSource := h.financePaidCoinSellerRechargeSource(appCode)
		if paidSource != nil {
			paidItems, paidTotal, err := paidSource.listFinancePaidCoinSellerRechargeBills(ctx, query, prefetch)
			if err != nil {
				return nil, 0, err
			}
			// 真实 H5 账单先进入结果；若同一个支付订单后来又被后台绑定，去重时保留原始账单金额和商品金币。
			items = append(items, paidItems...)
			total += paidTotal
		}
	}
	if h != nil && h.store != nil {
		orders, orderTotal, err := h.store.ListCoinSellerRechargeOrders(repository.CoinSellerRechargeOrderListOptions{
			Page:           1,
			PageSize:       prefetch,
			PageSizeLimit:  prefetch,
			AppCode:        appCode,
			VerifyStatus:   model.CoinSellerRechargeVerifyStatusVerified,
			RechargeSource: rechargeSource,
			Keyword:        query.Keyword,
			TargetUserID:   query.TargetUserID,
			RegionID:       query.RegionID,
			CreatedFromMS:  query.StartAtMS,
			CreatedToMS:    query.EndAtMS,
		})
		if err != nil {
			return nil, 0, err
		}
		total += orderTotal
		for _, order := range orders {
			if query.RegionID > 0 && order.TargetRegionID != query.RegionID {
				continue
			}
			items = append(items, financeCoinSellerRechargeOrderBill(order))
		}
	}
	if rechargeSource != repository.CoinSellerRechargeSourceUSDT && h != nil && h.walletDB != nil {
		h5Items, h5Total, err := h.listH5CoinSellerRechargeBills(ctx, appCode, query, prefetch)
		if err != nil {
			return nil, 0, err
		}
		total += h5Total
		items = append(items, h5Items...)
	}
	var duplicateCount int
	items, duplicateCount = deduplicateFinanceCoinSellerRechargeBills(items)
	if duplicateCount > 0 {
		total -= int64(duplicateCount)
		if total < 0 {
			total = 0
		}
	}
	sortRechargeBills(items)
	offset := (page - 1) * pageSize
	if offset >= len(items) {
		return []rechargeBillDTO{}, total, nil
	}
	end := offset + pageSize
	if end > len(items) {
		end = len(items)
	}
	return items[offset:end], total, nil
}

func deduplicateFinanceCoinSellerRechargeBills(items []rechargeBillDTO) ([]rechargeBillDTO, int) {
	seen := make(map[string]struct{}, len(items))
	out := make([]rechargeBillDTO, 0, len(items))
	duplicates := 0
	for _, item := range items {
		identity := financeCoinSellerRechargeBillIdentity(item)
		if identity == "" {
			out = append(out, item)
			continue
		}
		if _, exists := seen[identity]; exists {
			duplicates++
			continue
		}
		seen[identity] = struct{}{}
		out = append(out, item)
	}
	return out, duplicates
}

func financeCoinSellerRechargeBillIdentity(item rechargeBillDTO) string {
	externalRef := strings.ToLower(strings.TrimSpace(item.ExternalRef))
	if externalRef != "" {
		provider := strings.NewReplacer("_", "", "-", "", " ", "").Replace(strings.ToLower(strings.TrimSpace(item.ProviderCode)))
		return "provider:" + provider + ":" + externalRef
	}
	transactionID := strings.ToLower(strings.TrimSpace(item.TransactionID))
	if transactionID == "" {
		return ""
	}
	return "transaction:" + transactionID
}

func (h *Handler) listFinanceAllRechargeBills(ctx context.Context, appCode string, source RechargeBillSource, query financeCoinSellerRechargeQuery) ([]rechargeBillDTO, int64, error) {
	page, pageSize := financeCoinSellerPage(query.Page, query.PageSize, query.MaxPageSize)
	prefetch := financeRechargePrefetch(page, pageSize, query.MaxPageSize)
	items := make([]rechargeBillDTO, 0, prefetch*3)
	var total int64

	if source != nil {
		if !query.HasUserOnlyFilter && query.TargetUserID == 0 {
			// legacy 普通充值源没有 user/seller 过滤能力；有用户筛选时宁可不展示普通源，避免 all 视图返回未过滤全量。
			ordinary, ordinaryTotal, err := source.ListRechargeBills(ctx, legacyRechargeBillQuery{
				Keyword:         query.Keyword,
				RechargeType:    "",
				PaidState:       query.PaidState,
				RegionID:        query.RegionID,
				StartAtMS:       query.StartAtMS,
				EndAtMS:         query.EndAtMS,
				TzOffsetMinutes: query.TzOffsetMinutes,
				Page:            1,
				PageSize:        prefetch,
			})
			if err != nil {
				return nil, 0, err
			}
			items = append(items, ordinary...)
			total += ordinaryTotal
		}
	} else if h != nil && h.wallet != nil {
		// wallet 的空 recharge_type 会并入 coin_seller_stock_records；all 视图必须拆成普通三方和 Google 两路，币商改走财务新口径。
		for _, rechargeType := range []string{"third_party", "google_play_recharge"} {
			resp, err := h.wallet.ListRechargeBills(ctx, &walletv1.ListRechargeBillsRequest{
				RequestId:    query.RequestID,
				AppCode:      appCode,
				UserId:       query.UserID,
				SellerUserId: query.TargetUserID,
				RegionId:     query.RegionID,
				RechargeType: rechargeType,
				PaidState:    query.PaidState,
				Status:       query.Status,
				Keyword:      query.Keyword,
				StartAtMs:    query.StartAtMS,
				EndAtMs:      query.EndAtMS,
				Page:         1,
				PageSize:     int32(prefetch),
			})
			if err != nil {
				return nil, 0, err
			}
			total += resp.GetTotal()
			for _, item := range resp.GetBills() {
				items = append(items, rechargeBillFromProto(item))
			}
		}
	}

	coinItems, coinTotal, err := h.listFinanceCoinSellerRechargeBills(ctx, appCode, financeCoinSellerRechargeQuery{
		RechargeType:      financeCoinSellerRechargeType,
		Status:            query.Status,
		PaidState:         query.PaidState,
		Keyword:           query.Keyword,
		RegionID:          query.RegionID,
		StartAtMS:         query.StartAtMS,
		EndAtMS:           query.EndAtMS,
		TzOffsetMinutes:   query.TzOffsetMinutes,
		TargetUserID:      query.TargetUserID,
		HasUserOnlyFilter: query.HasUserOnlyFilter,
		Page:              1,
		PageSize:          prefetch,
		MaxPageSize:       prefetch,
	})
	if err != nil {
		return nil, 0, err
	}
	items = append(items, coinItems...)
	total += coinTotal

	sortRechargeBills(items)
	offset := (page - 1) * pageSize
	if offset >= len(items) {
		return []rechargeBillDTO{}, total, nil
	}
	end := offset + pageSize
	if end > len(items) {
		end = len(items)
	}
	return items[offset:end], total, nil
}

func (h *Handler) walletOrdinaryRechargeOverviewRegions(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery) ([]rechargeBillRegionBucketDTO, error) {
	if h == nil || h.wallet == nil {
		return []rechargeBillRegionBucketDTO{}, nil
	}
	if !financeWalletOverviewSupportsOrdinaryFilters(query) {
		// wallet overview proto 没有用户、币商、关键词和 paid_state 字段；过滤条件存在时返回空区域，避免展示未过滤的全量区域。
		return []rechargeBillRegionBucketDTO{}, nil
	}
	var out []rechargeBillRegionBucketDTO
	for _, rechargeType := range []string{"third_party", "google_play_recharge"} {
		resp, err := h.wallet.GetRechargeBillOverview(ctx, &walletv1.GetRechargeBillOverviewRequest{
			RequestId:       query.RequestID,
			AppCode:         appCode,
			RegionId:        query.RegionID,
			RechargeType:    rechargeType,
			Status:          query.Status,
			StartAtMs:       query.StartAtMS,
			EndAtMs:         query.EndAtMS,
			TzOffsetMinutes: query.TzOffsetMinutes,
		})
		if err != nil {
			return nil, err
		}
		for _, bucket := range resp.GetRegions() {
			out = append(out, rechargeBillRegionBucketDTO{
				RegionID:       bucket.GetRegionId(),
				BillCount:      bucket.GetBillCount(),
				UsdMinorAmount: bucket.GetUsdMinorAmount(),
			})
		}
	}
	return mergeRechargeRegionBuckets(out), nil
}

func financeWalletOverviewSupportsOrdinaryFilters(query financeCoinSellerRechargeQuery) bool {
	return query.UserID == 0 && query.TargetUserID == 0 && strings.TrimSpace(query.Keyword) == "" && strings.TrimSpace(query.PaidState) == ""
}

func (h *Handler) listH5CoinSellerRechargeBills(ctx context.Context, appCode string, query financeCoinSellerRechargeQuery, limit int) ([]rechargeBillDTO, int64, error) {
	where, args := h5CoinSellerRechargeWhere(appCode, query)
	var total int64
	if err := h.walletDB.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM external_recharge_orders eo
		JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
		`+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	if limit < 1 {
		return []rechargeBillDTO{}, total, nil
	}
	rows, err := h.walletDB.QueryContext(ctx, `
		SELECT eo.app_code, eo.order_id, wt.command_id, eo.target_user_id, eo.target_region_id,
		       eo.product_id, eo.product_code, eo.product_name, eo.coin_amount, eo.usd_minor_amount,
		       eo.provider_code, eo.currency_code, eo.provider_amount_minor, eo.provider_order_id,
		       eo.wallet_transaction_id, eo.updated_at_ms, eo.created_at_ms
		FROM external_recharge_orders eo
		JOIN wallet_transactions wt ON wt.app_code = eo.app_code AND wt.transaction_id = eo.wallet_transaction_id
		`+where+`
		ORDER BY eo.updated_at_ms DESC, eo.order_id DESC
		LIMIT ?`, append(args, limit)...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	items := make([]rechargeBillDTO, 0, limit)
	for rows.Next() {
		var item rechargeBillDTO
		var productID int64
		var productCode, productName, providerOrderID, walletTransactionID string
		var createdAtMS int64
		if err := rows.Scan(
			&item.AppCode,
			&item.ExternalRef,
			&item.CommandID,
			&item.SellerUserID,
			&item.SellerRegionID,
			&productID,
			&productCode,
			&productName,
			&item.CoinAmount,
			&item.USDMinorAmount,
			&item.ProviderCode,
			&item.UserPaidCurrencyCode,
			&item.ProviderAmountMinor,
			&providerOrderID,
			&walletTransactionID,
			&item.CreatedAtMS,
			&createdAtMS,
		); err != nil {
			return nil, 0, err
		}
		item.TransactionID = firstNonEmptyPaymentString(walletTransactionID, item.ExternalRef)
		// H5 订单直接使用支付事实里的 provider_code，避免厂商筛选后明细仍退化成笼统“三方”。
		item.RechargeType = financeCoinSellerBillType(item.ProviderCode)
		item.Status = "succeeded"
		item.TargetRegionID = item.SellerRegionID
		item.PolicyID = productID
		item.PolicyVersion = productCode
		item.CurrencyCode = "USD"
		item.UserPaidAmountMicro = item.ProviderAmountMinor * 10_000
		item.PaidSyncedAtMS = item.CreatedAtMS
		if item.UserPaidCurrencyCode == "" {
			item.UserPaidCurrencyCode = "USD"
			item.UserPaidAmountMicro = item.USDMinorAmount * 10_000
		}
		if providerOrderID != "" {
			item.ExternalRef = providerOrderID
		}
		if item.CreatedAtMS == 0 {
			item.CreatedAtMS = createdAtMS
		}
		items = append(items, item)
	}
	return items, total, rows.Err()
}

func financeCoinSellerRechargeOrderBill(order model.CoinSellerRechargeOrder) rechargeBillDTO {
	currency := firstNonEmptyPaymentString(order.ProviderCurrencyCode, "USD")
	providerAmountMinor := order.ProviderAmountMinor
	userPaidAmountMicro := providerAmountMinor * 10_000
	if providerAmountMinor == 0 {
		userPaidAmountMicro = order.USDMinorAmount * 10_000
	}
	return rechargeBillDTO{
		AppCode:              order.AppCode,
		TransactionID:        firstNonEmptyPaymentString(order.WalletTransactionID, fmt.Sprintf("coin-seller-order:%d", order.ID)),
		CommandID:            order.WalletCommandID,
		RechargeType:         financeCoinSellerBillType(order.ProviderCode),
		Status:               "succeeded",
		ExternalRef:          firstNonEmptyPaymentString(order.ProviderOrderID, order.ExternalOrderNo),
		SellerUserID:         order.TargetUserID,
		SellerRegionID:       order.TargetRegionID,
		TargetRegionID:       order.TargetRegionID,
		CurrencyCode:         "USD",
		CoinAmount:           order.CoinAmount,
		USDMinorAmount:       order.USDMinorAmount,
		ProviderAmountMinor:  providerAmountMinor,
		CreatedAtMS:          order.BusinessTimeMS(),
		ProviderCode:         order.ProviderCode,
		UserPaidCurrencyCode: currency,
		UserPaidAmountMicro:  userPaidAmountMicro,
		PaidSyncedAtMS:       order.BusinessTimeMS(),
	}
}

func h5CoinSellerRechargeWhere(appCode string, query financeCoinSellerRechargeQuery) (string, []any) {
	where := []string{
		"eo.app_code = ?",
		"eo.audience_type = 'coin_seller'",
		"eo.status = 'credited'",
		"eo.wallet_transaction_id <> ''",
		"wt.biz_type = 'coin_seller_recharge'",
		"wt.status = 'succeeded'",
	}
	args := []any{appCode}
	// wallet 表已有 (app_code,status,provider_code,updated_at_ms) 索引；把厂商条件下推到 SQL 才能保证统计、总数和分页一致。
	if rechargeSource, ok := financeCoinSellerRechargeSource(query.RechargeType); ok {
		switch rechargeSource {
		case repository.CoinSellerRechargeSourceThirdParty:
			where = append(where, "eo.provider_code IN (?, ?)")
			args = append(args, repository.CoinSellerRechargeSourceMiFaPay, repository.CoinSellerRechargeSourceV5Pay)
		case repository.CoinSellerRechargeSourceMiFaPay, repository.CoinSellerRechargeSourceV5Pay:
			where = append(where, "eo.provider_code = ?")
			args = append(args, rechargeSource)
		}
	}
	if query.TargetUserID > 0 {
		where = append(where, "eo.target_user_id = ?")
		args = append(args, query.TargetUserID)
	}
	if query.RegionID > 0 {
		where = append(where, "eo.target_region_id = ?")
		args = append(args, query.RegionID)
	}
	if query.StartAtMS > 0 {
		where = append(where, "eo.updated_at_ms >= ?")
		args = append(args, query.StartAtMS)
	}
	if query.EndAtMS > 0 {
		where = append(where, "eo.updated_at_ms < ?")
		args = append(args, query.EndAtMS)
	}
	if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
		like := "%" + keyword + "%"
		where = append(where, "(eo.order_id LIKE ? OR eo.provider_order_id LIKE ? OR eo.wallet_transaction_id LIKE ? OR eo.product_code LIKE ? OR eo.product_name LIKE ? OR eo.tx_hash LIKE ? OR CAST(eo.target_user_id AS CHAR) LIKE ?)")
		args = append(args, like, like, like, like, like, like, like)
	}
	return "WHERE " + strings.Join(where, " AND "), args
}

func financeCoinSellerSucceededStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "", "all", "succeeded", "success":
		return true
	default:
		return false
	}
}

func financeCoinSellerPage(page int, pageSize int, maxPageSize int) (int, int) {
	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 10
	}
	if maxPageSize <= 0 {
		maxPageSize = financeCoinSellerRechargeMaxPageSize
	}
	if pageSize > maxPageSize {
		pageSize = maxPageSize
	}
	return page, pageSize
}

func financeRechargePrefetch(page int, pageSize int, maxPageSize int) int {
	prefetch := page * pageSize
	limit := financeRechargeMergeMaxPrefetch
	if maxPageSize > limit {
		limit = maxPageSize
	}
	if prefetch > limit {
		return limit
	}
	return prefetch
}

func sortRechargeBills(items []rechargeBillDTO) {
	sort.Slice(items, func(i, j int) bool {
		if items[i].CreatedAtMS == items[j].CreatedAtMS {
			return items[i].TransactionID > items[j].TransactionID
		}
		return items[i].CreatedAtMS > items[j].CreatedAtMS
	})
}

func mergeRechargeRegionBuckets(groups ...[]rechargeBillRegionBucketDTO) []rechargeBillRegionBucketDTO {
	byRegion := map[int64]*rechargeBillRegionBucketDTO{}
	for _, group := range groups {
		for _, row := range group {
			if row.RegionID <= 0 {
				continue
			}
			bucket, ok := byRegion[row.RegionID]
			if !ok {
				copy := row
				byRegion[row.RegionID] = &copy
				continue
			}
			bucket.BillCount += row.BillCount
			bucket.UsdMinorAmount += row.UsdMinorAmount
			if bucket.Name == "" {
				bucket.Name = row.Name
			}
		}
	}
	out := make([]rechargeBillRegionBucketDTO, 0, len(byRegion))
	for _, bucket := range byRegion {
		out = append(out, *bucket)
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i].UsdMinorAmount > out[j].UsdMinorAmount
	})
	return out
}

func (h *Handler) fillFinanceCoinSellerRegionNames(ctx context.Context, appCode string, buckets []rechargeBillRegionBucketDTO) []rechargeBillRegionBucketDTO {
	if len(buckets) == 0 {
		return buckets
	}
	names := map[int64]string{}
	if h != nil {
		if regions, err := h.listRechargeBillRegions(ctx, appCode); err == nil {
			for _, region := range regions {
				name := firstNonEmptyPaymentString(region.Name, region.RegionCode, strconv.FormatInt(region.RegionID, 10))
				names[region.RegionID] = name
			}
		}
	}
	for i := range buckets {
		if name := names[buckets[i].RegionID]; name != "" {
			buckets[i].Name = name
			continue
		}
		buckets[i].Name = strconv.FormatInt(buckets[i].RegionID, 10)
	}
	return buckets
}

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