package lending

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"log"
	"math/big"
	"strings"
	"sync"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	ethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	dbpkg "github.com/gothinkster/golang-gin-realworld-example-app/common"
	appcfg "github.com/gothinkster/golang-gin-realworld-example-app/config"
	"github.com/gothinkster/golang-gin-realworld-example-app/models"
)

const (
	buyerLoopDelay      = 10 * time.Second
	buyerLookbackBlocks = int64(10000)
)

const collateralBuyerABIStr = `[
  {"anonymous":false,"inputs":[
    {"indexed":true,"name":"buyer","type":"address"},
    {"indexed":true,"name":"asset","type":"address"},
    {"indexed":false,"name":"baseAmount","type":"uint256"},
    {"indexed":false,"name":"collateralAmount","type":"uint256"}
  ],"name":"BuyCollateral","type":"event"},
  {"inputs":[],"name":"getReserves","outputs":[{"name":"","type":"int256"}],"stateMutability":"view","type":"function"},
  {"inputs":[],"name":"targetReserves","outputs":[{"name":"","type":"uint104"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"name":"asset","type":"address"}],"name":"getCollateralReserves","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"name":"asset","type":"address"},{"name":"baseAmount","type":"uint256"}],"name":"quoteCollateral","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"name":"asset","type":"address"},{"name":"minAmount","type":"uint256"},{"name":"baseAmount","type":"uint256"},{"name":"recipient","type":"address"}],"name":"buyCollateral","outputs":[],"stateMutability":"nonpayable","type":"function"},
  {"inputs":[],"name":"baseToken","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"name":"","type":"uint256"}],"name":"assetList","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"}
]`

const erc20BuyerABIStr = `[
  {"inputs":[{"name":"spender","type":"address"},{"name":"amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
  {"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
  {"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"stateMutability":"view","type":"function"},
  {"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"stateMutability":"view","type":"function"}
]`

// BuyerBotStatus is returned by the status API
type BuyerBotStatus struct {
	Running          bool      `json:"running"`
	BuyerAddr        string    `json:"buyer_addr"`
	ChainID          int       `json:"chain_id"`
	ContractAddr     string    `json:"contract_addr"`
	TotalBuys        int64     `json:"total_buys"`
	LastCheckTime    time.Time `json:"last_check_time"`
	LastBlockChecked uint64    `json:"last_block_checked"`
	LoopDelayMs      int       `json:"loop_delay_ms"`
	LookbackBlocks   int64     `json:"lookback_blocks"`
	SlippagePct      int       `json:"slippage_pct"`
	Error            string    `json:"error,omitempty"`
}

type collateralBuyerBot struct {
	mu               sync.Mutex
	running          bool
	stopChan         chan struct{}
	buyerAddr        string
	chainID          int
	contractAddr     string
	totalBuys        int64
	lastCheckTime    time.Time
	lastBlockChecked uint64
	lastErr          string
	slippagePct      int
}

var globalBuyer = &collateralBuyerBot{}

func GetBuyerBotStatus() BuyerBotStatus {
	globalBuyer.mu.Lock()
	defer globalBuyer.mu.Unlock()
	return BuyerBotStatus{
		Running:          globalBuyer.running,
		BuyerAddr:        globalBuyer.buyerAddr,
		ChainID:          globalBuyer.chainID,
		ContractAddr:     globalBuyer.contractAddr,
		TotalBuys:        globalBuyer.totalBuys,
		LastCheckTime:    globalBuyer.lastCheckTime,
		LastBlockChecked: globalBuyer.lastBlockChecked,
		LoopDelayMs:      int(buyerLoopDelay.Milliseconds()),
		LookbackBlocks:   buyerLookbackBlocks,
		SlippagePct:      globalBuyer.slippagePct,
		Error:            globalBuyer.lastErr,
	}
}

