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"
)

// AdminCreditCoinSellerStock 在同一事务里调整币商 COIN_SELLER_COIN 库存，并记录专用进货/冲回明细。
func (r *Repository) AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.CoinSellerStockCreditReceipt{}, 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.CoinSellerStockCreditReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	bizType := coinSellerStockBizType(command.StockType)
	if bizType == "" {
		return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerStockTypeInvalid, "stock_type is invalid")
	}
	requestHash := coinSellerStockRequestHash(command)
	if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizType, xerr.IdempotencyConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.CoinSellerStockCreditReceipt{}, err
		}
		return r.receiptForCoinSellerStockTransaction(ctx, tx, txRow.TransactionID)
	}
	if command.PaymentRef != "" {
		if duplicated, err := r.coinSellerStockPaymentRefExists(ctx, tx, command.StockType, command.PaymentRef); err != nil || duplicated {
			if err != nil {
				return ledger.CoinSellerStockCreditReceipt{}, err
			}
			return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.CoinSellerPaymentRefDuplicated, "payment_ref is duplicated")
		}
	}

	nowMs := time.Now().UnixMilli()
	availableDelta := coinSellerStockAvailableDelta(command)
	account, err := r.lockAccount(ctx, tx, command.SellerUserID, ledger.AssetCoinSellerCoin, availableDelta > 0, nowMs)
	if err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}
	if availableDelta < 0 && account.AvailableAmount < command.CoinAmount {
		return ledger.CoinSellerStockCreditReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	balanceAfter, err := checkedAddDelta(account.AvailableAmount, availableDelta)
	if err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}

	transactionID := transactionID(command.AppCode, command.CommandID)
	countsAsSellerRecharge := command.StockType == ledger.StockTypeUSDTPurchase || command.StockType == ledger.StockTypeUSDTDeduction
	recordCoinAmount := availableDelta
	recordPaidAmountMicro := coinSellerStockPaidAmountMicro(command)
	metadata := coinSellerStockMetadata{
		AppCode:                command.AppCode,
		SellerUserID:           command.SellerUserID,
		SellerCountryID:        command.SellerCountryID,
		SellerRegionID:         command.SellerRegionID,
		StockType:              command.StockType,
		CoinAmount:             recordCoinAmount,
		PaidCurrencyCode:       command.PaidCurrencyCode,
		PaidAmountMicro:        recordPaidAmountMicro,
		PaymentRef:             command.PaymentRef,
		EvidenceRef:            command.EvidenceRef,
		OperatorUserID:         command.OperatorUserID,
		Reason:                 command.Reason,
		AssetType:              ledger.AssetCoinSellerCoin,
		CountsAsSellerRecharge: countsAsSellerRecharge,
		BalanceAfter:           balanceAfter,
		CreatedAtMS:            nowMs,
	}
	externalRef := command.EvidenceRef
	if command.PaymentRef != "" {
		externalRef = command.PaymentRef
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizType, requestHash, externalRef, metadata, nowMs); err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, account, availableDelta, 0, nowMs); err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.SellerUserID,
		AssetType:      ledger.AssetCoinSellerCoin,
		AvailableDelta: availableDelta,
		FrozenDelta:    0,
		AvailableAfter: balanceAfter,
		FrozenAfter:    account.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}
	if err := r.insertCoinSellerStockRecord(ctx, tx, transactionID, command.CommandID, metadata, nowMs); err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.SellerUserID, ledger.AssetCoinSellerCoin, availableDelta, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMs),
		coinSellerStockCreditedEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.CoinSellerStockCreditReceipt{}, err
	}

	return receiptFromCoinSellerStockMetadata(transactionID, metadata), nil
}

