package mysql

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

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

	mysqlDriver "github.com/go-sql-driver/mysql"
)

func isDuplicateColumnError(err error) bool {
	var mysqlErr *mysqlDriver.MySQLError
	return errors.As(err, &mysqlErr) && mysqlErr.Number == 1060
}

type seedPaymentMethod struct {
	CountryCode  string
	CountryName  string
	CurrencyCode string
	PayWay       string
	PayType      string
	MethodName   string
	Rate         string
	SortOrder    int
}

func seedThirdPartyPaymentDefaults(ctx context.Context, db *sql.DB) error {
	nowMS := time.Now().UnixMilli()
	if _, err := db.ExecContext(ctx, `
		INSERT INTO third_party_payment_channels (
		  app_code, provider_code, provider_name, status, sort_order, created_at_ms, updated_at_ms
		) VALUES ('lalu', 'mifapay', 'MiFaPay', 'active', 10, ?, ?)
		ON DUPLICATE KEY UPDATE provider_name = VALUES(provider_name), status = VALUES(status), sort_order = VALUES(sort_order), updated_at_ms = VALUES(updated_at_ms)`, nowMS, nowMS); err != nil {
		return err
	}
	// MiFaPay 是 H5 的统一三方收银台入口：后台只管理 MiFaPay 自己支持的国家和支付产品，不再关心任何下游通道名称。
	// 这里只 seed 可运营的 payWay/payType 清单；商户号、商户标识、商户私钥、平台公钥都属于运行环境密钥，不进入数据库 seed。
	methods := []seedPaymentMethod{
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "BankTransfer", PayType: "BCA", MethodName: "BCA Bank", Rate: "1.00000000", SortOrder: 110},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "BankTransfer", PayType: "BNI", MethodName: "BNI Bank", Rate: "1.00000000", SortOrder: 120},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "BankTransfer", PayType: "BRI", MethodName: "BRI Bank", Rate: "1.00000000", SortOrder: 130},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "BankTransfer", PayType: "Mandiri", MethodName: "Mandiri Bank", Rate: "1.00000000", SortOrder: 140},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "BankTransfer", PayType: "Permata", MethodName: "Permata Bank", Rate: "1.00000000", SortOrder: 150},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "Ewallet", PayType: "DANA", MethodName: "DANA", Rate: "1.00000000", SortOrder: 160},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "Ewallet", PayType: "LinkAja", MethodName: "LinkAja", Rate: "1.00000000", SortOrder: 170},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "Ewallet", PayType: "OVO", MethodName: "OVO", Rate: "1.00000000", SortOrder: 180},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "Ewallet", PayType: "ShopeePay", MethodName: "ShopeePay", Rate: "1.00000000", SortOrder: 190},
		{CountryCode: "ID", CountryName: "Indonesia", CurrencyCode: "IDR", PayWay: "QR", PayType: "QRIS", MethodName: "QRIS", Rate: "1.00000000", SortOrder: 200},
		{CountryCode: "VN", CountryName: "Vietnam", CurrencyCode: "VND", PayWay: "BankTransfer", PayType: "VN_BankTransfer", MethodName: "Vietnam Bank Transfer", Rate: "1.00000000", SortOrder: 210},
		{CountryCode: "VN", CountryName: "Vietnam", CurrencyCode: "VND", PayWay: "QR", PayType: "VietQR", MethodName: "VietQR", Rate: "1.00000000", SortOrder: 220},
		{CountryCode: "BH", CountryName: "Bahrain", CurrencyCode: "BHD", PayWay: "Card", PayType: "CreditCard", MethodName: "Bahrain Credit Card", Rate: "1.00000000", SortOrder: 310},
		{CountryCode: "QA", CountryName: "Qatar", CurrencyCode: "QAR", PayWay: "Card", PayType: "CreditCard", MethodName: "Qatar Credit Card", Rate: "1.00000000", SortOrder: 410},
		{CountryCode: "OM", CountryName: "Oman", CurrencyCode: "OMR", PayWay: "Card", PayType: "CreditCard", MethodName: "Oman Credit Card", Rate: "1.00000000", SortOrder: 510},
		{CountryCode: "AE", CountryName: "United Arab Emirates", CurrencyCode: "AED", PayWay: "Card", PayType: "CreditCard", MethodName: "UAE Credit Card", Rate: "1.00000000", SortOrder: 610},
		{CountryCode: "KW", CountryName: "Kuwait", CurrencyCode: "KWD", PayWay: "Card", PayType: "Knet", MethodName: "KNET", Rate: "1.00000000", SortOrder: 710},
		{CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR", PayWay: "Card", PayType: "MADA", MethodName: "MADA", Rate: "1.00000000", SortOrder: 810},
		{CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR", PayWay: "Ewallet", PayType: "ApplePay", MethodName: "ApplePay", Rate: "1.00000000", SortOrder: 820},
		{CountryCode: "SA", CountryName: "Saudi Arabia", CurrencyCode: "SAR", PayWay: "Ewallet", PayType: "STCPay", MethodName: "STCPay", Rate: "1.00000000", SortOrder: 830},
		{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", PayWay: "Ewallet", PayType: "BHIM", MethodName: "BHIM", Rate: "1.00000000", SortOrder: 910},
		{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", PayWay: "Ewallet", PayType: "GooglePay", MethodName: "GooglePay", Rate: "1.00000000", SortOrder: 920},
		{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", PayWay: "Ewallet", PayType: "Mobikwik", MethodName: "Mobikwik", Rate: "1.00000000", SortOrder: 930},
		{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", PayWay: "Ewallet", PayType: "Paytm", MethodName: "Paytm", Rate: "1.00000000", SortOrder: 940},
		{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", PayWay: "Ewallet", PayType: "PhonePe", MethodName: "PhonePe", Rate: "1.00000000", SortOrder: 950},
		{CountryCode: "IN", CountryName: "India", CurrencyCode: "INR", PayWay: "Ewallet", PayType: "UPI", MethodName: "UPI", Rate: "1.00000000", SortOrder: 960},
		{CountryCode: "BD", CountryName: "Bangladesh", CurrencyCode: "BDT", PayWay: "Ewallet", PayType: "bKash", MethodName: "bKash", Rate: "1.00000000", SortOrder: 1010},
		{CountryCode: "PK", CountryName: "Pakistan", CurrencyCode: "PKR", PayWay: "Ewallet", PayType: "Easypaisa", MethodName: "Easypaisa", Rate: "1.00000000", SortOrder: 1110},
		{CountryCode: "PK", CountryName: "Pakistan", CurrencyCode: "PKR", PayWay: "Ewallet", PayType: "JazzCash", MethodName: "JazzCash", Rate: "1.00000000", SortOrder: 1120},
		{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP", PayWay: "Ewallet", PayType: "FawryPay", MethodName: "FawryPay", Rate: "1.00000000", SortOrder: 1210},
		{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP", PayWay: "Ewallet", PayType: "Meeza", MethodName: "Meeza", Rate: "1.00000000", SortOrder: 1220},
		{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP", PayWay: "Ewallet", PayType: "Orange", MethodName: "Orange", Rate: "1.00000000", SortOrder: 1230},
		{CountryCode: "EG", CountryName: "Egypt", CurrencyCode: "EGP", PayWay: "Ewallet", PayType: "Vodafone", MethodName: "Vodafone", Rate: "1.00000000", SortOrder: 1240},
		{CountryCode: "PH", CountryName: "Philippines", CurrencyCode: "PHP", PayWay: "Ewallet", PayType: "Gcash", MethodName: "GCash", Rate: "1.00000000", SortOrder: 1310},
		{CountryCode: "PH", CountryName: "Philippines", CurrencyCode: "PHP", PayWay: "Ewallet", PayType: "PayMaya", MethodName: "PayMaya", Rate: "1.00000000", SortOrder: 1320},
		{CountryCode: "PH", CountryName: "Philippines", CurrencyCode: "PHP", PayWay: "QR", PayType: "QRPH", MethodName: "QRPH", Rate: "1.00000000", SortOrder: 1330},
	}
	for _, method := range methods {
		rate := strings.TrimSpace(method.Rate)
		if rate == "" {
			rate = "1.00000000"
		}
		if _, err := db.ExecContext(ctx, `
			INSERT INTO third_party_payment_methods (
			  app_code, provider_code, country_code, country_name, currency_code, pay_way, pay_type,
			  method_name, logo_url, status, usd_to_currency_rate, sort_order, created_at_ms, updated_at_ms
			) VALUES ('lalu', 'mifapay', ?, ?, ?, ?, ?, ?, '', 'active', ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
			  country_name = VALUES(country_name),
			  currency_code = VALUES(currency_code),
			  method_name = VALUES(method_name),
			  sort_order = VALUES(sort_order),
			  usd_to_currency_rate = IF(usd_to_currency_rate <= 0, VALUES(usd_to_currency_rate), usd_to_currency_rate),
			  updated_at_ms = VALUES(updated_at_ms)`,
			strings.ToUpper(method.CountryCode), method.CountryName, strings.ToUpper(method.CurrencyCode),
			method.PayWay, method.PayType, method.MethodName, rate, method.SortOrder, nowMS, nowMS,
		); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ListThirdPartyPaymentChannels(ctx context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	app := appcode.FromContext(ctx)
	providerCode := strings.ToLower(strings.TrimSpace(query.ProviderCode))
	status := ledger.NormalizeThirdPartyPaymentStatus(query.Status)
	where := `WHERE app_code = ?`
	args := []any{app}
	if providerCode != "" {
		where += ` AND provider_code = ?`
		args = append(args, providerCode)
	}
	if status != "" {
		where += ` AND status = ?`
		args = append(args, status)
	}
	rows, err := r.db.QueryContext(ctx, `
		SELECT app_code, provider_code, provider_name, status, sort_order
		FROM third_party_payment_channels
		`+where+`
		ORDER BY sort_order ASC, provider_code ASC`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	channels := make([]ledger.ThirdPartyPaymentChannel, 0)
	for rows.Next() {
		var channel ledger.ThirdPartyPaymentChannel
		if err := rows.Scan(&channel.AppCode, &channel.ProviderCode, &channel.ProviderName, &channel.Status, &channel.SortOrder); err != nil {
			return nil, err
		}
		channels = append(channels, channel)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	for index := range channels {
		methods, err := r.listThirdPartyPaymentMethods(ctx, app, channels[index].ProviderCode, "", query.IncludeDisabledMethods, false)
		if err != nil {
			return nil, err
		}
		channels[index].Methods = methods
	}
	return channels, nil
}

func (r *Repository) SetThirdPartyPaymentMethodStatus(ctx context.Context, command ledger.ThirdPartyPaymentMethodStatusCommand) (ledger.ThirdPartyPaymentMethod, error) {
	if r == nil || r.db == nil {
		return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	status := ledger.ThirdPartyPaymentStatusDisabled
	if command.Enabled {
		status = ledger.ThirdPartyPaymentStatusActive
	}
	result, err := r.db.ExecContext(ctx, `
		UPDATE third_party_payment_methods
		SET status = ?, updated_at_ms = ?
		WHERE app_code = ? AND method_id = ?`,
		status, time.Now().UnixMilli(), appcode.FromContext(ctx), command.MethodID,
	)
	if err != nil {
		return ledger.ThirdPartyPaymentMethod{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return ledger.ThirdPartyPaymentMethod{}, err
	} else if affected == 0 {
		return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.NotFound, "payment method not found")
	}
	return r.GetThirdPartyPaymentMethod(ctx, appcode.FromContext(ctx), command.MethodID)
}

func (r *Repository) UpdateThirdPartyPaymentRate(ctx context.Context, command ledger.ThirdPartyPaymentRateCommand) (ledger.ThirdPartyPaymentMethod, error) {
	if r == nil || r.db == nil {
		return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, command.AppCode)
	result, err := r.db.ExecContext(ctx, `
		UPDATE third_party_payment_methods
		SET usd_to_currency_rate = ?, updated_at_ms = ?
		WHERE app_code = ? AND method_id = ?`,
		command.USDToCurrencyRate, time.Now().UnixMilli(), appcode.FromContext(ctx), command.MethodID,
	)
	if err != nil {
		return ledger.ThirdPartyPaymentMethod{}, err
	}
	if affected, err := result.RowsAffected(); err != nil {
		return ledger.ThirdPartyPaymentMethod{}, err
	} else if affected == 0 {
		return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.NotFound, "payment method not found")
	}
	return r.GetThirdPartyPaymentMethod(ctx, appcode.FromContext(ctx), command.MethodID)
}

func (r *Repository) GetThirdPartyPaymentMethod(ctx context.Context, appCode string, methodID int64) (ledger.ThirdPartyPaymentMethod, error) {
	ctx = contextWithCommandApp(ctx, appCode)
	row := r.db.QueryRowContext(ctx, thirdPartyPaymentMethodSelectSQL()+`
		WHERE m.app_code = ? AND m.method_id = ?`,
		appcode.FromContext(ctx), methodID,
	)
	method, err := scanThirdPartyPaymentMethod(row)
	if errors.Is(err, sql.ErrNoRows) {
		return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.NotFound, "payment method not found")
	}
	return method, err
}

func (r *Repository) ListH5RechargeOptions(ctx context.Context, query ledger.H5RechargeOptionsQuery) (ledger.H5RechargeOptions, error) {
	if r == nil || r.db == nil {
		return ledger.H5RechargeOptions{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, query.AppCode)
	products, _, err := r.ListRechargeProducts(ctx, query.TargetUserID, query.TargetRegionID, ledger.RechargeProductPlatformWeb, query.AudienceType)
	if err != nil {
		return ledger.H5RechargeOptions{}, err
	}
	methods, err := r.listThirdPartyPaymentMethods(ctx, appcode.FromContext(ctx), ledger.PaymentProviderMifaPay, strings.ToUpper(strings.TrimSpace(query.TargetCountryCode)), false, true)
	if err != nil {
		return ledger.H5RechargeOptions{}, err
	}
	return ledger.H5RechargeOptions{Products: products, PaymentMethods: methods}, nil
}

func (r *Repository) listThirdPartyPaymentMethods(ctx context.Context, appCode string, providerCode string, countryCode string, includeDisabled bool, requireRate bool) ([]ledger.ThirdPartyPaymentMethod, error) {
	where := `WHERE m.app_code = ? AND m.provider_code = ?`
	args := []any{appCode, providerCode}
	if countryCode != "" {
		where += ` AND m.country_code IN (?, 'GLOBAL')`
		args = append(args, countryCode)
	}
	if !includeDisabled {
		where += ` AND m.status = 'active'`
	}
	if requireRate {
		where += ` AND m.usd_to_currency_rate > 0`
	}
	rows, err := r.db.QueryContext(ctx, thirdPartyPaymentMethodSelectSQL()+`
		`+where+`
		ORDER BY CASE WHEN m.country_code = 'GLOBAL' THEN 1 ELSE 0 END ASC, m.sort_order ASC, m.method_id ASC`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	methods := make([]ledger.ThirdPartyPaymentMethod, 0)
	for rows.Next() {
		method, err := scanThirdPartyPaymentMethod(rows)
		if err != nil {
			return nil, err
		}
		methods = append(methods, method)
	}
	return methods, rows.Err()
}

func thirdPartyPaymentMethodSelectSQL() string {
	return `
		SELECT m.method_id, m.app_code, m.provider_code, c.provider_name, m.country_code, m.country_name,
		       m.currency_code, m.pay_way, m.pay_type, m.method_name, m.logo_url, m.status,
		       CAST(m.usd_to_currency_rate AS CHAR), m.sort_order, m.created_at_ms, m.updated_at_ms
		FROM third_party_payment_methods m
		JOIN third_party_payment_channels c ON c.app_code = m.app_code AND c.provider_code = m.provider_code
	`
}

func scanThirdPartyPaymentMethod(scanner scanTarget) (ledger.ThirdPartyPaymentMethod, error) {
	var method ledger.ThirdPartyPaymentMethod
	if err := scanner.Scan(
		&method.MethodID,
		&method.AppCode,
		&method.ProviderCode,
		&method.ProviderName,
		&method.CountryCode,
		&method.CountryName,
		&method.CurrencyCode,
		&method.PayWay,
		&method.PayType,
		&method.MethodName,
		&method.LogoURL,
		&method.Status,
		&method.USDToCurrencyRate,
		&method.SortOrder,
		&method.CreatedAtMS,
		&method.UpdatedAtMS,
	); err != nil {
		return ledger.ThirdPartyPaymentMethod{}, err
	}
	return method, nil
}

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)
	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_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.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,
		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,
	}
	if !isCoinSellerRecharge {
		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, metadata, 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
	}
	// 普通用户 H5 充值进入普通 COIN 钱包，并写普通充值记录/累计统计，供首充、累充、VIP 等用户侧口径消费。
	// 币商 H5 充值只进入 COIN_SELLER_COIN 库存，并写币商库存记录；不能写 wallet_recharge_records，避免被误算成普通用户充值。
	outboxEvents := []walletOutboxEvent{
		balanceChangedEvent(transactionID, order.CommandID, order.TargetUserID, assetType, order.CoinAmount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMS),
	}
	if isCoinSellerRecharge {
		paidAmountMicro, err := checkedMul(order.ProviderAmountMinor, 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:       order.CurrencyCode,
			PaidAmountMicro:        paidAmountMicro,
			PaymentRef:             externalRechargePaymentRef(order),
			EvidenceRef:            order.OrderID,
			Reason:                 "h5 external recharge",
			AssetType:              ledger.AssetCoinSellerCoin,
			CountsAsSellerRecharge: true,
			BalanceAfter:           balanceAfter,
			CreatedAtMS:            nowMS,
		}
		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) 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 <> ?`,
		ledger.ExternalRechargeStatusFailed, trimMax(reason, 512), nullableJSON(payloadJSON), time.Now().UnixMilli(),
		appcode.FromContext(ctx), strings.TrimSpace(orderID), ledger.ExternalRechargeStatusCredited,
	)
	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) 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_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.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:             "h5r_" + stableHash(command.AppCode+"|"+command.CommandID),
		AppCode:             command.AppCode,
		CommandID:           strings.TrimSpace(command.CommandID),
		TargetUserID:        command.TargetUserID,
		TargetRegionID:      command.TargetRegionID,
		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 (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.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])
}

func decimalRateIsPositive(rate string) bool {
	value, err := strconv.ParseFloat(strings.TrimSpace(rate), 64)
	return err == nil && value > 0
}