func StartCollateralBuyerBot(cfg *appcfg.Config) {
	// 优先用独立私钥，未配置则 fallback 到清算机器人私钥（不修改 cfg 原始值）
	privKey := cfg.CollateralBuyerPrivateKey
	if privKey == "" && cfg.LiquidatorPrivateKey != "" {
		privKey = cfg.LiquidatorPrivateKey
		log.Println("[BuyerBot] COLLATERAL_BUYER_PRIVATE_KEY 未配置，使用 LIQUIDATOR_PRIVATE_KEY")
	}
	if privKey == "" {
		log.Println("[BuyerBot] 未配置私钥，Bot 未启动")
		globalBuyer.mu.Lock()
		globalBuyer.lastErr = "未配置私钥，Bot 未启动"
		globalBuyer.mu.Unlock()
		return
	}

	globalBuyer.mu.Lock()
	if globalBuyer.running {
		globalBuyer.mu.Unlock()
		return
	}
	globalBuyer.stopChan = make(chan struct{})
	globalBuyer.running = true
	globalBuyer.lastErr = ""
	globalBuyer.mu.Unlock()

	go runBuyerBot(cfg, privKey)
	log.Println("[BuyerBot] Started")
}

func StopCollateralBuyerBot() {
	globalBuyer.mu.Lock()
	defer globalBuyer.mu.Unlock()
	if !globalBuyer.running {
		return
	}
	close(globalBuyer.stopChan)
	globalBuyer.running = false
	log.Println("[BuyerBot] Stop signal sent")
}

func runBuyerBot(cfg *appcfg.Config, privKeyStr string) {
	defer func() {
		globalBuyer.mu.Lock()
		globalBuyer.running = false
		globalBuyer.mu.Unlock()
		log.Println("[BuyerBot] Goroutine exited")
	}()

	privKeyHex := strings.TrimPrefix(privKeyStr, "0x")
	privateKey, err := crypto.HexToECDSA(privKeyHex)
	if err != nil {
		log.Printf("[BuyerBot] Invalid private key: %v", err)
		globalBuyer.mu.Lock()
		globalBuyer.lastErr = fmt.Sprintf("invalid private key: %v", err)
		globalBuyer.running = false
		globalBuyer.mu.Unlock()
		return
	}
	buyerAddr := crypto.PubkeyToAddress(privateKey.PublicKey)

	lendingAddr, chainID, loadErr := loadBotContracts()
	if loadErr != nil {
		log.Printf("[BuyerBot] Load contracts: %v", loadErr)
		globalBuyer.mu.Lock()
		globalBuyer.lastErr = loadErr.Error()
		globalBuyer.running = false
		globalBuyer.mu.Unlock()
		return
	}

	rpcURL := getRPCURL(chainID)
	if rpcURL == "" {
		log.Printf("[BuyerBot] No RPC URL for chain %d", chainID)
		globalBuyer.mu.Lock()
		globalBuyer.lastErr = fmt.Sprintf("no RPC URL for chain %d", chainID)
		globalBuyer.running = false
		globalBuyer.mu.Unlock()
		return
	}

	slippagePct := cfg.CollateralBuyerSlippage
	if slippagePct <= 0 || slippagePct > 10 {
		slippagePct = 1
	}

	globalBuyer.mu.Lock()
	globalBuyer.buyerAddr = buyerAddr.Hex()
	globalBuyer.chainID = chainID
	globalBuyer.contractAddr = lendingAddr
	globalBuyer.slippagePct = slippagePct
	globalBuyer.mu.Unlock()

	log.Printf("[BuyerBot] Buyer: %s | Chain: %d | Lending: %s | Slippage: %d%%",
		buyerAddr.Hex(), chainID, lendingAddr, slippagePct)

	lABI, err := abi.JSON(strings.NewReader(collateralBuyerABIStr))
	if err != nil {
		log.Printf("[BuyerBot] ABI parse error: %v", err)
		globalBuyer.mu.Lock()
		globalBuyer.lastErr = fmt.Sprintf("ABI parse error: %v", err)
		globalBuyer.running = false
		globalBuyer.mu.Unlock()
		return
	}
	processedBlock := loadBuyerBlock(chainID)

	for {
		select {
		case <-globalBuyer.stopChan:
			return
		default:
		}

		caughtUp := buyerTick(rpcURL, lABI, privateKey, buyerAddr,
			lendingAddr, chainID, slippagePct, &processedBlock)

		if caughtUp {
			select {
			case <-globalBuyer.stopChan:
				return
			case <-time.After(buyerLoopDelay):
			}
		}
	}
}