// TransferCoinFromSeller 在同一事务里扣币商专用金币、给玩家普通 COIN 入账，并记录充值口径。
func (r *Repository) TransferCoinFromSeller(ctx context.Context, command ledger.CoinSellerTransferCommand) (ledger.CoinSellerTransferReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.CoinSellerTransferReceipt{}, 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.CoinSellerTransferReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := coinSellerTransferRequestHash(command)
	if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeCoinSellerTransfer); err != nil || exists {
		if err != nil || !exists {
			return ledger.CoinSellerTransferReceipt{}, err
		}
		return r.receiptForCoinSellerTransferTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	seller, err := r.lockAccount(ctx, tx, command.SellerUserID, ledger.AssetCoinSellerCoin, false, nowMs)
	if err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if seller.AvailableAmount < command.Amount {
		return ledger.CoinSellerTransferReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	target, err := r.lockAccount(ctx, tx, command.TargetUserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}

	transactionID := transactionID(command.AppCode, command.CommandID)

	rechargeUSDMinor := int64(0)
	rechargeSequence, err := r.reserveUserRechargeSequence(ctx, tx, command.AppCode, command.TargetUserID, transactionID, command.Amount, rechargeUSDMinor, nowMs)
	if err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	sellerAfter := seller.AvailableAmount - command.Amount
	targetAfter := target.AvailableAmount + command.Amount
	metadata := coinSellerTransferMetadata{
		AppCode:                      command.AppCode,
		SellerUserID:                 command.SellerUserID,
		TargetUserID:                 command.TargetUserID,
		TargetCountryID:              command.TargetCountryID,
		SellerRegionID:               command.SellerRegionID,
		TargetRegionID:               command.TargetRegionID,
		Amount:                       command.Amount,
		Reason:                       command.Reason,
		SellerAssetType:              ledger.AssetCoinSellerCoin,
		TargetAssetType:              ledger.AssetCoin,
		SellerBalanceAfter:           sellerAfter,
		TargetBalanceAfter:           targetAfter,
		RechargeSequence:             rechargeSequence,
		RechargeUSDMinor:             rechargeUSDMinor,
		RechargeCurrencyCode:         "",
		RechargePolicyID:             0,
		RechargePolicyVersion:        "",
		RechargePolicyCoinAmount:     0,
		RechargePolicyUSDMinorAmount: 0,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeCoinSellerTransfer, requestHash, fmt.Sprintf("coin_seller:%d:%d", command.SellerUserID, command.TargetUserID), metadata, nowMs); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, seller, -command.Amount, 0, nowMs); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.SellerUserID,
		AssetType:          ledger.AssetCoinSellerCoin,
		AvailableDelta:     -command.Amount,
		FrozenDelta:        0,
		AvailableAfter:     sellerAfter,
		FrozenAfter:        seller.FrozenAmount,
		CounterpartyUserID: command.TargetUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, target, command.Amount, 0, nowMs); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.TargetUserID,
		AssetType:          ledger.AssetCoin,
		AvailableDelta:     command.Amount,
		FrozenDelta:        0,
		AvailableAfter:     targetAfter,
		FrozenAfter:        target.FrozenAmount,
		CounterpartyUserID: command.SellerUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := r.insertRechargeRecord(ctx, tx, transactionID, metadata, nowMs); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.SellerUserID, ledger.AssetCoinSellerCoin, -command.Amount, 0, sellerAfter, seller.FrozenAmount, seller.Version+1, metadata, nowMs),
		balanceChangedEvent(transactionID, command.CommandID, command.TargetUserID, ledger.AssetCoin, command.Amount, 0, targetAfter, target.FrozenAmount, target.Version+1, metadata, nowMs),
		coinSellerTransferredEvent(transactionID, command.CommandID, command.SellerUserID, -command.Amount, metadata, nowMs),
		rechargeRecordedEvent(transactionID, command.CommandID, command.TargetUserID, command.Amount, metadata, nowMs),
	}); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.CoinSellerTransferReceipt{}, err
	}

	return receiptFromCoinSellerTransferMetadata(transactionID, metadata), nil
}

