package lending

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

	"gorm.io/gorm/clause"

	"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 (
	botLoopDelay      = 5 * time.Second
	botLookbackBlocks = int64(10000)
)

// minimal ABI used by the liquidation bot
const lendingBotABI = `[
  {"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"isLiquidatable","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"internalType":"address","name":"absorber","type":"address"},{"internalType":"address[]","name":"accounts","type":"address[]"}],"name":"absorbMultiple","outputs":[],"stateMutability":"nonpayable","type":"function"},
  {"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"src","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},
  {"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"dst","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Supply","type":"event"},
  {"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"dst","type":"address"},{"indexed":true,"internalType":"address","name":"asset","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"SupplyCollateral","type":"event"},
  {"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"src","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"asset","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"WithdrawCollateral","type":"event"}
]`

// BotStatus is returned by the status API
type BotStatus struct {
	Running           bool      `json:"running"`
	LiquidatorAddr    string    `json:"liquidator_addr"`
	ChainID           int       `json:"chain_id"`
	ContractAddr      string    `json:"contract_addr"`
	TotalLiquidations int64     `json:"total_liquidations"`
	LastCheckTime     time.Time `json:"last_check_time"`
	LastBlockChecked  uint64    `json:"last_block_checked"`
	LoopDelayMs       int       `json:"loop_delay_ms"`
	LookbackBlocks    int64     `json:"lookback_blocks"`
	Error             string    `json:"error,omitempty"`
}

// liquidationBot holds mutable bot state
type liquidationBot struct {
	mu                sync.Mutex
	running           bool
	stopChan          chan struct{}
	liquidatorAddr    string
	chainID           int
	contractAddr      string
	totalLiquidations int64
	lastCheckTime     time.Time
	lastBlockChecked  uint64
	lastErr           string
}

var globalBot = &liquidationBot{}

// GetBotStatus returns a snapshot of the current bot state
func GetBotStatus() BotStatus {
	globalBot.mu.Lock()
	defer globalBot.mu.Unlock()
	return BotStatus{
		Running:           globalBot.running,
		LiquidatorAddr:    globalBot.liquidatorAddr,
		ChainID:           globalBot.chainID,
		ContractAddr:      globalBot.contractAddr,
		TotalLiquidations: globalBot.totalLiquidations,
		LastCheckTime:     globalBot.lastCheckTime,
		LastBlockChecked:  globalBot.lastBlockChecked,
		LoopDelayMs:       int(botLoopDelay.Milliseconds()),
		LookbackBlocks:    botLookbackBlocks,
		Error:             globalBot.lastErr,
	}
}

// StartLiquidationBot starts the bot goroutine; idempotent if already running
func StartLiquidationBot(cfg *appcfg.Config) {
	if cfg.LiquidatorPrivateKey == "" {
		log.Println("[LiquidationBot] LIQUIDATOR_PRIVATE_KEY not set, bot disabled")
		globalBot.mu.Lock()
		globalBot.lastErr = "LIQUIDATOR_PRIVATE_KEY not configured"
		globalBot.mu.Unlock()
		return
	}

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

	go runBot(cfg)
	log.Println("[LiquidationBot] Started")
}

// StopLiquidationBot signals the bot to stop
func StopLiquidationBot() {
	globalBot.mu.Lock()
	defer globalBot.mu.Unlock()
	if !globalBot.running {
		return
	}
	close(globalBot.stopChan)
	globalBot.running = false
	log.Println("[LiquidationBot] Stop signal sent")
}

