package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/wallet-service/internal/domain/ledger"
	"strings"
	"time"
)

func (r *Repository) CreateExternalRechargeOrder(ctx context.Context, command ledger.CreateExternalRechargeOrderCommand, product ledger.RechargeProduct, method *ledger.ThirdPartyPaymentMethod, providerAmountMinor int64, receiveAddress string) (ledger.ExternalRechargeOrder, error) {
	if r == nil || r.db == nil {
		return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	// 临时支付链接和正式 H5 充值共用 command_id 幂等约束；浏览器重试创建时必须返回同一笔订单，不能生成多个可支付链接。
	if existing, found, err := r.lookupExternalRechargeOrderByCommand(ctx, command.AppCode, command.CommandID); err != nil || found {
		if err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		existing.IdempotentReplay = true
		return existing, nil
	}
	order := externalRechargeOrderFromCommand(command, product, method, providerAmountMinor, receiveAddress, time.Now().UnixMilli())
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO external_recharge_orders (
		  app_code, order_id, command_id, target_user_id, target_region_id, target_country_id, target_country_code, audience_type,
		  product_id, product_code, product_name, coin_amount, usd_minor_amount, provider_code, payment_method_id,
		  country_code, currency_code, provider_amount_minor, pay_way, pay_type, receive_address, status,
		  created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		order.AppCode, order.OrderID, order.CommandID, order.TargetUserID, order.TargetRegionID, order.TargetCountryID, order.TargetCountryCode,
		order.AudienceType, order.ProductID, order.ProductCode, order.ProductName, order.CoinAmount, order.USDMinorAmount,
		order.ProviderCode, order.PaymentMethodID, order.CountryCode, order.CurrencyCode, order.ProviderAmountMinor,
		order.PayWay, order.PayType, order.ReceiveAddress, order.Status, order.CreatedAtMS, order.UpdatedAtMS,
	)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	return order, nil
}

func (r *Repository) CreateTemporaryExternalRechargeOrder(ctx context.Context, command ledger.CreateTemporaryExternalRechargeOrderCommand, method ledger.ThirdPartyPaymentMethod, providerAmountMinor int64) (ledger.ExternalRechargeOrder, error) {
	if r == nil || r.db == nil {
		return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	command.AppCode = appcode.FromContext(ctx)
	if existing, found, err := r.lookupExternalRechargeOrderByCommand(ctx, command.AppCode, command.CommandID); err != nil || found {
		if err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		existing.IdempotentReplay = true
		return existing, nil
	}

	nowMS := time.Now().UnixMilli()
	// 同表保存能复用三方回调、查单和后台列表投影；target_user_id/product_id 置 0 表示这笔钱只供人工核销，不自动发金币。
	order := temporaryExternalRechargeOrderFromCommand(command, method, providerAmountMinor, nowMS)
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO external_recharge_orders (
		  app_code, order_id, command_id, target_user_id, target_region_id, target_country_id, target_country_code, audience_type,
		  product_id, product_code, product_name, coin_amount, usd_minor_amount, provider_code, payment_method_id,
		  country_code, currency_code, provider_amount_minor, pay_way, pay_type, receive_address, status,
		  created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		order.AppCode, order.OrderID, order.CommandID, order.TargetUserID, order.TargetRegionID, order.TargetCountryID, order.TargetCountryCode,
		order.AudienceType, order.ProductID, order.ProductCode, order.ProductName, order.CoinAmount, order.USDMinorAmount,
		order.ProviderCode, order.PaymentMethodID, order.CountryCode, order.CurrencyCode, order.ProviderAmountMinor,
		order.PayWay, order.PayType, order.ReceiveAddress, order.Status, order.CreatedAtMS, order.UpdatedAtMS,
	)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	return order, nil
}

func (r *Repository) MarkExternalRechargeOrderRedirected(ctx context.Context, appCode string, orderID string, providerOrderID string, payURL string, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
	ctx = contextWithCommandApp(ctx, appCode)
	nowMS := time.Now().UnixMilli()
	_, err := r.db.ExecContext(ctx, `
		UPDATE external_recharge_orders
		SET provider_order_id = ?, pay_url = ?, provider_payload = ?, status = ?, updated_at_ms = ?
		WHERE app_code = ? AND order_id = ? AND status = ?`,
		strings.TrimSpace(providerOrderID), strings.TrimSpace(payURL), nullableJSON(payloadJSON), ledger.ExternalRechargeStatusRedirected,
		nowMS, appcode.FromContext(ctx), strings.TrimSpace(orderID), ledger.ExternalRechargeStatusPending,
	)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	return r.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), orderID)
}