// TransferCoinSellerStockToChild 在父子币商 COIN_SELLER_COIN 账户之间转移库存；它不是玩家充值，不写充值统计表。
func (r *Repository) TransferCoinSellerStockToChild(ctx context.Context, command ledger.CoinSellerSubTransferCommand) (ledger.CoinSellerSubTransferReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.CoinSellerSubTransferReceipt{}, 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.CoinSellerSubTransferReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := coinSellerSubTransferRequestHash(command)
	if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypeCoinSellerSubTransfer, xerr.IdempotencyConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.CoinSellerSubTransferReceipt{}, err
		}
		return r.receiptForCoinSellerSubTransferTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	// 父子账户使用同一资产，按统一顺序加锁，避免未来出现双向划拨时产生反向锁等待。
	states, err := r.lockAccountsOrdered(ctx, tx, []walletAccountLockRequest{
		{UserID: command.ParentUserID, AssetType: ledger.AssetCoinSellerCoin, CreateIfMissing: false},
		{UserID: command.ChildUserID, AssetType: ledger.AssetCoinSellerCoin, CreateIfMissing: true},
	}, nowMs)
	if err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	parent := states[walletAccountLockKey(command.ParentUserID, ledger.AssetCoinSellerCoin)].account
	if parent.AvailableAmount < command.Amount {
		return ledger.CoinSellerSubTransferReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	child := states[walletAccountLockKey(command.ChildUserID, ledger.AssetCoinSellerCoin)].account
	parentAfter := parent.AvailableAmount - command.Amount
	childAfter, err := checkedAdd(child.AvailableAmount, command.Amount)
	if err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	transactionID := transactionID(command.AppCode, command.CommandID)
	metadata := coinSellerSubTransferMetadata{
		AppCode:            command.AppCode,
		ParentUserID:       command.ParentUserID,
		ChildUserID:        command.ChildUserID,
		RegionID:           command.RegionID,
		Amount:             command.Amount,
		Reason:             command.Reason,
		AssetType:          ledger.AssetCoinSellerCoin,
		ParentBalanceAfter: parentAfter,
		ChildBalanceAfter:  childAfter,
		CreatedAtMS:        nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeCoinSellerSubTransfer, requestHash, fmt.Sprintf("coin_seller_sub:%d:%d", command.ParentUserID, command.ChildUserID), metadata, nowMs); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, parent, -command.Amount, 0, nowMs); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.ParentUserID,
		AssetType:          ledger.AssetCoinSellerCoin,
		AvailableDelta:     -command.Amount,
		FrozenDelta:        0,
		AvailableAfter:     parentAfter,
		FrozenAfter:        parent.FrozenAmount,
		CounterpartyUserID: command.ChildUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, child, command.Amount, 0, nowMs); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.ChildUserID,
		AssetType:          ledger.AssetCoinSellerCoin,
		AvailableDelta:     command.Amount,
		FrozenDelta:        0,
		AvailableAfter:     childAfter,
		FrozenAfter:        child.FrozenAmount,
		CounterpartyUserID: command.ParentUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.ParentUserID, ledger.AssetCoinSellerCoin, -command.Amount, 0, parentAfter, parent.FrozenAmount, parent.Version+1, metadata, nowMs, bizTypeCoinSellerSubTransfer),
		balanceChangedEvent(transactionID, command.CommandID, command.ChildUserID, ledger.AssetCoinSellerCoin, command.Amount, 0, childAfter, child.FrozenAmount, child.Version+1, metadata, nowMs, bizTypeCoinSellerSubTransfer),
		coinSellerSubTransferredEvent(transactionID, command.CommandID, metadata, nowMs),
	}); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.CoinSellerSubTransferReceipt{}, err
	}

	return receiptFromCoinSellerSubTransferMetadata(transactionID, metadata), nil
}