func runBot(cfg *appcfg.Config) {
	defer func() {
		globalBot.mu.Lock()
		globalBot.running = false
		globalBot.mu.Unlock()
		log.Println("[LiquidationBot] Goroutine exited")
	}()

	// Parse private key
	privKeyHex := strings.TrimPrefix(cfg.LiquidatorPrivateKey, "0x")
	privateKey, err := crypto.HexToECDSA(privKeyHex)
	if err != nil {
		log.Printf("[LiquidationBot] Invalid private key: %v", err)
		globalBot.mu.Lock()
		globalBot.lastErr = fmt.Sprintf("invalid private key: %v", err)
		globalBot.running = false
		globalBot.mu.Unlock()
		return
	}
	liquidatorAddr := crypto.PubkeyToAddress(privateKey.PublicKey)

	// Load contract addresses from DB
	lendingAddr, chainID, loadErr := loadBotContracts()
	if loadErr != nil {
		log.Printf("[LiquidationBot] Load contracts: %v", loadErr)
		globalBot.mu.Lock()
		globalBot.lastErr = loadErr.Error()
		globalBot.running = false
		globalBot.mu.Unlock()
		return
	}

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

	globalBot.mu.Lock()
	globalBot.liquidatorAddr = liquidatorAddr.Hex()
	globalBot.chainID = chainID
	globalBot.contractAddr = lendingAddr
	globalBot.mu.Unlock()

	log.Printf("[LiquidationBot] Liquidator: %s | Chain: %d | Lending: %s",
		liquidatorAddr.Hex(), chainID, lendingAddr)

	// Parse ABI
	lABI, err := abi.JSON(strings.NewReader(lendingBotABI))
	if err != nil {
		log.Printf("[LiquidationBot] ABI parse error: %v", err)
		globalBot.mu.Lock()
		globalBot.lastErr = fmt.Sprintf("ABI parse error: %v", err)
		globalBot.running = false
		globalBot.mu.Unlock()
		return
	}

	// Load last processed block from DB; 0 means first run
	processedBlock := loadLiquidationBlock(chainID)

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

		caughtUp := botTick(rpcURL, lABI, privateKey, liquidatorAddr, lendingAddr, chainID, &processedBlock)

		if caughtUp {
			// Normal pace
			select {
			case <-globalBot.stopChan:
				return
			case <-time.After(botLoopDelay):
			}
		}
		// Not caught up: loop immediately to process next chunk
	}
}

// botTick processes one chunk of blocks. Returns true when caught up to chain head.
func botTick(
	rpcURL string,
	lABI abi.ABI,
	privateKey *ecdsa.PrivateKey,
	liquidatorAddr ethcommon.Address,
	lendingAddr string,
	chainID int,
	processedBlock *uint64,
) bool {
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()

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

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

	// On first run (processedBlock == 0), start from current - lookback
	if *processedBlock == 0 {
		if currentBlock >= uint64(botLookbackBlocks) {
			*processedBlock = currentBlock - uint64(botLookbackBlocks)
		}
	}

	if currentBlock <= *processedBlock {
		return true // already up to date
	}

	// Scan at most botLookbackBlocks blocks per tick
	fromBlock := *processedBlock + 1
	toBlock := fromBlock + uint64(botLookbackBlocks) - 1
	if toBlock > currentBlock {
		toBlock = currentBlock
	}

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

	lendingContract := ethcommon.HexToAddress(lendingAddr)

	// ── 1. 扫描事件，将新地址存入 known_borrowers（永久积累）──────────────
	log.Printf("[LiquidationBot] Scanning %d~%d (chain head: %d)", fromBlock, toBlock, currentBlock)
	newAddrs := queryUniqueAddresses(ctx, client, lABI, lendingContract, fromBlock, toBlock)
	if len(newAddrs) > 0 {
		saveKnownBorrowers(chainID, newAddrs)
	}

	// ── 2. 对所有历史地址检查 isLiquidatable（含价格变动导致健康因子归零的账户）
	allBorrowers := loadKnownBorrowers(chainID)
	liquidated, txHash, gasUsed, blockNum, execErr := checkAndAbsorb(
		ctx, client, lABI, privateKey, liquidatorAddr, lendingContract, chainID, allBorrowers,
	)

	// Always save progress, even if no liquidation happened
	saveLiquidationBlock(chainID, toBlock)
	*processedBlock = toBlock

	if execErr != nil {
		log.Printf("[LiquidationBot] Execution error: %v", execErr)
		if len(liquidated) > 0 {
			saveLiquidationRecord(chainID, txHash, liquidatorAddr, liquidated, gasUsed, blockNum, "failed", execErr.Error())
		}
		return toBlock >= currentBlock
	}

	if len(liquidated) > 0 {
		saveLiquidationRecord(chainID, txHash, liquidatorAddr, liquidated, gasUsed, blockNum, "success", "")
		globalBot.mu.Lock()
		globalBot.totalLiquidations += int64(len(liquidated))
		globalBot.mu.Unlock()
	}

	return toBlock >= currentBlock
}