func buyerTick(
	rpcURL string,
	lABI abi.ABI,
	privateKey *ecdsa.PrivateKey,
	buyerAddr ethcommon.Address,
	lendingAddr string,
	chainID int,
	slippagePct int,
	processedBlock *uint64,
) bool {
	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
	defer cancel()

	client, err := ethclient.DialContext(ctx, rpcURL)
	if err != nil {
		log.Printf("[BuyerBot] RPC dial error: %v", err)
		return true
	}
	defer client.Close()

	currentBlock, err := client.BlockNumber(ctx)
	if err != nil {
		log.Printf("[BuyerBot] BlockNumber error: %v", err)
		return true
	}

	if *processedBlock == 0 {
		if currentBlock >= uint64(buyerLookbackBlocks) {
			*processedBlock = currentBlock - uint64(buyerLookbackBlocks)
		}
	}

	fromBlock := *processedBlock + 1
	toBlock := fromBlock + uint64(buyerLookbackBlocks) - 1
	if toBlock > currentBlock {
		toBlock = currentBlock
	}

	globalBuyer.mu.Lock()
	globalBuyer.lastCheckTime = time.Now()
	globalBuyer.lastBlockChecked = toBlock
	globalBuyer.mu.Unlock()

	lendingContract := ethcommon.HexToAddress(lendingAddr)

	// ── 1. 检查国库条件（轮询核心） ──────────────────────────────────────
	reserves, targReserves, condErr := checkBuyCondition(ctx, client, lABI, lendingContract)
	if condErr != nil {
		log.Printf("[BuyerBot] 条件检查失败: %v", condErr)
		saveBuyerBlock(chainID, toBlock)
		*processedBlock = toBlock
		return toBlock >= currentBlock
	}

	if reserves.Cmp(targReserves) >= 0 {
		// 国库充足，静默推进
		saveBuyerBlock(chainID, toBlock)
		*processedBlock = toBlock
		return toBlock >= currentBlock
	}

	log.Printf("[BuyerBot] 国库不足: reserves=%s < targetReserves=%s，开始购买",
		reserves.String(), targReserves.String())

	saveBuyerBlock(chainID, toBlock)
	*processedBlock = toBlock

	// ── 2. 获取 baseToken 及精度 ─────────────────────────────────────────
	eABI, eABIErr := abi.JSON(strings.NewReader(erc20BuyerABIStr))
	if eABIErr != nil {
		log.Printf("[BuyerBot] ERC20 ABI parse error: %v", eABIErr)
		return toBlock >= currentBlock
	}
	baseTokenAddr, btErr := callViewAddress(ctx, client, lABI, lendingContract, "baseToken")
	if btErr != nil {
		log.Printf("[BuyerBot] baseToken error: %v", btErr)
		return toBlock >= currentBlock
	}

	// ── 3. 扫描所有已配置资产，过滤出储备 > 0 的 ────────────────────────
	type assetInfo struct {
		addr    ethcommon.Address
		reserve *big.Int
		symbol  string
	}
	var assetsWithReserves []assetInfo

	allAssets := getAllAssets(ctx, client, lABI, lendingContract)
	for addr := range allAssets {
		collRes, err := callViewRaw(ctx, client, lABI, lendingContract, "getCollateralReserves", addr)
		if err != nil {
			continue
		}
		reserve, ok := collRes[0].(*big.Int)
		if !ok || reserve.Sign() == 0 {
			continue
		}
		sym := getAssetSymbol(ctx, client, eABI, addr)
		assetsWithReserves = append(assetsWithReserves, assetInfo{addr: addr, reserve: new(big.Int).Set(reserve), symbol: sym})
		log.Printf("[BuyerBot] 可购买资产: %s 储备=%s", sym, reserve.String())
	}

	if len(assetsWithReserves) == 0 {
		log.Println("[BuyerBot] 无可购买资产（储备均为零）")
		return toBlock >= currentBlock
	}

	// ── 4. 一次性授权检查（allowance < MaxUint256/2 才 approve）──────────
	maxUint256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
	halfMax := new(big.Int).Rsh(maxUint256, 1)

	allowance, alwErr := callERC20BigInt(ctx, client, eABI, baseTokenAddr, "allowance", buyerAddr, lendingContract)
	if alwErr != nil {
		log.Printf("[BuyerBot] allowance error: %v", alwErr)
		return toBlock >= currentBlock
	}
	if allowance.Cmp(halfMax) < 0 {
		log.Printf("[BuyerBot] 授权不足，执行 approve(MaxUint256)...")
		chainIDBig := big.NewInt(int64(chainID))
		auth, authErr := bind.NewKeyedTransactorWithChainID(privateKey, chainIDBig)
		if authErr != nil {
			log.Printf("[BuyerBot] create transactor: %v", authErr)
			return toBlock >= currentBlock
		}
		auth.GasLimit = 100000
		erc20Bound := bind.NewBoundContract(baseTokenAddr, eABI, client, client, client)
		approveTx, appErr := erc20Bound.Transact(auth, "approve", lendingContract, maxUint256)
		if appErr != nil {
			log.Printf("[BuyerBot] approve error: %v", appErr)
			return toBlock >= currentBlock
		}
		approveCtx, approveCancel := context.WithTimeout(context.Background(), 60*time.Second)
		_, waitErr := bind.WaitMined(approveCtx, client, approveTx)
		approveCancel()
		if waitErr != nil {
			log.Printf("[BuyerBot] approve wait: %v", waitErr)
			return toBlock >= currentBlock
		}
		log.Printf("[BuyerBot] 授权成功: %s", approveTx.Hash().Hex())
	} else {
		log.Println("[BuyerBot] 授权充足，跳过 approve")
	}

	// ── 5. 逐资产购买 ───────────────────────────────────────────────────
	chainIDBig := big.NewInt(int64(chainID))
	successCount := 0

	for _, asset := range assetsWithReserves {
		// 每次购买前重新读取余额
		freshBalance, balErr := callERC20BigInt(ctx, client, eABI, baseTokenAddr, "balanceOf", buyerAddr)
		if balErr != nil {
			log.Printf("[BuyerBot] balanceOf error: %v", balErr)
			break
		}
		if freshBalance.Sign() == 0 {
			log.Println("[BuyerBot] 买家余额耗尽，停止购买")
			break
		}

		// minAmount = reserve * (100 - slippagePct) / 100
		minAmount := new(big.Int).Mul(asset.reserve, big.NewInt(int64(100-slippagePct)))
		minAmount.Div(minAmount, big.NewInt(100))

		log.Printf("[BuyerBot] 购买 %s: balance=%s reserve=%s minAmount=%s",
			asset.symbol, freshBalance.String(), asset.reserve.String(), minAmount.String())

		actualPaid, actualReceived, txHash, gasUsed, blockNum, buyErr := executeBuy(
			ctx, client, lABI, privateKey, buyerAddr,
			lendingContract, asset.addr, minAmount, freshBalance,
			chainIDBig,
		)
		if buyErr != nil {
			log.Printf("[BuyerBot] 购买 %s 失败（跳过）: %v", asset.symbol, buyErr)
			// 失败时用时间戳生成唯一 key，避免 uniqueIndex 冲突
			failKey := fmt.Sprintf("FAILED_%s_%d", asset.addr.Hex()[:10], time.Now().UnixNano())
			saveBuyRecord(chainID, failKey, buyerAddr, asset.addr, asset.symbol,
				freshBalance, big.NewInt(0), 0, 0, "failed", buyErr.Error())
			continue // 单资产失败不影响其他资产
		}

		log.Printf("[BuyerBot] %s 购买成功: 支付=%s 获得=%s tx=%s",
			asset.symbol, actualPaid.String(), actualReceived.String(), txHash)
		saveBuyRecord(chainID, txHash, buyerAddr, asset.addr, asset.symbol,
			actualPaid, actualReceived, gasUsed, blockNum, "success", "")
		globalBuyer.mu.Lock()
		globalBuyer.totalBuys++
		globalBuyer.mu.Unlock()
		successCount++
	}

	log.Printf("[BuyerBot] 本轮完成: 成功 %d / %d 个资产", successCount, len(assetsWithReserves))
	return toBlock >= currentBlock
}

