package mysql

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
)

type pointToCoinSellerMetadata struct {
	AppCode                 string `json:"app_code"`
	SourceUserID            int64  `json:"source_user_id"`
	SellerUserID            int64  `json:"seller_user_id"`
	SourceCountryCode       string `json:"source_country_code"`
	RegionID                int64  `json:"region_id"`
	SourceAssetType         string `json:"source_asset_type"`
	SellerAssetType         string `json:"seller_asset_type"`
	PointAmount             int64  `json:"point_amount"`
	SellerCoinAmount        int64  `json:"seller_coin_amount"`
	RatioPointAmount        int64  `json:"ratio_point_amount"`
	RatioSellerCoinAmount   int64  `json:"ratio_seller_coin_amount"`
	GrossUSDMinor           int64  `json:"gross_usd_minor,omitempty"`
	FeeUSDMinor             int64  `json:"fee_usd_minor,omitempty"`
	NetUSDMinor             int64  `json:"net_usd_minor,omitempty"`
	WithdrawFeeBPS          int32  `json:"withdraw_fee_bps,omitempty"`
	PolicyID                uint64 `json:"policy_id,omitempty"`
	PolicyVersion           uint64 `json:"policy_version,omitempty"`
	SourcePointBalanceAfter int64  `json:"source_point_balance_after"`
	SellerBalanceAfter      int64  `json:"seller_balance_after"`
	Reason                  string `json:"reason"`
	CreatedAtMS             int64  `json:"created_at_ms"`
}

