package lending

import (
	"context"
	"fmt"
	"log"
	"math/big"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	ethcommon "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	appcfg "github.com/gothinkster/golang-gin-realworld-example-app/config"
	db "github.com/gothinkster/golang-gin-realworld-example-app/common"
	"github.com/gothinkster/golang-gin-realworld-example-app/models"
	"gorm.io/gorm"
)

const lendingSnapshotInterval = 15 * time.Minute

// Only the 4 view functions needed for APY snapshot
const lendingRateABIJSON = `[
	{"inputs":[],"name":"getSupplyRate","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},
	{"inputs":[],"name":"getBorrowRate","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},
	{"inputs":[],"name":"getTotalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
	{"inputs":[],"name":"getTotalBorrow","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}
]`

// StartLendingAPYSnapshot starts the hourly lending APY snapshot background service.
// Call as: go lending.StartLendingAPYSnapshot(cfg)
func StartLendingAPYSnapshot(cfg *appcfg.Config) {
	log.Println("=== Lending APY Snapshot Service Started (interval: 15min) ===")
	runLendingSnapshot(cfg)
	ticker := time.NewTicker(lendingSnapshotInterval)
	defer ticker.Stop()
	for range ticker.C {
		runLendingSnapshot(cfg)
	}
}

func runLendingSnapshot(cfg *appcfg.Config) {
	start := time.Now()
	log.Printf("[LendingSnapshot] Starting at %s", start.Format("2006-01-02 15:04:05"))

	database := db.GetDB()

	// Load lendingProxy contracts from system_contracts
	var lendingContracts []struct {
		Name    string `gorm:"column:name"`
		ChainID int    `gorm:"column:chain_id"`
		Address string `gorm:"column:address"`
	}
	if err := database.Table("system_contracts").
		Where("name = ? AND is_active = ?", "lendingProxy", true).
		Select("name, chain_id, address").
		Find(&lendingContracts).Error; err != nil {
		log.Printf("[LendingSnapshot] Load lendingProxy error: %v", err)
		return
	}
	if len(lendingContracts) == 0 {
		log.Println("[LendingSnapshot] No lendingProxy in system_contracts, skipping")
		return
	}

	var usdcAsset models.Asset
	if err := database.Where("asset_code = ?", "USDC").First(&usdcAsset).Error; err != nil {
		log.Printf("[LendingSnapshot] USDC asset not found in assets table: %v", err)
		return
	}

	parsedABI, err := abi.JSON(strings.NewReader(lendingRateABIJSON))
	if err != nil {
		log.Printf("[LendingSnapshot] Parse ABI error: %v", err)
		return
	}

	for _, lc := range lendingContracts {
		if lc.Address == "" {
			log.Printf("[LendingSnapshot] lendingProxy chain_id=%d has no address, skipping", lc.ChainID)
			continue
		}
		rpcURL := getRPCURL(lc.ChainID)
		if rpcURL == "" {
			log.Printf("[LendingSnapshot] lendingProxy unsupported chain_id=%d, skipping", lc.ChainID)
			continue
		}
		if err := snapshotLendingMarket(database, parsedABI, rpcURL, lc.Address, lc.ChainID, usdcAsset); err != nil {
			log.Printf("[LendingSnapshot] lendingProxy chain_id=%d error: %v", lc.ChainID, err)
		}
	}

	log.Printf("[LendingSnapshot] Done in %v", time.Since(start))
}

func snapshotLendingMarket(
	database *gorm.DB,
	parsedABI abi.ABI,
	rpcURL, contractAddr string,
	chainID int,
	usdcAsset models.Asset,
) error {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	client, err := ethclient.DialContext(ctx, rpcURL)
	if err != nil {
		return fmt.Errorf("dial rpc: %w", err)
	}
	defer client.Close()

	addr := ethcommon.HexToAddress(contractAddr)

	// Generic call helper: packs, calls, unpacks, returns *big.Int
	call := func(name string) (*big.Int, error) {
		data, err := parsedABI.Pack(name)
		if err != nil {
			return nil, fmt.Errorf("pack %s: %w", name, err)
		}
		result, err := client.CallContract(ctx, ethereum.CallMsg{To: &addr, Data: data}, nil)
		if err != nil {
			return nil, fmt.Errorf("call %s: %w", name, err)
		}
		decoded, err := parsedABI.Unpack(name, result)
		if err != nil {
			return nil, fmt.Errorf("unpack %s: %w", name, err)
		}
		if len(decoded) == 0 {
			return nil, fmt.Errorf("%s: empty result", name)
		}
		switch v := decoded[0].(type) {
		case uint64:
			return new(big.Int).SetUint64(v), nil
		case *big.Int:
			return v, nil
		default:
			return nil, fmt.Errorf("%s: unexpected type %T", name, decoded[0])
		}
	}

	supplyRateRaw, err := call("getSupplyRate")
	if err != nil {
		return err
	}
	borrowRateRaw, err := call("getBorrowRate")
	if err != nil {
		return err
	}
	totalSupplyRaw, err := call("getTotalSupply")
	if err != nil {
		return err
	}
	totalBorrowRaw, err := call("getTotalBorrow")
	if err != nil {
		return err
	}

	// APR% = rate / 1e18 * 100
	// This contract's getSupplyRate/getBorrowRate already return annualized rate (APR × 1e18),
	// NOT per-second rate. Do NOT multiply by secondsPerYear.
	toAPRPct := func(raw *big.Int) float64 {
		f, _ := new(big.Float).SetPrec(256).Quo(
			new(big.Float).SetPrec(256).SetInt(raw),
			new(big.Float).SetPrec(256).SetFloat64(1e18),
		).Float64()
		return f * 100
	}

	usdcDec := int64(usdcAsset.Decimals)
	totalSupply := lendingBigIntToFloat(totalSupplyRaw, usdcDec)
	totalBorrow := lendingBigIntToFloat(totalBorrowRaw, usdcDec)
	supplyAPR := toAPRPct(supplyRateRaw)
	borrowAPR := toAPRPct(borrowRateRaw)

	log.Printf("[LendingSnapshot] supply=%.4f USDC, borrow=%.4f USDC, utilization=%.2f%%, supplyAPR=%.4f%%, borrowAPR=%.4f%%",
		totalSupply, totalBorrow,
		func() float64 {
			if totalSupply == 0 {
				return 0
			}
			return totalBorrow / totalSupply * 100
		}(),
		supplyAPR, borrowAPR,
	)

	snap := models.APYSnapshot{
		AssetID:         usdcAsset.ID,
		ChainID:         chainID,
		ContractAddress: contractAddr,
		SupplyAPY:       supplyAPR,
		BorrowAPY:       borrowAPR,
		TotalAssets:     totalSupply, // total USDC deposited
		TotalSupply:     totalBorrow, // total USDC borrowed
		SnapshotTime:    time.Now(),
	}
	return database.Create(&snap).Error
}

// lendingBigIntToFloat converts *big.Int to float64 by dividing by 10^decimals
func lendingBigIntToFloat(n *big.Int, decimals int64) float64 {
	divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil)
	f, _ := new(big.Float).SetPrec(256).Quo(
		new(big.Float).SetPrec(256).SetInt(n),
		new(big.Float).SetPrec(256).SetInt(divisor),
	).Float64()
	return f
}