// ListCoinSellerSalaryExchangeRateTiers 返回指定区域工资转币商的兑换区间；默认只返回 active 区间。
func (r *Repository) ListCoinSellerSalaryExchangeRateTiers(ctx context.Context, appCode string, regionID int64, includeDisabled bool) ([]ledger.CoinSellerSalaryExchangeRateTier, error) {
	if r == nil || r.db == nil {
		return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
	}
	ctx = contextWithCommandApp(ctx, appCode)
	statusClause := "AND status = 'active'"
	if includeDisabled {
		statusClause = ""
	}
	query := fmt.Sprintf(`
		SELECT region_id, min_usd_minor, max_usd_minor, coin_per_usd, status, sort_order, updated_at_ms
		FROM coin_seller_salary_exchange_rate_tiers
		WHERE app_code = ? AND region_id = ? %s
		ORDER BY sort_order DESC, min_usd_minor ASC, tier_id ASC`, statusClause)
	rows, err := r.db.QueryContext(ctx, query, appcode.FromContext(ctx), regionID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	tiers := make([]ledger.CoinSellerSalaryExchangeRateTier, 0)
	for rows.Next() {
		var tier ledger.CoinSellerSalaryExchangeRateTier
		if err := rows.Scan(&tier.RegionID, &tier.MinUSDMinor, &tier.MaxUSDMinor, &tier.CoinPerUSD, &tier.Status, &tier.SortOrder, &tier.UpdatedAtMS); err != nil {
			return nil, err
		}
		tiers = append(tiers, tier)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return tiers, nil
}

// ExchangeSalaryToCoin 在同一事务里扣工资美元钱包、给当前用户普通 COIN 入账，保证两边余额和幂等一起落库。
func (r *Repository) ExchangeSalaryToCoin(ctx context.Context, command ledger.SalaryExchangeCommand) (ledger.SalaryExchangeReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.SalaryExchangeReceipt{}, 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.SalaryExchangeReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := salaryExchangeRequestHash(command)
	if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypeSalaryExchangeToCoin, xerr.IdempotencyConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.SalaryExchangeReceipt{}, err
		}
		return r.receiptForSalaryExchangeTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	coinAmount, err := calculateSalaryCoinAmount(command.SalaryUSDMinor, ledger.SalaryExchangeCoinPerUSD)
	if err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	salaryAccount, err := r.lockAccount(ctx, tx, command.UserID, command.SalaryAssetType, false, nowMs)
	if err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if salaryAccount.AvailableAmount < command.SalaryUSDMinor {
		return ledger.SalaryExchangeReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	coinAccount, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, true, nowMs)
	if err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}

	transactionID := transactionID(command.AppCode, command.CommandID)
	salaryAfter := salaryAccount.AvailableAmount - command.SalaryUSDMinor
	coinAfter, err := checkedAdd(coinAccount.AvailableAmount, coinAmount)
	if err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	metadata := salaryExchangeMetadata{
		AppCode:            command.AppCode,
		UserID:             command.UserID,
		SalaryAssetType:    command.SalaryAssetType,
		CoinAssetType:      ledger.AssetCoin,
		SalaryUSDMinor:     command.SalaryUSDMinor,
		CoinPerUSD:         ledger.SalaryExchangeCoinPerUSD,
		CoinAmount:         coinAmount,
		SalaryBalanceAfter: salaryAfter,
		CoinBalanceAfter:   coinAfter,
		Reason:             command.Reason,
		CreatedAtMS:        nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeSalaryExchangeToCoin, requestHash, fmt.Sprintf("salary_exchange:%d", command.UserID), metadata, nowMs); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, salaryAccount, -command.SalaryUSDMinor, 0, nowMs); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      command.SalaryAssetType,
		AvailableDelta: -command.SalaryUSDMinor,
		FrozenDelta:    0,
		AvailableAfter: salaryAfter,
		FrozenAfter:    salaryAccount.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, coinAccount, coinAmount, 0, nowMs); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:  transactionID,
		UserID:         command.UserID,
		AssetType:      ledger.AssetCoin,
		AvailableDelta: coinAmount,
		FrozenDelta:    0,
		AvailableAfter: coinAfter,
		FrozenAfter:    coinAccount.FrozenAmount,
		CreatedAtMS:    nowMs,
	}); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, command.SalaryAssetType, -command.SalaryUSDMinor, 0, salaryAfter, salaryAccount.FrozenAmount, salaryAccount.Version+1, metadata, nowMs),
		balanceChangedEvent(transactionID, command.CommandID, command.UserID, ledger.AssetCoin, coinAmount, 0, coinAfter, coinAccount.FrozenAmount, coinAccount.Version+1, metadata, nowMs),
	}); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.SalaryExchangeReceipt{}, err
	}

	return receiptFromSalaryExchangeMetadata(transactionID, metadata), nil
}