// ListPointWithdrawalCoinSellers 返回当前 App 的白名单；区域/国家过滤在 wallet 内执行，gateway 不能绕过服务范围。
func (r *Repository) ListPointWithdrawalCoinSellers(ctx context.Context, appCode string, regionID int64, countryCode string, includeDisabled bool) ([]ledger.PointWithdrawalCoinSellerConfig, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	where := "app_code = ?"
	args := []any{appcode.FromContext(ctx)}
	if !includeDisabled {
		where += " AND status = 'active'"
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, seller_user_id, sort_order, point_amount, seller_coin_amount,
		       COALESCE(service_region_ids, JSON_ARRAY()), service_country_codes,
		       status, created_at_ms, updated_at_ms
		FROM point_withdrawal_coin_seller_configs
		WHERE `+where+`
		ORDER BY sort_order DESC, seller_user_id ASC`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
	items := make([]ledger.PointWithdrawalCoinSellerConfig, 0)
	for rows.Next() {
		var item ledger.PointWithdrawalCoinSellerConfig
		var regionsJSON []byte
		var countriesJSON []byte
		if err := rows.Scan(&item.AppCode, &item.SellerUserID, &item.SortOrder, &item.PointAmount, &item.SellerCoinAmount, &regionsJSON, &countriesJSON, &item.Status, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		if err := json.Unmarshal(regionsJSON, &item.ServiceRegionIDs); err != nil {
			return nil, xerr.New(xerr.Internal, "point withdrawal seller region config is invalid")
		}
		if err := json.Unmarshal(countriesJSON, &item.ServiceCountryCodes); err != nil {
			return nil, xerr.New(xerr.Internal, "point withdrawal seller country config is invalid")
		}
		item.ServiceRegionIDs = normalizePointWithdrawalRegionIDs(item.ServiceRegionIDs)
		item.ServiceCountryCodes = normalizeCountryCodes(item.ServiceCountryCodes)
		if item.PointAmount <= 0 || item.SellerCoinAmount <= 0 {
			return nil, xerr.New(xerr.Internal, "point withdrawal seller ratio is invalid")
		}
		// region=0 且 country 为空是 Admin/内部全量读取；用户入口会携带资料中的真实区域和国家。
		if regionID > 0 || countryCode != "" {
			allowed, scopeErr := serviceScopeAllowed(item.ServiceRegionIDs, item.ServiceCountryCodes, regionID, countryCode)
			if scopeErr != nil {
				return nil, scopeErr
			}
			if !allowed {
				continue
			}
		}
		items = append(items, item)
	}
	return items, rows.Err()
}

// TransferPointToCoinSeller 在一个事务中锁定白名单配置和双方账户；任何比例更新只影响新 command_id。
func (r *Repository) TransferPointToCoinSeller(ctx context.Context, command ledger.PointToCoinSellerCommand) (ledger.PointToCoinSellerReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	sourceAssetType := strings.ToUpper(strings.TrimSpace(command.SourceAssetType))
	if sourceAssetType == "" {
		sourceAssetType = ledger.AssetPoint
	}
	if sourceAssetType != ledger.AssetPoint && sourceAssetType != ledger.AssetPointDiamond {
		return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "point source asset type is invalid")
	}
	// region/country/reason 都是 Gateway 根据当前资料派生的权威上下文，不属于用户稳定意图。
	// 同一 command_id 仍严格绑定源用户、目标币商、资产和用户提交金额，防止篡改真实兑换目标。
	requestHash := stableHash(fmt.Sprintf("point_transfer_coin_seller|%s|%d|%d|%s|%d|%d",
		command.AppCode, command.SourceUserID, command.SellerUserID, sourceAssetType, command.PointAmount, command.GrossUSDMinor))
	if txRow, exists, lookupErr := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypePointTransferToCoinSeller, xerr.IdempotencyConflict); lookupErr != nil || exists {
		if lookupErr != nil || !exists {
			return ledger.PointToCoinSellerReceipt{}, lookupErr
		}
		return r.receiptForPointToCoinSellerTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMS := time.Now().UTC().UnixMilli()
	pointAmount := command.PointAmount
	ratioPointAmount := int64(0)
	ratioSellerCoinAmount := int64(0)
	sellerCoinAmount := int64(0)
	grossUSDMinor := int64(0)
	feeUSDMinor := int64(0)
	netUSDMinor := int64(0)
	withdrawFeeBPS := int32(0)
	policyID := uint64(0)
	policyVersion := uint64(0)
	if sourceAssetType == ledger.AssetPointDiamond {
		policy, err := r.resolvePointDiamondHostPolicy(ctx, tx, command.RegionID, nowMS)
		if err != nil {
			return ledger.PointToCoinSellerReceipt{}, err
		}
		if command.GrossUSDMinor < policy.CoinSellerMinimumWithdrawUSDMinor {
			return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "withdrawal amount is below configured minimum")
		}
		pointAmount, feeUSDMinor, netUSDMinor, sellerCoinAmount, err = calculatePointDiamondCoinSellerAmounts(
			command.GrossUSDMinor, policy.PointDiamondsPerUSD, policy.CoinsPerUSD, policy.WithdrawFeeBPS,
		)
		if err != nil {
			return ledger.PointToCoinSellerReceipt{}, err
		}
		ratioPointAmount, ratioSellerCoinAmount = policy.PointDiamondsPerUSD, policy.CoinsPerUSD
		grossUSDMinor, withdrawFeeBPS = command.GrossUSDMinor, policy.WithdrawFeeBPS
		policyID, policyVersion = policy.PolicyID, policy.PolicyVersion
		if pointAmount <= 0 || sellerCoinAmount <= 0 {
			return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "withdrawal amount is below exchange precision")
		}
	} else {
		policy, found, err := r.resolveActiveWalletPolicy(ctx, tx, command.AppCode, command.RegionID, nowMS)
		if err != nil {
			return ledger.PointToCoinSellerReceipt{}, err
		}
		if !found {
			return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.PermissionDenied, "point wallet policy is not configured")
		}
		if pointAmount < policy.MinimumPoints {
			return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "point withdrawal amount is below configured minimum")
		}
	}

	config, err := r.lockPointWithdrawalCoinSellerConfig(ctx, tx, command.SellerUserID)
	if err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	countryCode := strings.ToUpper(strings.TrimSpace(command.SourceCountryCode))
	allowed, err := serviceScopeAllowed(config.ServiceRegionIDs, config.ServiceCountryCodes, command.RegionID, countryCode)
	if err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if !allowed {
		return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.PermissionDenied, "coin seller does not serve user region or country")
	}
	if sourceAssetType == ledger.AssetPoint {
		numerator, err := checkedMul(pointAmount, config.SellerCoinAmount)
		if err != nil {
			return ledger.PointToCoinSellerReceipt{}, err
		}
		sellerCoinAmount = numerator / config.PointAmount
		ratioPointAmount, ratioSellerCoinAmount = config.PointAmount, config.SellerCoinAmount
		if sellerCoinAmount <= 0 {
			return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InvalidArgument, "point amount is below exchange precision")
		}
	}

	// 送礼、积分兑币和币商提现可能沿相反方向触达相同资产账户；双方账户必须先按全局
	// asset/user 顺序一次锁齐，再消费次数限制，避免 source->limit->seller 与其他账变反向等待。
	accountStates, err := r.lockAccountsOrdered(ctx, tx, []walletAccountLockRequest{
		{UserID: command.SourceUserID, AssetType: sourceAssetType, CreateIfMissing: false},
		{UserID: command.SellerUserID, AssetType: ledger.AssetCoinSellerCoin, CreateIfMissing: true},
	}, nowMS)
	if err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	source := accountStates[walletAccountLockKey(command.SourceUserID, sourceAssetType)].account
	seller := accountStates[walletAccountLockKey(command.SellerUserID, ledger.AssetCoinSellerCoin)].account
	if source.AvailableAmount < pointAmount {
		return ledger.PointToCoinSellerReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient point balance")
	}
	policyType := ""
	if sourceAssetType == ledger.AssetPointDiamond {
		// 永久积分继续引用最后一份 POINT_DIAMOND 快照，后续切换工资政策不影响旧余额的转出限制。
		policyType = ledger.HostPolicyTypePointDiamond
	}
	if err := r.consumeHostPolicyWithdrawalLimitForPolicyType(ctx, tx, command.SourceUserID, command.RegionID, pointWithdrawalChannelCoinSeller, command.CommandID, "", nowMS, sourceAssetType, policyType, requireHostPolicy); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	sourceAfter := source.AvailableAmount - pointAmount
	sellerAfter, err := checkedAdd(seller.AvailableAmount, sellerCoinAmount)
	if err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	metadata := pointToCoinSellerMetadata{
		AppCode: command.AppCode, SourceUserID: command.SourceUserID, SellerUserID: command.SellerUserID,
		SourceCountryCode: countryCode, SourceAssetType: sourceAssetType, SellerAssetType: ledger.AssetCoinSellerCoin,
		RegionID:    command.RegionID,
		PointAmount: pointAmount, SellerCoinAmount: sellerCoinAmount,
		RatioPointAmount: ratioPointAmount, RatioSellerCoinAmount: ratioSellerCoinAmount,
		GrossUSDMinor: grossUSDMinor, FeeUSDMinor: feeUSDMinor, NetUSDMinor: netUSDMinor,
		WithdrawFeeBPS: withdrawFeeBPS, PolicyID: policyID, PolicyVersion: policyVersion,
		SourcePointBalanceAfter: sourceAfter, SellerBalanceAfter: sellerAfter, Reason: command.Reason, CreatedAtMS: nowMS,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypePointTransferToCoinSeller, requestHash, fmt.Sprintf("point_coin_seller:%d:%d", command.SourceUserID, command.SellerUserID), metadata, nowMS); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, source, -pointAmount, 0, nowMS); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{TransactionID: transactionID, UserID: command.SourceUserID, AssetType: sourceAssetType, AvailableDelta: -pointAmount, AvailableAfter: sourceAfter, FrozenAfter: source.FrozenAmount, CounterpartyUserID: command.SellerUserID, CreatedAtMS: nowMS}); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, seller, sellerCoinAmount, 0, nowMS); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{TransactionID: transactionID, UserID: command.SellerUserID, AssetType: ledger.AssetCoinSellerCoin, AvailableDelta: sellerCoinAmount, AvailableAfter: sellerAfter, FrozenAfter: seller.FrozenAmount, CounterpartyUserID: command.SourceUserID, CreatedAtMS: nowMS}); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.SourceUserID, sourceAssetType, -pointAmount, 0, sourceAfter, source.FrozenAmount, source.Version+1, metadata, nowMS, bizTypePointTransferToCoinSeller),
		balanceChangedEvent(transactionID, command.CommandID, command.SellerUserID, ledger.AssetCoinSellerCoin, sellerCoinAmount, 0, sellerAfter, seller.FrozenAmount, seller.Version+1, metadata, nowMS, bizTypePointTransferToCoinSeller),
	}); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	return receiptFromPointToCoinSellerMetadata(transactionID, metadata), nil
}

func calculatePointDiamondCoinSellerAmounts(grossUSDMinor int64, pointsPerUSD int64, coinsPerUSD int64, feeBPS int32) (pointAmount int64, feeUSDMinor int64, netUSDMinor int64, sellerCoinAmount int64, err error) {
	if grossUSDMinor <= 0 || pointsPerUSD <= 0 || coinsPerUSD <= 0 || feeBPS < 0 || feeBPS > 10_000 {
		err = xerr.New(xerr.InvalidArgument, "point diamond withdrawal policy is invalid")
		return
	}
	pointProduct, calculateErr := checkedMul(grossUSDMinor, pointsPerUSD)
	if calculateErr != nil {
		err = calculateErr
		return
	}
	feeProduct, calculateErr := checkedMul(grossUSDMinor, int64(feeBPS))
	if calculateErr != nil {
		err = calculateErr
		return
	}
	pointAmount = pointProduct / 100
	feeUSDMinor = feeProduct / 10_000
	netUSDMinor = grossUSDMinor - feeUSDMinor
	coinProduct, calculateErr := checkedMul(netUSDMinor, coinsPerUSD)
	if calculateErr != nil {
		err = calculateErr
		return
	}
	sellerCoinAmount = coinProduct / 100
	if pointAmount <= 0 || netUSDMinor <= 0 || sellerCoinAmount <= 0 {
		err = xerr.New(xerr.InvalidArgument, "withdrawal amount is below exchange precision")
	}
	return
}

// GetPointWithdrawalConfig 只暴露原 wallet 政策中 POINT 提现和兑换实际消费的字段。
// 礼物收益不会读取该政策；缺少配置时显式 found=false，不回退编译期常量。
func (r *Repository) GetPointWithdrawalConfig(ctx context.Context, appCode string, regionID int64, nowMS int64) (ledger.PointWithdrawalRuntimeConfig, error) {
	return r.GetPointWithdrawalConfigForAsset(ctx, appCode, regionID, nowMS, ledger.AssetPoint)
}

func (r *Repository) GetPointWithdrawalConfigForAsset(ctx context.Context, appCode string, regionID int64, nowMS int64, assetType string) (ledger.PointWithdrawalRuntimeConfig, error) {
	return r.getPointWithdrawalConfig(ctx, appCode, 0, regionID, nowMS, assetType)
}

func (r *Repository) GetPointWithdrawalConfigForUser(ctx context.Context, appCode string, userID int64, regionID int64, nowMS int64, assetType string) (ledger.PointWithdrawalRuntimeConfig, error) {
	if userID <= 0 {
		return ledger.PointWithdrawalRuntimeConfig{}, xerr.New(xerr.InvalidArgument, "point withdrawal config user is invalid")
	}
	return r.getPointWithdrawalConfig(ctx, appCode, userID, regionID, nowMS, assetType)
}

func (r *Repository) getPointWithdrawalConfig(ctx context.Context, appCode string, userID int64, regionID int64, nowMS int64, assetType string) (ledger.PointWithdrawalRuntimeConfig, error) {
	if r == nil || r.db == nil {
		return ledger.PointWithdrawalRuntimeConfig{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return ledger.PointWithdrawalRuntimeConfig{}, err
	}
	defer func() { _ = tx.Rollback() }()
	assetType = strings.ToUpper(strings.TrimSpace(assetType))
	if assetType == ledger.AssetPointDiamond {
		policy, err := r.resolvePointDiamondHostPolicy(ctx, tx, regionID, nowMS)
		if err != nil {
			if xerr.CodeOf(err) == xerr.PermissionDenied {
				return ledger.PointWithdrawalRuntimeConfig{Found: false}, nil
			}
			return ledger.PointWithdrawalRuntimeConfig{}, err
		}
		minimumPoints, calculateErr := checkedMul(policy.PlatformMinimumWithdrawUSDMinor, policy.PointDiamondsPerUSD)
		if calculateErr != nil {
			return ledger.PointWithdrawalRuntimeConfig{}, calculateErr
		}
		config := ledger.PointWithdrawalRuntimeConfig{
			Found: true, PointsPerUSD: policy.PointDiamondsPerUSD, CoinsPerUSD: policy.CoinsPerUSD,
			FeeBPS: int64(policy.WithdrawFeeBPS), MinimumPoints: minimumPoints / 100,
			PolicyInstanceCode: fmt.Sprintf("host:%d:%d", policy.PolicyID, policy.PolicyVersion), PolicyType: policy.PolicyType,
			MinimumWithdrawUSDMinor:           policy.PlatformMinimumWithdrawUSDMinor,
			CoinSellerMinimumWithdrawUSDMinor: policy.CoinSellerMinimumWithdrawUSDMinor,
			PlatformMinimumWithdrawUSDMinor:   policy.PlatformMinimumWithdrawUSDMinor,
			DiamondExchangeEnabled:            policy.DiamondExchangeEnabled,
			AgencyPointShareBPS:               policy.AgencyPointShareBPS,
			PolicyID:                          policy.PolicyID, PolicyVersion: policy.PolicyVersion,
		}
		if userID > 0 {
			now := time.UnixMilli(nowMS).UTC()
			coinSellerLimit, limitErr := hostPolicyWithdrawalLimitFromPolicy(policy, pointWithdrawalChannelCoinSeller, now)
			if limitErr != nil {
				return ledger.PointWithdrawalRuntimeConfig{}, limitErr
			}
			platformLimit, limitErr := hostPolicyWithdrawalLimitFromPolicy(policy, pointWithdrawalChannelPlatform, now)
			if limitErr != nil {
				return ledger.PointWithdrawalRuntimeConfig{}, limitErr
			}
			config.CoinSellerAvailability, limitErr = r.pointWithdrawalActionAvailability(ctx, tx, userID, assetType, pointWithdrawalChannelCoinSeller, coinSellerLimit, now)
			if limitErr != nil {
				return ledger.PointWithdrawalRuntimeConfig{}, limitErr
			}
			config.PlatformAvailability, limitErr = r.pointWithdrawalActionAvailability(ctx, tx, userID, assetType, pointWithdrawalChannelPlatform, platformLimit, now)
			if limitErr != nil {
				return ledger.PointWithdrawalRuntimeConfig{}, limitErr
			}
			config.AvailabilityEvaluated = true
		}
		return config, nil
	}
	policy, found, err := r.resolveActiveWalletPolicy(ctx, tx, appcode.FromContext(ctx), regionID, nowMS)
	if err != nil || !found {
		return ledger.PointWithdrawalRuntimeConfig{Found: false}, err
	}
	return ledger.PointWithdrawalRuntimeConfig{
		Found:              true,
		PointsPerUSD:       policy.PointsPerUSD,
		CoinsPerUSD:        ledger.SalaryExchangeCoinPerUSD,
		FeeBPS:             policy.WithdrawFeeBPS,
		MinimumPoints:      policy.MinimumPoints,
		PolicyInstanceCode: policy.InstanceCode,
	}, nil
}

func (r *Repository) lockPointWithdrawalCoinSellerConfig(ctx context.Context, tx *sql.Tx, sellerUserID int64) (ledger.PointWithdrawalCoinSellerConfig, error) {
	var item ledger.PointWithdrawalCoinSellerConfig
	var regionsJSON []byte
	var countriesJSON []byte
	err := tx.QueryRowContext(ctx, `
		SELECT app_code, seller_user_id, sort_order, point_amount, seller_coin_amount,
		       COALESCE(service_region_ids, JSON_ARRAY()), service_country_codes,
		       status, created_at_ms, updated_at_ms
		FROM point_withdrawal_coin_seller_configs
		WHERE app_code = ? AND seller_user_id = ? AND status = 'active'
		FOR UPDATE`, appcode.FromContext(ctx), sellerUserID).Scan(
		&item.AppCode, &item.SellerUserID, &item.SortOrder, &item.PointAmount, &item.SellerCoinAmount,
		&regionsJSON, &countriesJSON, &item.Status, &item.CreatedAtMS, &item.UpdatedAtMS,
	)
	if errors.Is(err, sql.ErrNoRows) {
		return item, xerr.New(xerr.NotFound, "point withdrawal coin seller is not available")
	}
	if err != nil {
		return item, err
	}
	if err := json.Unmarshal(regionsJSON, &item.ServiceRegionIDs); err != nil {
		return item, xerr.New(xerr.Internal, "point withdrawal coin seller region config is invalid")
	}
	if err := json.Unmarshal(countriesJSON, &item.ServiceCountryCodes); err != nil || item.PointAmount <= 0 || item.SellerCoinAmount <= 0 {
		return item, xerr.New(xerr.Internal, "point withdrawal coin seller config is invalid")
	}
	item.ServiceRegionIDs = normalizePointWithdrawalRegionIDs(item.ServiceRegionIDs)
	item.ServiceCountryCodes = normalizeCountryCodes(item.ServiceCountryCodes)
	if len(item.ServiceRegionIDs) > 0 && len(item.ServiceCountryCodes) > 0 {
		return item, xerr.New(xerr.Internal, "point withdrawal coin seller service scopes conflict")
	}
	return item, nil
}

func (r *Repository) receiptForPointToCoinSellerTransaction(ctx context.Context, tx *sql.Tx, transactionID string) (ledger.PointToCoinSellerReceipt, error) {
	var metadataJSON string
	if err := tx.QueryRowContext(ctx, `SELECT COALESCE(CAST(metadata_json AS CHAR), '{}') FROM wallet_transactions WHERE app_code = ? AND transaction_id = ?`, appcode.FromContext(ctx), transactionID).Scan(&metadataJSON); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	var metadata pointToCoinSellerMetadata
	if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
		return ledger.PointToCoinSellerReceipt{}, err
	}
	return receiptFromPointToCoinSellerMetadata(transactionID, metadata), nil
}

func receiptFromPointToCoinSellerMetadata(transactionID string, metadata pointToCoinSellerMetadata) ledger.PointToCoinSellerReceipt {
	return ledger.PointToCoinSellerReceipt{
		TransactionID: transactionID, SourceUserID: metadata.SourceUserID, SellerUserID: metadata.SellerUserID,
		SourcePointBalanceAfter: metadata.SourcePointBalanceAfter, SellerBalanceAfter: metadata.SellerBalanceAfter,
		PointAmount: metadata.PointAmount, SellerCoinAmount: metadata.SellerCoinAmount,
		RatioPointAmount: metadata.RatioPointAmount, RatioSellerCoinAmount: metadata.RatioSellerCoinAmount, CreatedAtMS: metadata.CreatedAtMS,
		SourceAssetType: metadata.SourceAssetType, GrossUSDMinor: metadata.GrossUSDMinor, FeeUSDMinor: metadata.FeeUSDMinor,
		NetUSDMinor: metadata.NetUSDMinor, WithdrawFeeBPS: metadata.WithdrawFeeBPS, PolicyID: metadata.PolicyID, PolicyVersion: metadata.PolicyVersion,
	}
}

func normalizeCountryCodes(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.ToUpper(strings.TrimSpace(value))
		if value == "" {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func normalizePointWithdrawalRegionIDs(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	out := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

// serviceScopeAllowed 保持三态边界：区域白名单、国家白名单、两个都为空的全量范围。
// 同时存在两种范围属于损坏配置，必须失败而不是猜测 OR/AND 语义后放宽资金入口。
func serviceScopeAllowed(regionIDs []int64, countryCodes []string, regionID int64, countryCode string) (bool, error) {
	if len(regionIDs) > 0 && len(countryCodes) > 0 {
		return false, xerr.New(xerr.Internal, "point withdrawal coin seller service scopes conflict")
	}
	if len(regionIDs) > 0 {
		for _, configuredID := range regionIDs {
			if configuredID == regionID {
				return true, nil
			}
		}
		return false, nil
	}
	if len(countryCodes) == 0 {
		return true, nil
	}
	countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
	if countryCode == "" {
		return false, nil
	}
	for _, configuredCode := range countryCodes {
		if strings.EqualFold(configuredCode, countryCode) {
			return true, nil
		}
	}
	return false, nil
}