func (r *Repository) AttachExternalRechargeTx(ctx context.Context, command ledger.SubmitExternalRechargeTxCommand, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
	ctx = contextWithCommandApp(ctx, command.AppCode)
	txHash := strings.TrimSpace(command.TxHash)
	result, err := r.db.ExecContext(ctx, `
		UPDATE external_recharge_orders
		SET tx_hash = ?, provider_payload = ?, updated_at_ms = ?
		WHERE app_code = ? AND order_id = ? AND target_user_id = ? AND provider_code = ? AND status = ?`,
		txHash, nullableJSON(payloadJSON), time.Now().UnixMilli(), appcode.FromContext(ctx), strings.TrimSpace(command.OrderID),
		command.TargetUserID, ledger.PaymentProviderUSDTTRC20, ledger.ExternalRechargeStatusPending,
	)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	} else if affected == 0 {
		return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Conflict, "external recharge order is not pending usdt order")
	}
	return r.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), command.OrderID)
}

func (r *Repository) CreditExternalRechargeOrder(ctx context.Context, appCode string, orderID string, providerOrderID string, txHash string, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
	if r == nil || r.db == nil {
		return ledger.ExternalRechargeOrder{}, 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.ExternalRechargeOrder{}, err
	}
	defer func() { _ = tx.Rollback() }()

	order, err := r.lockExternalRechargeOrder(ctx, tx, appcode.FromContext(ctx), strings.TrimSpace(orderID))
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if order.Status == ledger.ExternalRechargeStatusCredited {
		order.IdempotentReplay = true
		return order, nil
	}
	if order.Status == ledger.ExternalRechargeStatusFailed {
		return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Conflict, "external recharge order is failed")
	}
	if strings.TrimSpace(txHash) != "" {
		if err := r.ensureExternalTxHashUnused(ctx, tx, order.AppCode, order.ProviderCode, strings.TrimSpace(txHash), order.OrderID); err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		order.TxHash = strings.TrimSpace(txHash)
	}
	if strings.TrimSpace(providerOrderID) != "" {
		order.ProviderOrderID = strings.TrimSpace(providerOrderID)
	}

	nowMS := time.Now().UnixMilli()
	assetType := ledger.AssetCoin
	rechargeType := order.ProviderCode
	bizType := providerRechargeBizType(order.ProviderCode)
	isCoinSellerRecharge := order.AudienceType == ledger.RechargeAudienceCoinSeller
	if isCoinSellerRecharge {
		assetType = ledger.AssetCoinSellerCoin
		rechargeType = "coin_seller_recharge"
		bizType = "coin_seller_recharge"
	}
	account, err := r.lockAccount(ctx, tx, order.TargetUserID, assetType, true, nowMS)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	transactionID := transactionID(order.AppCode, order.CommandID)
	if _, exists, err := r.lookupTransaction(ctx, tx, order.CommandID, externalRechargeRequestHash(order, assetType), bizType); err != nil || exists {
		if err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		return r.lockExternalRechargeOrder(ctx, tx, order.AppCode, order.OrderID)
	}
	balanceAfter, err := checkedAdd(account.AvailableAmount, order.CoinAmount)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	metadata := coinSellerTransferMetadata{
		AppCode:                      order.AppCode,
		PaymentOrderID:               order.OrderID,
		ProviderCode:                 order.ProviderCode,
		PaymentMethodID:              order.PaymentMethodID,
		TargetUserID:                 order.TargetUserID,
		TargetCountryID:              order.TargetCountryID,
		TargetRegionID:               order.TargetRegionID,
		Amount:                       order.CoinAmount,
		TargetAssetType:              assetType,
		TargetBalanceAfter:           balanceAfter,
		RechargeUSDMinor:             order.USDMinorAmount,
		RechargeCurrencyCode:         order.CurrencyCode,
		RechargePolicyID:             order.ProductID,
		RechargePolicyVersion:        order.ProductCode,
		RechargePolicyCoinAmount:     order.CoinAmount,
		RechargePolicyUSDMinorAmount: order.USDMinorAmount,
		RechargeType:                 rechargeType,
	}
	var transactionMetadata any = metadata
	var stockMetadata coinSellerStockMetadata
	if isCoinSellerRecharge {

		paidAmountMicro, err := checkedMul(order.USDMinorAmount, 10_000)
		if err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		stockMetadata = coinSellerStockMetadata{
			AppCode:                order.AppCode,
			PaymentOrderID:         order.OrderID,
			ProviderCode:           order.ProviderCode,
			PaymentMethodID:        order.PaymentMethodID,
			SellerUserID:           order.TargetUserID,
			SellerRegionID:         order.TargetRegionID,
			StockType:              ledger.StockTypeUSDTPurchase,
			CoinAmount:             order.CoinAmount,
			PaidCurrencyCode:       ledger.PaidCurrencyUSDT,
			PaidAmountMicro:        paidAmountMicro,
			PaymentRef:             externalRechargePaymentRef(order),
			EvidenceRef:            order.OrderID,
			Reason:                 "h5 external recharge",
			AssetType:              ledger.AssetCoinSellerCoin,
			CountsAsSellerRecharge: true,
			BalanceAfter:           balanceAfter,
			CreatedAtMS:            nowMS,
		}
		transactionMetadata = stockMetadata
	} else {
		rechargeSequence, err := r.reserveUserRechargeSequence(ctx, tx, order.AppCode, order.TargetUserID, transactionID, order.CoinAmount, order.USDMinorAmount, nowMS)
		if err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		metadata.RechargeSequence = rechargeSequence
	}
	if err := r.insertTransaction(ctx, tx, transactionID, order.CommandID, bizType, externalRechargeRequestHash(order, assetType), order.OrderID, transactionMetadata, nowMS); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, order.CoinAmount, 0, nowMS); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         order.TargetUserID,
		AssetType:      assetType,
		AvailableDelta: order.CoinAmount,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMS,
	}); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}

	outboxEvents := []walletOutboxEvent{
		balanceChangedEvent(transactionID, order.CommandID, order.TargetUserID, assetType, order.CoinAmount, 0, balanceAfter, account.FrozenAmount, account.Version+1, transactionMetadata, nowMS),
	}
	if isCoinSellerRecharge {
		if err := r.insertCoinSellerStockRecord(ctx, tx, transactionID, order.CommandID, stockMetadata, nowMS); err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		outboxEvents = append(outboxEvents, coinSellerStockCreditedEvent(transactionID, order.CommandID, stockMetadata, nowMS))
	} else {
		if err := r.insertRechargeRecord(ctx, tx, transactionID, metadata, nowMS); err != nil {
			return ledger.ExternalRechargeOrder{}, err
		}
		outboxEvents = append(outboxEvents, rechargeRecordedEvent(transactionID, order.CommandID, order.TargetUserID, order.CoinAmount, metadata, nowMS))
	}
	if _, err := tx.ExecContext(ctx, `
		UPDATE external_recharge_orders
		SET status = ?, provider_order_id = ?, tx_hash = ?, wallet_transaction_id = ?, provider_payload = ?, updated_at_ms = ?
		WHERE app_code = ? AND order_id = ?`,
		ledger.ExternalRechargeStatusCredited, order.ProviderOrderID, order.TxHash, transactionID, nullableJSON(payloadJSON),
		nowMS, order.AppCode, order.OrderID,
	); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, outboxEvents); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	return r.GetExternalRechargeOrder(ctx, order.AppCode, order.OrderID)
}