const isLiquidatableMaxConcurrent = 10 // 并行检查上限，作为 Multicall3 失败时的兜底
const multicallBatchSize = 500         // 每批 Multicall3 最多打包的地址数

// Multicall3 已部署在所有主流 EVM 链的固定地址
var multicall3Addr = ethcommon.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11")

const multicall3ABIStr = `[{
  "inputs": [{"components": [
    {"internalType":"address","name":"target","type":"address"},
    {"internalType":"bool","name":"allowFailure","type":"bool"},
    {"internalType":"bytes","name":"callData","type":"bytes"}
  ],"name":"calls","type":"tuple[]"}],
  "name":"aggregate3",
  "outputs": [{"components": [
    {"internalType":"bool","name":"success","type":"bool"},
    {"internalType":"bytes","name":"returnData","type":"bytes"}
  ],"name":"returnData","type":"tuple[]"}],
  "stateMutability":"payable","type":"function"
}]`

// mc3Call 是 Multicall3.aggregate3 的输入结构体（字段名需与 ABI 组件名 lowercase 匹配）
type mc3Call struct {
	Target       ethcommon.Address
	AllowFailure bool
	CallData     []byte
}

// multicallIsLiquidatable 用 Multicall3 批量检查，每批 500 个地址只发 1 个 RPC
// 失败时自动降级到并发 goroutine 方案
func multicallIsLiquidatable(
	ctx context.Context,
	client *ethclient.Client,
	lABI abi.ABI,
	lendingContract ethcommon.Address,
	addresses []ethcommon.Address,
) []ethcommon.Address {
	m3ABI, err := abi.JSON(strings.NewReader(multicall3ABIStr))
	if err != nil {
		log.Printf("[LiquidationBot] multicall3 ABI parse error: %v，降级到并发模式", err)
		return parallelIsLiquidatable(ctx, client, lABI, lendingContract, addresses)
	}

	var liquidatable []ethcommon.Address
	totalBatches := (len(addresses) + multicallBatchSize - 1) / multicallBatchSize

	for i := 0; i < len(addresses); i += multicallBatchSize {
		end := i + multicallBatchSize
		if end > len(addresses) {
			end = len(addresses)
		}
		batch := addresses[i:end]
		batchIdx := i/multicallBatchSize + 1

		// 构造调用列表
		calls := make([]mc3Call, len(batch))
		for j, addr := range batch {
			data, packErr := lABI.Pack("isLiquidatable", addr)
			if packErr != nil {
				continue
			}
			calls[j] = mc3Call{Target: lendingContract, AllowFailure: true, CallData: data}
		}

		packed, packErr := m3ABI.Pack("aggregate3", calls)
		if packErr != nil {
			log.Printf("[LiquidationBot] multicall3 pack error batch %d/%d: %v", batchIdx, totalBatches, packErr)
			continue
		}

		mc3 := multicall3Addr
		raw, callErr := client.CallContract(ctx, ethereum.CallMsg{To: &mc3, Data: packed}, nil)
		if callErr != nil {
			log.Printf("[LiquidationBot] multicall3 rpc error batch %d/%d: %v", batchIdx, totalBatches, callErr)
			continue
		}

		unpacked, unpackErr := m3ABI.Unpack("aggregate3", raw)
		if unpackErr != nil || len(unpacked) == 0 {
			log.Printf("[LiquidationBot] multicall3 unpack error batch %d/%d: %v", batchIdx, totalBatches, unpackErr)
			continue
		}

		// go-ethereum 返回动态 struct 类型，通过反射访问字段
		rv := reflect.ValueOf(unpacked[0])
		for j := 0; j < rv.Len() && j < len(batch); j++ {
			elem := rv.Index(j)
			if !elem.FieldByName("Success").Bool() {
				continue
			}
			retData := elem.FieldByName("ReturnData").Bytes()
			// isLiquidatable 返回 bool，ABI 编码为 32 字节，最后一字节 1=true
			if len(retData) >= 32 && retData[31] == 1 {
				log.Printf("[LiquidationBot]  Liquidatable: %s", batch[j].Hex())
				liquidatable = append(liquidatable, batch[j])
			}
		}
		log.Printf("[LiquidationBot] Multicall3 batch %d/%d (%d addresses)", batchIdx, totalBatches, len(batch))
	}
	return liquidatable
}