// executeBuy 执行一次 buyCollateral 并从事件解析实际支付/获得量
func executeBuy(
	ctx context.Context,
	client *ethclient.Client,
	lABI abi.ABI,
	privateKey *ecdsa.PrivateKey,
	buyerAddr ethcommon.Address,
	lendingContract ethcommon.Address,
	asset ethcommon.Address,
	minAmount *big.Int,
	baseAmount *big.Int, // 买家全部余额作为上限
	chainIDBig *big.Int,
) (actualPaid *big.Int, actualReceived *big.Int, txHash string, gasUsed, blockNum uint64, err error) {
	auth, authErr := bind.NewKeyedTransactorWithChainID(privateKey, chainIDBig)
	if authErr != nil {
		return nil, nil, "", 0, 0, fmt.Errorf("create transactor: %w", authErr)
	}
	auth.GasLimit = 300000

	lendingBound := bind.NewBoundContract(lendingContract, lABI, client, client, client)
	tx, txErr := lendingBound.Transact(auth, "buyCollateral", asset, minAmount, baseAmount, buyerAddr)
	if txErr != nil {
		return nil, nil, "", 0, 0, fmt.Errorf("buyCollateral: %w", txErr)
	}

	txHash = tx.Hash().Hex()
	log.Printf("[BuyerBot] 交易已提交: %s", txHash)

	receiptCtx, receiptCancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer receiptCancel()
	receipt, waitErr := bind.WaitMined(receiptCtx, client, tx)
	if waitErr != nil {
		return nil, nil, txHash, 0, 0, fmt.Errorf("wait mined: %w", waitErr)
	}

	gasUsed = receipt.GasUsed
	blockNum = receipt.BlockNumber.Uint64()

	if receipt.Status != 1 {
		return nil, nil, txHash, gasUsed, blockNum,
			fmt.Errorf("交易回滚 block %d, tx %s", blockNum, txHash)
	}

	log.Printf("[BuyerBot] 交易确认 Gas=%d Block=%d", gasUsed, blockNum)

	// 从 BuyCollateral 事件解析实际数值
	eventID := lABI.Events["BuyCollateral"].ID
	for _, l := range receipt.Logs {
		if len(l.Topics) < 1 || l.Topics[0] != eventID {
			continue
		}
		decoded, decErr := lABI.Unpack("BuyCollateral", l.Data)
		if decErr != nil || len(decoded) < 2 {
			continue
		}
		paid, ok1 := decoded[0].(*big.Int)
		received, ok2 := decoded[1].(*big.Int)
		if ok1 && ok2 {
			return paid, received, txHash, gasUsed, blockNum, nil
		}
	}

	// BuyCollateral 事件未找到，用余额差估算（实际数量需链上核实）
	log.Printf("[BuyerBot] 警告: BuyCollateral 事件解析失败 tx=%s，receivedAmount 记录为 0", txHash)
	return baseAmount, big.NewInt(0), txHash, gasUsed, blockNum, nil
}