func (r *Repository) MarkExternalRechargeOrderPaid(ctx context.Context, appCode string, orderID string, providerOrderID string, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
	ctx = contextWithCommandApp(ctx, appCode)
	nowMS := time.Now().UnixMilli()
	// paid 是临时支付的终态：只确认三方已收款，不创建 wallet transaction，也不写充值记录，避免把内部临时收款误发给用户。
	result, err := r.db.ExecContext(ctx, `
		UPDATE external_recharge_orders
		SET status = ?, provider_order_id = CASE WHEN ? = '' THEN provider_order_id ELSE ? END, provider_payload = ?, updated_at_ms = ?
		WHERE app_code = ? AND order_id = ? AND status NOT IN (?, ?)`,
		ledger.ExternalRechargeStatusPaid, strings.TrimSpace(providerOrderID), strings.TrimSpace(providerOrderID), nullableJSON(payloadJSON), nowMS,
		appcode.FromContext(ctx), strings.TrimSpace(orderID), ledger.ExternalRechargeStatusCredited, ledger.ExternalRechargeStatusFailed,
	)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	} else if affected == 0 {
		return r.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), orderID)
	}
	return r.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), orderID)
}

func (r *Repository) MarkExternalRechargeOrderFailed(ctx context.Context, appCode string, orderID string, reason string, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
	ctx = contextWithCommandApp(ctx, appCode)
	_, err := r.db.ExecContext(ctx, `
		UPDATE external_recharge_orders
		SET status = ?, failure_reason = ?, provider_payload = ?, updated_at_ms = ?
		WHERE app_code = ? AND order_id = ? AND status NOT IN (?, ?)`,
		ledger.ExternalRechargeStatusFailed, trimMax(reason, 512), nullableJSON(payloadJSON), time.Now().UnixMilli(),
		appcode.FromContext(ctx), strings.TrimSpace(orderID), ledger.ExternalRechargeStatusCredited, ledger.ExternalRechargeStatusPaid,
	)
	if err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	return r.GetExternalRechargeOrder(ctx, appcode.FromContext(ctx), orderID)
}