// parallelIsLiquidatable 是 Multicall3 不可用时的兜底方案
func parallelIsLiquidatable(
	ctx context.Context,
	client *ethclient.Client,
	lABI abi.ABI,
	lendingContract ethcommon.Address,
	addresses []ethcommon.Address,
) []ethcommon.Address {
	type result struct {
		addr  ethcommon.Address
		isLiq bool
	}
	resultCh := make(chan result, len(addresses))
	sem := make(chan struct{}, isLiquidatableMaxConcurrent)
	var wg sync.WaitGroup
	for _, addr := range addresses {
		addr := addr
		wg.Add(1)
		go func() {
			defer wg.Done()
			sem <- struct{}{}
			defer func() { <-sem }()
			isLiq, _ := callViewBool(ctx, client, lABI, lendingContract, "isLiquidatable", addr)
			resultCh <- result{addr, isLiq}
		}()
	}
	wg.Wait()
	close(resultCh)
	var liquidatable []ethcommon.Address
	for r := range resultCh {
		if r.isLiq {
			log.Printf("[LiquidationBot]  Liquidatable: %s", r.addr.Hex())
			liquidatable = append(liquidatable, r.addr)
		}
	}
	return liquidatable
}

// checkAndAbsorb 检查所有已知地址，对可清算账户执行 absorbMultiple
func checkAndAbsorb(
	ctx context.Context,
	client *ethclient.Client,
	lABI abi.ABI,
	privateKey *ecdsa.PrivateKey,
	liquidatorAddr ethcommon.Address,
	lendingContract ethcommon.Address,
	chainID int,
	addresses []ethcommon.Address,
) (liquidated []ethcommon.Address, txHash string, gasUsed uint64, blockNum uint64, err error) {

	if len(addresses) == 0 {
		return
	}

	log.Printf("[LiquidationBot] Checking %d known borrowers via Multicall3...", len(addresses))
	toAbsorb := multicallIsLiquidatable(ctx, client, lABI, lendingContract, addresses)

	if len(toAbsorb) == 0 {
		log.Println("[LiquidationBot] No liquidatable accounts")
		return
	}

	log.Printf("[LiquidationBot] Liquidating %d accounts...", len(toAbsorb))

	chainIDBig := big.NewInt(int64(chainID))
	boundContract := bind.NewBoundContract(lendingContract, lABI, client, client, client)

	auth, authErr := bind.NewKeyedTransactorWithChainID(privateKey, chainIDBig)
	if authErr != nil {
		err = fmt.Errorf("create transactor: %w", authErr)
		return
	}
	auth.GasLimit = uint64(300000 + 150000*uint64(len(toAbsorb)))

	tx, txErr := boundContract.Transact(auth, "absorbMultiple", liquidatorAddr, toAbsorb)
	if txErr != nil {
		err = fmt.Errorf("absorbMultiple: %w", txErr)
		return
	}

	log.Printf("[LiquidationBot] Tx sent: %s", tx.Hash().Hex())
	txHash = tx.Hash().Hex()
	liquidated = toAbsorb

	receiptCtx, receiptCancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer receiptCancel()

	receipt, waitErr := bind.WaitMined(receiptCtx, client, tx)
	if waitErr != nil {
		log.Printf("[LiquidationBot] Wait receipt error: %v", waitErr)
		return
	}

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

	if receipt.Status == 1 {
		log.Printf("[LiquidationBot] Success! Gas: %d | Block: %d", gasUsed, blockNum)
	} else {
		err = fmt.Errorf("transaction reverted at block %d", blockNum)
		log.Printf("[LiquidationBot] Tx reverted: %s", txHash)
	}
	return
}

