package lending

import (
	"context"
	"encoding/hex"
	"fmt"
	"log"
	"math/big"
	"time"

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

// TokenInfo represents token contract information
type TokenInfo struct {
	Symbol          string
	Name            string
	Decimals        int
	ContractAddress string
	ChainID         int
	AssetCode       string
}

// getRPCURL returns the RPC URL for a given chain ID from config
func getRPCURL(chainId int) string {
	cfg := appcfg.AppConfig
	switch chainId {
	case 97: // BSC Testnet
		if cfg != nil && cfg.BSCTestnetRPC != "" {
			return cfg.BSCTestnetRPC
		}
	case 421614: // Arbitrum Sepolia
		if cfg != nil && cfg.ArbSepoliaRPC != "" {
			return cfg.ArbSepoliaRPC
		}
	}
	return ""
}

// getContractAddressByChain returns the contract address from TokenInfo (chain-agnostic now)
func getContractAddressByChain(info TokenInfo, chainId int) string {
	return info.ContractAddress
}

// fetchDecimalsOnChain calls decimals() on an ERC20 contract via RPC
// Returns error if chain call fails so caller can fall back to DB
func fetchDecimalsOnChain(contractAddress, rpcURL string) (int, error) {
	if rpcURL == "" || contractAddress == "" {
		return 0, fmt.Errorf("missing rpcURL or contractAddress")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

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

	// decimals() selector = keccak256("decimals()")[0:4] = 313ce567
	data, _ := hex.DecodeString("313ce567")
	addr := ethcommon.HexToAddress(contractAddress)

	result, err := client.CallContract(ctx, ethereum.CallMsg{
		To:   &addr,
		Data: data,
	}, nil)
	if err != nil {
		return 0, fmt.Errorf("call contract: %w", err)
	}
	if len(result) < 1 {
		return 0, fmt.Errorf("empty result")
	}

	decimals := new(big.Int).SetBytes(result[len(result)-1:])
	return int(decimals.Int64()), nil
}

// resolveDecimals tries chain first (if chainId != 0), falls back to dbDecimals
func resolveDecimals(contractAddress string, chainId, dbDecimals int) int {
	if chainId == 0 {
		return dbDecimals
	}
	rpcURL := getRPCURL(chainId)
	if rpcURL == "" {
		return dbDecimals
	}
	decimals, err := fetchDecimalsOnChain(contractAddress, rpcURL)
	if err != nil {
		log.Printf("⚠️ [Lending] 链上获取精度失败 %s (chain %d): %v，使用数据库配置 %d", contractAddress, chainId, err, dbDecimals)
		return dbDecimals
	}
	return decimals
}

// GetTokenInfoFromDB queries any token from assets table by asset_code
func GetTokenInfoFromDB(assetCode string) (*TokenInfo, error) {
	db := common.GetDB()
	var asset models.Asset
	if err := db.Where("asset_code = ? AND is_active = ?", assetCode, true).First(&asset).Error; err != nil {
		return nil, err
	}
	return &TokenInfo{
		Symbol:          asset.TokenSymbol,
		Name:            asset.Name,
		Decimals:        asset.Decimals,
		ContractAddress: asset.ContractAddress,
		ChainID:         asset.ChainID,
		AssetCode:       asset.AssetCode,
	}, nil
}

// GetUSDCInfo returns USDC token information.
// chainId != 0: tries on-chain decimals() first, falls back to DB.
// chainId == 0: DB only.
func GetUSDCInfo(chainId int) TokenInfo {
	info, err := GetTokenInfoFromDB("USDC")
	if err != nil {
		log.Printf("⚠️ [Lending] USDC 不在 assets 表: %v，使用默认值", err)
		return TokenInfo{AssetCode: "USDC", Symbol: "USDC", Name: "USD Coin", Decimals: 18}
	}
	contractAddr := getContractAddressByChain(*info, chainId)
	info.Decimals = resolveDecimals(contractAddr, chainId, info.Decimals)
	return *info
}

// GetYTTokenInfo returns YT token information.
// chainId != 0: tries on-chain decimals() first, falls back to DB.
func GetYTTokenInfo(assetCode string, chainId int) (*TokenInfo, error) {
	db := common.GetDB()
	var asset models.Asset
	if err := db.Where("asset_code = ? AND is_active = ?", assetCode, true).First(&asset).Error; err != nil {
		return nil, err
	}
	info := &TokenInfo{
		Symbol:          asset.TokenSymbol,
		Name:            asset.Name,
		Decimals:        asset.Decimals,
		ContractAddress: asset.ContractAddress,
		ChainID:         asset.ChainID,
		AssetCode:       asset.AssetCode,
	}
	info.Decimals = resolveDecimals(info.ContractAddress, info.ChainID, info.Decimals)
	return info, nil
}

// GetAllStablecoins returns all active stablecoin tokens (token_role = 'stablecoin').
func GetAllStablecoins(chainId int) ([]TokenInfo, error) {
	db := common.GetDB()
	var assets []models.Asset
	if err := db.Where("token_role = ? AND is_active = ?", "stablecoin", true).Find(&assets).Error; err != nil {
		return nil, err
	}
	tokens := make([]TokenInfo, 0, len(assets))
	for _, asset := range assets {
		info := TokenInfo{
			Symbol:          asset.TokenSymbol,
			Name:            asset.Name,
			Decimals:        asset.Decimals,
			ContractAddress: asset.ContractAddress,
			ChainID:         asset.ChainID,
			AssetCode:       asset.AssetCode,
		}
		info.Decimals = resolveDecimals(info.ContractAddress, chainId, info.Decimals)
		tokens = append(tokens, info)
	}
	return tokens, nil
}

// GetAllYTTokens returns all active YT tokens.
// chainId != 0: tries on-chain decimals() first, falls back to DB.
func GetAllYTTokens(chainId int) ([]TokenInfo, error) {
	db := common.GetDB()
	var assets []models.Asset
	if err := db.Where("token_role = ? AND is_active = ?", "yt_token", true).Find(&assets).Error; err != nil {
		return nil, err
	}

	tokens := make([]TokenInfo, 0, len(assets))
	for _, asset := range assets {
		info := TokenInfo{
			Symbol:          asset.TokenSymbol,
			Name:            asset.Name,
			Decimals:        asset.Decimals,
			ContractAddress: asset.ContractAddress,
			ChainID:         asset.ChainID,
			AssetCode:       asset.AssetCode,
		}
		info.Decimals = resolveDecimals(info.ContractAddress, info.ChainID, info.Decimals)
		tokens = append(tokens, info)
	}
	return tokens, nil
}

// ValidateYTToken checks if the given asset code is a valid YT token
func ValidateYTToken(assetCode string) bool {
	db := common.GetDB()
	var count int64
	db.Model(&models.Asset{}).
		Where("asset_code = ? AND token_role = ? AND is_active = ?", assetCode, "yt_token", true).
		Count(&count)
	return count > 0
}

// GetLendingMarketConfig returns lending market configuration
func GetLendingMarketConfig() map[string]interface{} {
	return map[string]interface{}{
		"market_name":                 "AssetX Lending Market",
		"base_supply_apy":             6.1,
		"base_borrow_apy":             9.1,
		"borrow_collateral_factor":    0.7,
		"liquidate_collateral_factor": 0.75,
		"liquidation_penalty":         0.1,
		"kink_rate":                   0.8,
	}
}

// CalculateHealthFactor calculates health factor
func CalculateHealthFactor(collateralValueUSD, borrowedValueUSD float64) float64 {
	if borrowedValueUSD == 0 {
		return 999999.0
	}
	config := GetLendingMarketConfig()
	liquidationThreshold := config["liquidate_collateral_factor"].(float64)
	return (collateralValueUSD * liquidationThreshold) / borrowedValueUSD
}

// CalculateLTV calculates Loan-to-Value ratio
func CalculateLTV(collateralValueUSD, borrowedValueUSD float64) float64 {
	if collateralValueUSD == 0 {
		return 0
	}
	return (borrowedValueUSD / collateralValueUSD) * 100
}

// GetContractAddress returns contract address for the given chain
func GetContractAddress(token TokenInfo, chainID int) string {
	return getContractAddressByChain(token, chainID)
}