func (r *Repository) GetExternalRechargeOrder(ctx context.Context, appCode string, orderID string) (ledger.ExternalRechargeOrder, error) {
	ctx = contextWithCommandApp(ctx, appCode)
	order, err := scanExternalRechargeOrder(r.db.QueryRowContext(ctx, externalRechargeOrderSelectSQL()+`
		WHERE app_code = ? AND order_id = ?`,
		appcode.FromContext(ctx), strings.TrimSpace(orderID),
	))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
	}
	return order, err
}

func (r *Repository) ListTemporaryExternalRechargeOrders(ctx context.Context, query ledger.ListTemporaryExternalRechargeOrdersQuery) ([]ledger.ExternalRechargeOrder, int64, error) {
	if r == nil || r.db == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	where, args := temporaryExternalRechargeOrdersWhere(appcode.FromContext(ctx), query)
	var total int64
	if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM external_recharge_orders `+where, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	page, pageSize := normalizeTemporaryExternalRechargePage(query.Page, query.PageSize)
	rows, err := r.db.QueryContext(ctx, externalRechargeOrderSelectSQL()+`
		`+where+`
		ORDER BY created_at_ms DESC, order_id DESC
		LIMIT ? OFFSET ?`, append(args, pageSize, (page-1)*pageSize)...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()

	orders := make([]ledger.ExternalRechargeOrder, 0, pageSize)
	for rows.Next() {
		order, err := scanExternalRechargeOrder(rows)
		if err != nil {
			return nil, 0, err
		}
		orders = append(orders, order)
	}
	return orders, total, rows.Err()
}

func (r *Repository) ListExternalRechargeOrdersForReconcile(ctx context.Context, appCode string, limit int) ([]ledger.ExternalRechargeOrder, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	if limit <= 0 {
		return nil, nil
	}
	if limit > 500 {
		limit = 500
	}
	appCode = strings.ToLower(strings.TrimSpace(appCode))
	where := `
		WHERE provider_code IN (?, ?)
		  AND status = ?
	`
	args := []any{ledger.PaymentProviderMifaPay, ledger.PaymentProviderV5Pay, ledger.ExternalRechargeStatusRedirected}
	if appCode != "" {
		where += ` AND app_code = ?`
		args = append(args, appcode.Normalize(appCode))
	}
	where += ` ORDER BY updated_at_ms ASC, created_at_ms ASC, app_code ASC LIMIT ?`
	args = append(args, limit)
	rows, err := r.db.QueryContext(ctx, externalRechargeOrderSelectSQL()+where, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	orders := make([]ledger.ExternalRechargeOrder, 0, limit)
	for rows.Next() {
		order, err := scanExternalRechargeOrder(rows)
		if err != nil {
			return nil, err
		}
		orders = append(orders, order)
	}
	return orders, rows.Err()
}

func temporaryExternalRechargeOrdersWhere(appCode string, query ledger.ListTemporaryExternalRechargeOrdersQuery) (string, []any) {
	where := `WHERE app_code = ? AND audience_type = ?`
	args := []any{appCode, ledger.RechargeAudienceTemporary}
	if status := strings.TrimSpace(query.Status); status != "" {
		where += ` AND status = ?`
		args = append(args, status)
	}
	if providerCode := ledger.NormalizePaymentProvider(query.ProviderCode); providerCode != "" {
		where += ` AND provider_code = ?`
		args = append(args, providerCode)
	}
	if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
		where += ` AND (order_id LIKE ? OR provider_order_id LIKE ?)`
		like := "%" + keyword + "%"
		args = append(args, like, like)
	}
	return where, args
}

func normalizeTemporaryExternalRechargePage(page int32, pageSize int32) (int32, int32) {
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}
	return page, pageSize
}

func (r *Repository) lookupExternalRechargeOrderByCommand(ctx context.Context, appCode string, commandID string) (ledger.ExternalRechargeOrder, bool, error) {
	order, err := scanExternalRechargeOrder(r.db.QueryRowContext(ctx, externalRechargeOrderSelectSQL()+`
		WHERE app_code = ? AND command_id = ?`,
		appCode, strings.TrimSpace(commandID),
	))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.ExternalRechargeOrder{}, false, nil
	}
	return order, err == nil, err
}

func (r *Repository) lockExternalRechargeOrder(ctx context.Context, tx *sql.Tx, appCode string, orderID string) (ledger.ExternalRechargeOrder, error) {
	order, err := scanExternalRechargeOrder(tx.QueryRowContext(ctx, externalRechargeOrderSelectSQL()+`
		WHERE app_code = ? AND order_id = ?
		FOR UPDATE`,
		appCode, strings.TrimSpace(orderID),
	))
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
	}
	return order, err
}

func externalRechargeOrderSelectSQL() string {
	return `
		SELECT app_code, order_id, command_id, target_user_id, target_region_id, target_country_id, target_country_code, audience_type,
		       product_id, product_code, product_name, coin_amount, usd_minor_amount, provider_code, payment_method_id,
		       country_code, currency_code, provider_amount_minor, pay_way, pay_type, COALESCE(pay_url, ''),
		       provider_order_id, tx_hash, receive_address, status, failure_reason, wallet_transaction_id,
		       COALESCE(CAST(provider_payload AS CHAR), ''), created_at_ms, updated_at_ms
		FROM external_recharge_orders
	`
}

func scanExternalRechargeOrder(scanner scanTarget) (ledger.ExternalRechargeOrder, error) {
	var order ledger.ExternalRechargeOrder
	if err := scanner.Scan(
		&order.AppCode,
		&order.OrderID,
		&order.CommandID,
		&order.TargetUserID,
		&order.TargetRegionID,
		&order.TargetCountryID,
		&order.TargetCountryCode,
		&order.AudienceType,
		&order.ProductID,
		&order.ProductCode,
		&order.ProductName,
		&order.CoinAmount,
		&order.USDMinorAmount,
		&order.ProviderCode,
		&order.PaymentMethodID,
		&order.CountryCode,
		&order.CurrencyCode,
		&order.ProviderAmountMinor,
		&order.PayWay,
		&order.PayType,
		&order.PayURL,
		&order.ProviderOrderID,
		&order.TxHash,
		&order.ReceiveAddress,
		&order.Status,
		&order.FailureReason,
		&order.TransactionID,
		&order.ProviderPayloadJSON,
		&order.CreatedAtMS,
		&order.UpdatedAtMS,
	); err != nil {
		return ledger.ExternalRechargeOrder{}, err
	}
	return order, nil
}

func externalRechargePaymentRef(order ledger.ExternalRechargeOrder) string {
	for _, value := range []string{order.ProviderOrderID, order.TxHash, order.OrderID} {
		if value = strings.TrimSpace(value); value != "" {
			return value
		}
	}
	return ""
}

func externalRechargeOrderFromCommand(command ledger.CreateExternalRechargeOrderCommand, product ledger.RechargeProduct, method *ledger.ThirdPartyPaymentMethod, providerAmountMinor int64, receiveAddress string, nowMS int64) ledger.ExternalRechargeOrder {
	order := ledger.ExternalRechargeOrder{
		OrderID:             externalRechargeOrderID(command.AppCode, command.CommandID),
		AppCode:             command.AppCode,
		CommandID:           strings.TrimSpace(command.CommandID),
		TargetUserID:        command.TargetUserID,
		TargetRegionID:      command.TargetRegionID,
		TargetCountryID:     command.TargetCountryID,
		TargetCountryCode:   strings.ToUpper(strings.TrimSpace(command.TargetCountryCode)),
		AudienceType:        ledger.NormalizeRechargeAudienceType(command.AudienceType),
		ProductID:           product.ProductID,
		ProductCode:         product.ProductCode,
		ProductName:         product.ProductName,
		CoinAmount:          product.CoinAmount,
		USDMinorAmount:      googleAmountMicroToUSDMinor(product.AmountMicro),
		ProviderCode:        ledger.NormalizePaymentProvider(command.ProviderCode),
		ProviderAmountMinor: providerAmountMinor,
		ReceiveAddress:      strings.TrimSpace(receiveAddress),
		Status:              ledger.ExternalRechargeStatusPending,
		CreatedAtMS:         nowMS,
		UpdatedAtMS:         nowMS,
	}
	if method != nil {
		order.PaymentMethodID = method.MethodID
		order.CountryCode = method.CountryCode
		order.CurrencyCode = method.CurrencyCode
		order.PayWay = method.PayWay
		order.PayType = method.PayType
	}
	if order.ProviderCode == ledger.PaymentProviderUSDTTRC20 {
		order.CountryCode = "GLOBAL"
		order.CurrencyCode = ledger.PaidCurrencyUSDT
	}
	return order
}

func temporaryExternalRechargeOrderFromCommand(command ledger.CreateTemporaryExternalRechargeOrderCommand, method ledger.ThirdPartyPaymentMethod, providerAmountMinor int64, nowMS int64) ledger.ExternalRechargeOrder {
	// 临时收款不绑定用户和商品，target/product 字段固定为 0，避免后续回调误入正式充值入账链路。
	return ledger.ExternalRechargeOrder{
		OrderID:             externalRechargeOrderID(command.AppCode, command.CommandID),
		AppCode:             command.AppCode,
		CommandID:           strings.TrimSpace(command.CommandID),
		TargetUserID:        0,
		TargetRegionID:      0,
		TargetCountryCode:   strings.ToUpper(strings.TrimSpace(method.CountryCode)),
		AudienceType:        ledger.RechargeAudienceTemporary,
		ProductID:           0,
		ProductCode:         "temporary",
		ProductName:         temporaryExternalRechargeProductName(command.USDMinorAmount),
		CoinAmount:          command.CoinAmount,
		USDMinorAmount:      command.USDMinorAmount,
		ProviderCode:        ledger.NormalizePaymentProvider(command.ProviderCode),
		PaymentMethodID:     method.MethodID,
		CountryCode:         method.CountryCode,
		CurrencyCode:        method.CurrencyCode,
		ProviderAmountMinor: providerAmountMinor,
		PayWay:              method.PayWay,
		PayType:             method.PayType,
		Status:              ledger.ExternalRechargeStatusPending,
		CreatedAtMS:         nowMS,
		UpdatedAtMS:         nowMS,
	}
}

func temporaryExternalRechargeProductName(usdMinorAmount int64) string {
	if usdMinorAmount <= 0 {
		return "Temporary payment"
	}
	return fmt.Sprintf("Temporary payment USD %d.%02d", usdMinorAmount/100, usdMinorAmount%100)
}

func externalRechargeOrderID(appCode string, commandID string) string {

	return stableHash(appcode.Normalize(appCode) + "|" + strings.TrimSpace(commandID))[:32]
}

func (r *Repository) ensureExternalTxHashUnused(ctx context.Context, tx *sql.Tx, appCode string, providerCode string, txHash string, orderID string) error {
	var existing string
	err := tx.QueryRowContext(ctx, `
		SELECT order_id
		FROM external_recharge_orders
		WHERE app_code = ? AND provider_code = ? AND tx_hash = ? AND order_id <> ? AND status = ?
		LIMIT 1`,
		appCode, providerCode, txHash, orderID, ledger.ExternalRechargeStatusCredited,
	).Scan(&existing)
	if errors.Is(err, sql.ErrNoRows) {
		return nil
	}
	if err != nil {
		return err
	}
	return xerr.New(xerr.Conflict, "tx_hash already credited")
}

func providerRechargeBizType(providerCode string) string {
	switch ledger.NormalizePaymentProvider(providerCode) {
	case ledger.PaymentProviderMifaPay:
		return bizTypeMifaPayRecharge
	case ledger.PaymentProviderV5Pay:
		return bizTypeV5PayRecharge
	case ledger.PaymentProviderUSDTTRC20:
		return bizTypeUSDTTRC20Recharge
	default:
		return strings.ToLower(strings.TrimSpace(providerCode))
	}
}

func externalRechargeRequestHash(order ledger.ExternalRechargeOrder, assetType string) string {
	return stableHash(fmt.Sprintf("external_recharge|%s|%s|%d|%d|%d|%s|%s|%s",
		order.AppCode, order.OrderID, order.TargetUserID, order.ProductID, order.CoinAmount,
		order.ProviderCode, order.ProviderOrderID, assetType,
	))
}

func nullableJSON(payload string) any {
	payload = strings.TrimSpace(payload)
	if payload == "" {
		return nil
	}
	return payload
}

func trimMax(value string, max int) string {
	value = strings.TrimSpace(value)
	if len([]rune(value)) <= max {
		return value
	}
	return string([]rune(value)[:max])
}