// queryUniqueAddresses queries 4 event types and returns deduplicated addresses
func queryUniqueAddresses(ctx context.Context, client *ethclient.Client, lABI abi.ABI, contractAddr ethcommon.Address, fromBlock, toBlock uint64) []ethcommon.Address {
	query := ethereum.FilterQuery{
		FromBlock: new(big.Int).SetUint64(fromBlock),
		ToBlock:   new(big.Int).SetUint64(toBlock),
		Addresses: []ethcommon.Address{contractAddr},
		Topics: [][]ethcommon.Hash{{
			lABI.Events["Withdraw"].ID,
			lABI.Events["Supply"].ID,
			lABI.Events["SupplyCollateral"].ID,
			lABI.Events["WithdrawCollateral"].ID,
		}},
	}

	logs, err := client.FilterLogs(ctx, query)
	if err != nil {
		log.Printf("[LiquidationBot] FilterLogs error: %v", err)
		return nil
	}

	seen := make(map[ethcommon.Address]struct{})
	for _, l := range logs {
		// topic[1] = first indexed addr, topic[2] = second indexed addr
		if len(l.Topics) >= 2 {
			seen[ethcommon.BytesToAddress(l.Topics[1].Bytes())] = struct{}{}
		}
		if len(l.Topics) >= 3 {
			seen[ethcommon.BytesToAddress(l.Topics[2].Bytes())] = struct{}{}
		}
	}

	addrs := make([]ethcommon.Address, 0, len(seen))
	for addr := range seen {
		addrs = append(addrs, addr)
	}
	log.Printf("[LiquidationBot] Events [%d~%d]: %d logs → %d unique addresses",
		fromBlock, toBlock, len(logs), len(addrs))
	return addrs
}

// callViewBool calls a view function and returns bool result
func callViewBool(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, addr ethcommon.Address, method string, args ...interface{}) (bool, error) {
	res, err := callViewRaw(ctx, client, contractABI, addr, method, args...)
	if err != nil {
		return false, err
	}
	if len(res) == 0 {
		return false, fmt.Errorf("empty result")
	}
	v, ok := res[0].(bool)
	if !ok {
		return false, fmt.Errorf("unexpected type %T", res[0])
	}
	return v, nil
}

// callViewRaw calls a view function and returns decoded values
func callViewRaw(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, addr ethcommon.Address, method string, args ...interface{}) ([]interface{}, error) {
	data, err := contractABI.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 := contractABI.Unpack(method, result)
	if err != nil {
		return nil, fmt.Errorf("unpack %s: %w", method, err)
	}
	return decoded, nil
}