// ── 合约枚举 / 条件检查 helpers ──────────────────────────────────────────────

// getAllAssets 遍历 assetList(0,1,...) 直到 revert
func getAllAssets(ctx context.Context, client *ethclient.Client, lABI abi.ABI,
	contract ethcommon.Address) map[ethcommon.Address]struct{} {
	result := make(map[ethcommon.Address]struct{})
	zero := ethcommon.Address{}
	for i := 0; i < 50; i++ {
		res, err := callViewRaw(ctx, client, lABI, contract, "assetList", big.NewInt(int64(i)))
		if err != nil {
			break
		}
		addr, ok := res[0].(ethcommon.Address)
		if !ok || addr == zero {
			break
		}
		result[addr] = struct{}{}
	}
	log.Printf("[BuyerBot] assetList: %d 个资产", len(result))
	return result
}

// checkBuyCondition 返回 (reserves int256, targetReserves, error)
func checkBuyCondition(ctx context.Context, client *ethclient.Client, lABI abi.ABI,
	addr ethcommon.Address) (*big.Int, *big.Int, error) {
	res, err := callViewRaw(ctx, client, lABI, addr, "getReserves")
	if err != nil {
		return nil, nil, fmt.Errorf("getReserves: %w", err)
	}
	reserves, ok := res[0].(*big.Int)
	if !ok {
		return nil, nil, fmt.Errorf("getReserves type %T", res[0])
	}
	res2, err := callViewRaw(ctx, client, lABI, addr, "targetReserves")
	if err != nil {
		return nil, nil, fmt.Errorf("targetReserves: %w", err)
	}
	targReserves, ok2 := res2[0].(*big.Int)
	if !ok2 {
		return nil, nil, fmt.Errorf("targetReserves type %T", res2[0])
	}
	return reserves, targReserves, nil
}