// TransferSalaryToCoinSeller 在同一事务里扣来源工资钱包、按当前区域配置给币商专用金币库存入账。
func (r *Repository) TransferSalaryToCoinSeller(ctx context.Context, command ledger.SalaryTransferToCoinSellerCommand) (ledger.SalaryTransferToCoinSellerReceipt, error) {
	if r == nil || r.db == nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, 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.SalaryTransferToCoinSellerReceipt{}, err
	}
	defer func() { _ = tx.Rollback() }()

	requestHash := salaryTransferToCoinSellerRequestHash(command)
	if txRow, exists, err := r.lookupTransactionWithConflictCode(ctx, tx, command.CommandID, requestHash, bizTypeSalaryTransferToCoinSeller, xerr.IdempotencyConflict); err != nil || exists {
		if err != nil || !exists {
			return ledger.SalaryTransferToCoinSellerReceipt{}, err
		}
		return r.receiptForSalaryTransferToCoinSellerTransaction(ctx, tx, txRow.TransactionID)
	}

	nowMs := time.Now().UnixMilli()
	rate, err := r.resolveCoinSellerSalaryExchangeRateTier(ctx, tx, command.RegionID, command.SalaryUSDMinor)
	if err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	coinAmount, err := calculateSalaryCoinAmount(command.SalaryUSDMinor, rate.CoinPerUSD)
	if err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	source, err := r.lockAccount(ctx, tx, command.SourceUserID, command.SalaryAssetType, false, nowMs)
	if err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if source.AvailableAmount < command.SalaryUSDMinor {
		return ledger.SalaryTransferToCoinSellerReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
	}
	seller, err := r.lockAccount(ctx, tx, command.SellerUserID, ledger.AssetCoinSellerCoin, true, nowMs)
	if err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if ledger.UsesHostAgencySalaryPolicy(command.SalaryAssetType) {
		// Host/Agency 工资转币商和平台提现共享后台 Host 政策；资产来源决定规则，不按应用名分支。
		// 未发布本月及紧邻上月政策时保留旧工资余额的无限次转出能力；已发布政策仍正常限制。
		if err := r.consumeHostPolicyWithdrawalLimitForPolicyType(ctx, tx, command.SourceUserID, command.RegionID, pointWithdrawalChannelCoinSeller, command.CommandID, "", nowMs, command.SalaryAssetType, "", allowLegacyWithoutHostPolicy); err != nil {
			return ledger.SalaryTransferToCoinSellerReceipt{}, err
		}
	}

	transactionID := transactionID(command.AppCode, command.CommandID)
	sourceAfter := source.AvailableAmount - command.SalaryUSDMinor
	sellerAfter, err := checkedAdd(seller.AvailableAmount, coinAmount)
	if err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	metadata := salaryTransferToCoinSellerMetadata{
		AppCode:                  command.AppCode,
		SourceUserID:             command.SourceUserID,
		SellerUserID:             command.SellerUserID,
		RegionID:                 command.RegionID,
		SalaryAssetType:          command.SalaryAssetType,
		SellerAssetType:          ledger.AssetCoinSellerCoin,
		SalaryUSDMinor:           command.SalaryUSDMinor,
		CoinPerUSD:               rate.CoinPerUSD,
		CoinAmount:               coinAmount,
		RateMinUSDMinor:          rate.MinUSDMinor,
		RateMaxUSDMinor:          rate.MaxUSDMinor,
		SourceSalaryBalanceAfter: sourceAfter,
		SellerBalanceAfter:       sellerAfter,
		Reason:                   command.Reason,
		CreatedAtMS:              nowMs,
	}
	if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeSalaryTransferToCoinSeller, requestHash, fmt.Sprintf("salary_coin_seller:%d:%d", command.SourceUserID, command.SellerUserID), metadata, nowMs); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, source, -command.SalaryUSDMinor, 0, nowMs); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.SourceUserID,
		AssetType:          command.SalaryAssetType,
		AvailableDelta:     -command.SalaryUSDMinor,
		FrozenDelta:        0,
		AvailableAfter:     sourceAfter,
		FrozenAfter:        source.FrozenAmount,
		CounterpartyUserID: command.SellerUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if err := r.applyAccountDelta(ctx, tx, seller, coinAmount, 0, nowMs); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if err := r.insertEntry(ctx, tx, walletEntry{
		TransactionID:      transactionID,
		UserID:             command.SellerUserID,
		AssetType:          ledger.AssetCoinSellerCoin,
		AvailableDelta:     coinAmount,
		FrozenDelta:        0,
		AvailableAfter:     sellerAfter,
		FrozenAfter:        seller.FrozenAmount,
		CounterpartyUserID: command.SourceUserID,
		CreatedAtMS:        nowMs,
	}); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
		balanceChangedEvent(transactionID, command.CommandID, command.SourceUserID, command.SalaryAssetType, -command.SalaryUSDMinor, 0, sourceAfter, source.FrozenAmount, source.Version+1, metadata, nowMs, bizTypeSalaryTransferToCoinSeller),
		balanceChangedEvent(transactionID, command.CommandID, command.SellerUserID, ledger.AssetCoinSellerCoin, coinAmount, 0, sellerAfter, seller.FrozenAmount, seller.Version+1, metadata, nowMs, bizTypeSalaryTransferToCoinSeller),
	}); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}
	if err := tx.Commit(); err != nil {
		return ledger.SalaryTransferToCoinSellerReceipt{}, err
	}

	return receiptFromSalaryTransferToCoinSellerMetadata(transactionID, metadata), nil
}