// saveKnownBorrowers 批量 upsert：新地址插入，已存在的只更新 last_seen_at
// 使用单条 SQL 替代 N+1 查询，几千地址也只需 ceil(N/200) 次 DB 请求
func saveKnownBorrowers(chainID int, addrs []ethcommon.Address) {
	if len(addrs) == 0 {
		return
	}
	now := time.Now()
	records := make([]models.KnownBorrower, len(addrs))
	for i, addr := range addrs {
		records[i] = models.KnownBorrower{
			ChainID:     chainID,
			Address:     addr.Hex(),
			FirstSeenAt: now,
			LastSeenAt:  now,
		}
	}
	database := dbpkg.GetDB()
	if err := database.Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "chain_id"}, {Name: "address"}},
		DoUpdates: clause.Assignments(map[string]interface{}{"last_seen_at": now}),
	}).CreateInBatches(records, 200).Error; err != nil {
		log.Printf("[LiquidationBot] saveKnownBorrowers error: %v", err)
	}
}

// loadKnownBorrowers returns all addresses ever seen for a chain
func loadKnownBorrowers(chainID int) []ethcommon.Address {
	database := dbpkg.GetDB()
	var borrowers []models.KnownBorrower
	database.Where("chain_id = ?", chainID).Find(&borrowers)
	addrs := make([]ethcommon.Address, len(borrowers))
	for i, b := range borrowers {
		addrs[i] = ethcommon.HexToAddress(b.Address)
	}
	if len(addrs) > 0 {
		log.Printf("[LiquidationBot] Loaded %d known borrowers from DB", len(addrs))
	}
	return addrs
}

// loadLiquidationBlock reads the last processed block from scanner_state
func loadLiquidationBlock(chainID int) uint64 {
	database := dbpkg.GetDB()
	var state models.ScannerState
	if err := database.Where("chain_id = ? AND scanner_type = ?", chainID, "liquidation").First(&state).Error; err != nil {
		return 0
	}
	return state.LastScannedBlock
}

// saveLiquidationBlock persists the last processed block to scanner_state
func saveLiquidationBlock(chainID int, block uint64) {
	database := dbpkg.GetDB()
	state := models.ScannerState{
		ScannerType:      "liquidation",
		ChainID:          chainID,
		LastScannedBlock: block,
	}
	database.Where("chain_id = ? AND scanner_type = ?", chainID, "liquidation").Assign(state).FirstOrCreate(&state)
	database.Model(&state).Updates(map[string]interface{}{
		"last_scanned_block": block,
		"updated_at":         time.Now(),
	})
}

// loadBotContracts loads lendingProxy address from system_contracts
func loadBotContracts() (lendingAddr string, chainID int, err error) {
	database := dbpkg.GetDB()

	type row struct {
		ChainID int
		Address string
	}
	var r row
	if dbErr := database.Table("system_contracts").
		Where("name = ? AND is_active = ?", "lendingProxy", true).
		Select("chain_id, address").
		First(&r).Error; dbErr != nil {
		err = fmt.Errorf("lendingProxy not found in system_contracts: %w", dbErr)
		return
	}
	if r.Address == "" {
		err = fmt.Errorf("lendingProxy address is empty")
		return
	}
	lendingAddr = r.Address
	chainID = r.ChainID
	return
}

// saveLiquidationRecord persists a liquidation event to DB
func saveLiquidationRecord(chainID int, txHash string, liquidatorAddr ethcommon.Address, accounts []ethcommon.Address, gasUsed, blockNum uint64, status, errMsg string) {
	addrs := make([]string, len(accounts))
	for i, a := range accounts {
		addrs[i] = a.Hex()
	}
	addrsJSON, _ := json.Marshal(addrs)

	record := models.LiquidationRecord{
		ChainID:        chainID,
		TxHash:         txHash,
		LiquidatorAddr: liquidatorAddr.Hex(),
		AccountCount:   len(accounts),
		Accounts:       string(addrsJSON),
		GasUsed:        gasUsed,
		BlockNumber:    blockNum,
		Status:         status,
		ErrorMessage:   errMsg,
		CreatedAt:      time.Now(),
	}
	database := dbpkg.GetDB()
	if dbErr := database.Create(&record).Error; dbErr != nil {
		log.Printf("[LiquidationBot] Save record error: %v", dbErr)
	}
}