// ── ABI call helpers ──────────────────────────────────────────────────────────

func callViewAddress(ctx context.Context, client *ethclient.Client, contractABI abi.ABI,
	addr ethcommon.Address, method string, args ...interface{}) (ethcommon.Address, error) {
	res, err := callViewRaw(ctx, client, contractABI, addr, method, args...)
	if err != nil {
		return ethcommon.Address{}, err
	}
	v, ok := res[0].(ethcommon.Address)
	if !ok {
		return ethcommon.Address{}, fmt.Errorf("%s type %T", method, res[0])
	}
	return v, nil
}

func callERC20BigInt(ctx context.Context, client *ethclient.Client, eABI abi.ABI,
	addr ethcommon.Address, method string, args ...interface{}) (*big.Int, error) {
	data, err := eABI.Pack(method, args...)
	if err != nil {
		return nil, fmt.Errorf("pack %s: %w", method, err)
	}
	result, err := client.CallContract(ctx, ethereum.CallMsg{To: &addr, Data: data}, nil)
	if err != nil {
		return nil, fmt.Errorf("call %s: %w", method, err)
	}
	decoded, err := eABI.Unpack(method, result)
	if err != nil {
		return nil, fmt.Errorf("unpack %s: %w", method, err)
	}
	v, ok := decoded[0].(*big.Int)
	if !ok {
		return nil, fmt.Errorf("%s type %T", method, decoded[0])
	}
	return v, nil
}

func getAssetSymbol(ctx context.Context, client *ethclient.Client, eABI abi.ABI, asset ethcommon.Address) string {
	data, _ := eABI.Pack("symbol")
	result, err := client.CallContract(ctx, ethereum.CallMsg{To: &asset, Data: data}, nil)
	if err != nil {
		return asset.Hex()[:10]
	}
	decoded, err := eABI.Unpack("symbol", result)
	if err != nil || len(decoded) == 0 {
		return asset.Hex()[:10]
	}
	s, ok := decoded[0].(string)
	if !ok {
		return asset.Hex()[:10]
	}
	return s
}

// ── DB helpers ────────────────────────────────────────────────────────────────

func loadBuyerBlock(chainID int) uint64 {
	db := dbpkg.GetDB()
	var state models.ScannerState
	if err := db.Where("chain_id = ? AND scanner_type = ?", chainID, "collateral_buyer").First(&state).Error; err != nil {
		return 0
	}
	return state.LastScannedBlock
}

func saveBuyerBlock(chainID int, block uint64) {
	db := dbpkg.GetDB()
	state := models.ScannerState{
		ScannerType:      "collateral_buyer",
		ChainID:          chainID,
		LastScannedBlock: block,
	}
	db.Where("chain_id = ? AND scanner_type = ?", chainID, "collateral_buyer").Assign(state).FirstOrCreate(&state)
	db.Model(&state).Updates(map[string]interface{}{
		"last_scanned_block": block,
		"updated_at":         time.Now(),
	})
}

func saveBuyRecord(chainID int, txHash string, buyerAddr, assetAddr ethcommon.Address,
	assetSymbol string, paidAmount, receivedAmount *big.Int, gasUsed, blockNum uint64, status, errMsg string) {
	record := models.CollateralBuyRecord{
		ChainID:        chainID,
		TxHash:         txHash,
		BuyerAddr:      buyerAddr.Hex(),
		AssetAddr:      assetAddr.Hex(),
		AssetSymbol:    assetSymbol,
		PaidAmount:     paidAmount.String(),
		ReceivedAmount: receivedAmount.String(),
		GasUsed:        gasUsed,
		BlockNumber:    blockNum,
		Status:         status,
		ErrorMessage:   errMsg,
		CreatedAt:      time.Now(),
	}
	db := dbpkg.GetDB()
	if dbErr := db.Create(&record).Error; dbErr != nil {
		log.Printf("[BuyerBot] Save buy record error: %v", dbErr)
	}
}