func (r *Repository) insertRechargeRecord(ctx context.Context, tx *sql.Tx, transactionID string, metadata coinSellerTransferMetadata, nowMs int64) error {
	_, err := tx.ExecContext(ctx,
		`INSERT INTO wallet_recharge_records (
		   app_code, transaction_id, user_id, recharge_sequence, seller_user_id, seller_region_id, target_region_id,
		   policy_id, policy_version, currency_code, coin_amount, usd_minor_amount,
		   exchange_coin_amount, exchange_usd_minor_amount, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		normalizedEntryAppCode(ctx, metadata.AppCode),
		transactionID,
		metadata.TargetUserID,
		metadata.RechargeSequence,
		metadata.SellerUserID,
		metadata.SellerRegionID,
		metadata.TargetRegionID,
		metadata.RechargePolicyID,
		metadata.RechargePolicyVersion,
		metadata.RechargeCurrencyCode,
		metadata.Amount,
		metadata.RechargeUSDMinor,
		metadata.Amount,
		metadata.RechargePolicyUSDMinorAmount,
		nowMs,
	)
	return err
}

func (r *Repository) reserveUserRechargeSequence(ctx context.Context, tx *sql.Tx, appCode string, userID int64, transactionID string, coinAmount int64, usdMinorAmount int64, nowMs int64) (int64, error) {
	normalizedApp := normalizedEntryAppCode(ctx, appCode)
	var count int64
	err := tx.QueryRowContext(ctx, `
		SELECT recharge_count
		FROM wallet_user_recharge_stats
		WHERE app_code = ? AND user_id = ?
		FOR UPDATE`,
		normalizedApp, userID,
	).Scan(&count)
	if errors.Is(err, sql.ErrNoRows) {
		_, err = tx.ExecContext(ctx, `
			INSERT INTO wallet_user_recharge_stats (
				app_code, user_id, recharge_count, first_transaction_id, first_recharged_at_ms,
				total_coin_amount, total_usd_minor_amount, created_at_ms, updated_at_ms
			) VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?)`,
			normalizedApp, userID, transactionID, nowMs, coinAmount, usdMinorAmount, nowMs, nowMs,
		)
		if err != nil {
			return 0, err
		}
		return 1, nil
	}
	if err != nil {
		return 0, err
	}
	next := count + 1
	_, err = tx.ExecContext(ctx, `
		UPDATE wallet_user_recharge_stats
		SET recharge_count = ?,
		    total_coin_amount = total_coin_amount + ?,
		    total_usd_minor_amount = total_usd_minor_amount + ?,
		    updated_at_ms = ?
		WHERE app_code = ? AND user_id = ?`,
		next, coinAmount, usdMinorAmount, nowMs, normalizedApp, userID,
	)
	if err != nil {
		return 0, err
	}
	return next, nil
}

func (r *Repository) insertCoinSellerStockRecord(ctx context.Context, tx *sql.Tx, transactionID string, commandID string, metadata coinSellerStockMetadata, nowMs int64) error {
	metadataJSON, err := json.Marshal(metadata)
	if err != nil {
		return err
	}
	var paymentRef any
	if metadata.PaymentRef != "" {
		paymentRef = metadata.PaymentRef
	}
	_, err = tx.ExecContext(ctx,
		`INSERT INTO coin_seller_stock_records (
		   app_code, stock_id, transaction_id, command_id, seller_user_id, stock_type,
		   counts_as_seller_recharge, coin_amount, paid_currency_code, paid_amount_micro,
		   payment_ref, evidence_ref, operator_user_id, reason, balance_after, metadata_json, created_at_ms
		 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		normalizedEntryAppCode(ctx, metadata.AppCode),
		coinSellerStockID(metadata.AppCode, commandID),
		transactionID,
		commandID,
		metadata.SellerUserID,
		metadata.StockType,
		metadata.CountsAsSellerRecharge,
		metadata.CoinAmount,
		metadata.PaidCurrencyCode,
		metadata.PaidAmountMicro,
		paymentRef,
		metadata.EvidenceRef,
		metadata.OperatorUserID,
		metadata.Reason,
		metadata.BalanceAfter,
		string(metadataJSON),
		nowMs,
	)
	if err != nil && isMySQLDuplicateError(err) && metadata.PaymentRef != "" {
		return xerr.New(xerr.CoinSellerPaymentRefDuplicated, "payment_ref is duplicated")
	}
	return err
}

func (r *Repository) coinSellerStockPaymentRefExists(ctx context.Context, tx *sql.Tx, stockType string, paymentRef string) (bool, error) {
	var stockID string
	err := tx.QueryRowContext(ctx,
		`SELECT stock_id
		 FROM coin_seller_stock_records
		 WHERE app_code = ? AND stock_type = ? AND payment_ref = ?
		 LIMIT 1`,
		appcode.FromContext(ctx),
		stockType,
		paymentRef,
	).Scan(&stockID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func coinSellerStockRequestHash(command ledger.CoinSellerStockCreditCommand) string {
	return stableHash(fmt.Sprintf("coin_seller_stock|%s|%d|%d|%d|%s|%d|%s|%d|%s|%s|%d|%s",
		appcode.Normalize(command.AppCode),
		command.SellerUserID,
		command.SellerCountryID,
		command.SellerRegionID,
		command.StockType,
		command.CoinAmount,
		command.PaidCurrencyCode,
		command.PaidAmountMicro,
		command.PaymentRef,
		command.EvidenceRef,
		command.OperatorUserID,
		command.Reason,
	))
}

func coinSellerTransferRequestHash(command ledger.CoinSellerTransferCommand) string {
	return stableHash(fmt.Sprintf("coin_seller_transfer|%s|%d|%d|%d|%d|%d|%s",
		appcode.Normalize(command.AppCode),
		command.SellerUserID,
		command.TargetUserID,
		command.SellerRegionID,
		command.TargetRegionID,
		command.Amount,
		strings.TrimSpace(command.Reason),
	))
}

func coinSellerSubTransferRequestHash(command ledger.CoinSellerSubTransferCommand) string {
	return stableHash(fmt.Sprintf("coin_seller_sub_transfer|%s|%d|%d|%d|%d|%s",
		appcode.Normalize(command.AppCode),
		command.ParentUserID,
		command.ChildUserID,
		command.RegionID,
		command.Amount,
		strings.TrimSpace(command.Reason),
	))
}

func salaryExchangeRequestHash(command ledger.SalaryExchangeCommand) string {
	return stableHash(fmt.Sprintf("salary_exchange|%s|%d|%s|%d|%s",
		appcode.Normalize(command.AppCode),
		command.UserID,
		strings.ToUpper(strings.TrimSpace(command.SalaryAssetType)),
		command.SalaryUSDMinor,
		strings.TrimSpace(command.Reason),
	))
}

func salaryTransferToCoinSellerRequestHash(command ledger.SalaryTransferToCoinSellerCommand) string {
	return stableHash(fmt.Sprintf("salary_transfer_coin_seller|%s|%d|%d|%s|%d|%d|%s",
		appcode.Normalize(command.AppCode),
		command.SourceUserID,
		command.SellerUserID,
		strings.ToUpper(strings.TrimSpace(command.SalaryAssetType)),
		command.SalaryUSDMinor,
		command.RegionID,
		strings.TrimSpace(command.Reason),
	))
}

func coinSellerStockBizType(stockType string) string {
	switch stockType {
	case ledger.StockTypeUSDTPurchase:
		return bizTypeCoinSellerStockPurchase
	case ledger.StockTypeUSDTDeduction:
		return bizTypeCoinSellerStockDeduction
	case ledger.StockTypeCoinCompensation:
		return bizTypeCoinSellerCoinCompensation
	default:
		return ""
	}
}

func coinSellerStockAvailableDelta(command ledger.CoinSellerStockCreditCommand) int64 {
	if command.StockType == ledger.StockTypeUSDTDeduction {
		return -command.CoinAmount
	}
	return command.CoinAmount
}

func coinSellerStockPaidAmountMicro(command ledger.CoinSellerStockCreditCommand) int64 {
	if command.StockType == ledger.StockTypeUSDTDeduction {
		return -command.PaidAmountMicro
	}
	return command.PaidAmountMicro
}
