package fundmarket

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

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

const (
	// YTAssetFactory on BSC Testnet
	ytAssetFactoryAddressBSC = "0x37B2CD7D94ba1400a6FEB34804a32EfD555bbfc8"
	// YTAssetFactory on Arbitrum Sepolia
	ytAssetFactoryAddressArb = "0x37B2CD7D94ba1400a6FEB34804a32EfD555bbfc8"

	// fastSnapshotInterval: price / TVL / APY — cheap single RPC call per asset
	fastSnapshotInterval = 5 * time.Minute
	// volumeSnapshotInterval: 24h volume via FilterLogs — heavier, runs less often
	volumeSnapshotInterval = 30 * time.Minute
)

// Buy / Sell events — emitted by YT token contract
// Buy(address user, uint256 usdcAmount, uint256 ytAmount)
// Sell(address user, uint256 ytAmount, uint256 usdcAmount)
const buySellEventABIJSON = `[
	{
		"anonymous": false,
		"inputs": [
			{"indexed": true,  "name": "user",       "type": "address"},
			{"indexed": false, "name": "usdcAmount",  "type": "uint256"},
			{"indexed": false, "name": "ytAmount",    "type": "uint256"}
		],
		"name": "Buy",
		"type": "event"
	},
	{
		"anonymous": false,
		"inputs": [
			{"indexed": true,  "name": "user",       "type": "address"},
			{"indexed": false, "name": "ytAmount",   "type": "uint256"},
			{"indexed": false, "name": "usdcAmount", "type": "uint256"}
		],
		"name": "Sell",
		"type": "event"
	}
]`

// blockTimeByChain returns approximate seconds-per-block for volume estimation
func blockTimeByChain(chainID int) float64 {
	switch chainID {
	case 97:     return 3    // BSC Testnet ~3s/block
	case 421614: return 0.25 // Arbitrum Sepolia ~0.25s/block
	default:     return 3
	}
}

// YTAssetFactory.getVaultInfo(address) ABI
// Returns: exists(bool), totalAssets, idleAssets, managedAssets, totalSupply,
//          hardCap, usdcPrice(8dec), ytPrice(30dec), nextRedemptionTime
const getVaultInfoABIJSON = `[{
	"inputs":[{"internalType":"address","name":"_vault","type":"address"}],
	"name":"getVaultInfo",
	"outputs":[
		{"internalType":"bool","name":"exists","type":"bool"},
		{"internalType":"uint256","name":"totalAssets","type":"uint256"},
		{"internalType":"uint256","name":"idleAssets","type":"uint256"},
		{"internalType":"uint256","name":"managedAssets","type":"uint256"},
		{"internalType":"uint256","name":"totalSupply","type":"uint256"},
		{"internalType":"uint256","name":"hardCap","type":"uint256"},
		{"internalType":"uint256","name":"usdcPrice","type":"uint256"},
		{"internalType":"uint256","name":"ytPrice","type":"uint256"},
		{"internalType":"uint256","name":"nextRedemptionTime","type":"uint256"}
	],
	"stateMutability":"view",
	"type":"function"
}]`

// StartPriceSnapshot starts two background loops:
//   - Fast loop (every 5min): price / TVL / APY via getVaultInfo
//   - Volume loop (every 30min): 24h USDC volume via FilterLogs
//
// Call as: go fundmarket.StartPriceSnapshot(cfg)
func StartPriceSnapshot(cfg *config.Config) {
	log.Println("=== Price Snapshot Service: fast=5min, volume=30min ===")

	// Fast loop runs in a sub-goroutine so both loops start concurrently.
	go func() {
		runFastSnapshot(cfg)
		t := time.NewTicker(fastSnapshotInterval)
		defer t.Stop()
		for range t.C {
			runFastSnapshot(cfg)
		}
	}()

	// Volume loop blocks this goroutine (keeps StartPriceSnapshot alive).
	runVolumeSnapshot(cfg)
	t := time.NewTicker(volumeSnapshotInterval)
	defer t.Stop()
	for range t.C {
		runVolumeSnapshot(cfg)
	}
}

// loadActiveAssets returns all active assets that have a contract address set.
func loadActiveAssets(database *gorm.DB, label string) ([]models.Asset, bool) {
	var assets []models.Asset
	if err := database.Where(
		"is_active = ? AND contract_address IS NOT NULL AND contract_address != ''",
		true,
	).Find(&assets).Error; err != nil {
		log.Printf("[%s] DB query error: %v", label, err)
		return nil, false
	}
	if len(assets) == 0 {
		log.Printf("[%s] No active assets with contract_address set, skipping", label)
		return nil, false
	}
	return assets, true
}

// ── Fast snapshot (price / TVL / APY) ────────────────────────────────────────

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

	database := db.GetDB()
	assets, ok := loadActiveAssets(database, "FastSnapshot")
	if !ok {
		return
	}

	factoryABI, err := abi.JSON(strings.NewReader(getVaultInfoABIJSON))
	if err != nil {
		log.Printf("[FastSnapshot] Parse factory ABI error: %v", err)
		return
	}

	today := time.Now().Truncate(24 * time.Hour)

	for _, asset := range assets {
		rpcURL, factoryAddrStr := getSnapshotClientForChain(asset.ChainID, cfg)
		if rpcURL == "" {
			log.Printf("[FastSnapshot] %s: unsupported chain_id=%d, skipping", asset.AssetCode, asset.ChainID)
			continue
		}
		client, err := ethclient.Dial(rpcURL)
		if err != nil {
			log.Printf("[FastSnapshot] %s: RPC connect error: %v", asset.AssetCode, err)
			continue
		}
		factoryAddr := common.HexToAddress(factoryAddrStr)
		if err := snapshotAssetFast(client, database, asset, factoryABI, factoryAddr, today); err != nil {
			log.Printf("[FastSnapshot] %s error: %v", asset.AssetCode, err)
		}
		client.Close()
	}

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

func snapshotAssetFast(
	client *ethclient.Client,
	database *gorm.DB,
	asset models.Asset,
	factoryABI abi.ABI,
	factoryAddr common.Address,
	today time.Time,
) error {
	ctx := context.Background()
	vaultAddr := common.HexToAddress(asset.ContractAddress)

	callData, err := factoryABI.Pack("getVaultInfo", vaultAddr)
	if err != nil {
		return fmt.Errorf("pack getVaultInfo: %w", err)
	}
	result, err := client.CallContract(ctx, ethereum.CallMsg{
		To:   &factoryAddr,
		Data: callData,
	}, nil)
	if err != nil {
		return fmt.Errorf("getVaultInfo call: %w", err)
	}

	decoded, err := factoryABI.Unpack("getVaultInfo", result)
	if err != nil {
		return fmt.Errorf("unpack getVaultInfo: %w", err)
	}
	if len(decoded) < 9 {
		return fmt.Errorf("getVaultInfo returned %d values, expected 9", len(decoded))
	}

	info := vaultInfo{
		exists:      decoded[0].(bool),
		totalAssets: bigIntToFloat(decoded[1].(*big.Int), 18),
		totalSupply: bigIntToFloat(decoded[4].(*big.Int), 18),
		hardCap:     bigIntToFloat(decoded[5].(*big.Int), 18),
		usdcPrice:   bigIntToFloat(decoded[6].(*big.Int), 8),
		ytPrice:     bigIntToFloat(decoded[7].(*big.Int), 30),
	}
	if !info.exists {
		return fmt.Errorf("vault %s not registered in factory", asset.ContractAddress)
	}

	poolCapPercent := 0.0
	if info.hardCap > 0 {
		poolCapPercent = info.totalSupply / info.hardCap * 100
	}

	apy := calcAPY(database, asset.ID, info.ytPrice, today)

	log.Printf("[FastSnapshot] %s: ytPrice=%.8f USDC, supply=%.2f, TVL=$%.2f, APY=%.2f%%",
		asset.AssetCode, info.ytPrice, info.totalSupply, info.totalAssets, apy)

	// Upsert today's row — do NOT touch volume_24h_usd (managed by volume task).
	todayDate := today.Format("2006-01-02")
	var perf models.AssetPerformance
	if err := database.Where("asset_id = ? AND snapshot_date = ?", asset.ID, todayDate).First(&perf).Error; err != nil {
		// Row doesn't exist yet — create it. volume_24h_usd defaults to 0 until volume task runs.
		perf = models.AssetPerformance{
			AssetID:             asset.ID,
			CurrentAPY:          apy,
			TVLUSD:              info.totalAssets,
			CirculatingSupply:   info.totalSupply,
			PoolCapacityPercent: poolCapPercent,
			YTPrice:             info.ytPrice,
			Volume24hUSD:        0,
			SnapshotDate:        today,
		}
		if err := database.Create(&perf).Error; err != nil {
			return err
		}
	} else {
		if err := database.Model(&perf).Updates(map[string]interface{}{
			"current_apy":           apy,
			"tvlusd":                info.totalAssets,
			"circulating_supply":    info.totalSupply,
			"pool_capacity_percent": poolCapPercent,
			"yt_price":              info.ytPrice,
			// volume_24h_usd intentionally omitted
		}).Error; err != nil {
			return err
		}
	}

	// Sync pool_cap_usd in assets table.
	if capUSD := info.hardCap * info.ytPrice; capUSD > 0 {
		if err := database.Model(&models.Asset{}).Where("id = ?", asset.ID).
			Update("pool_cap_usd", capUSD).Error; err != nil {
			log.Printf("[FastSnapshot] %s: update pool_cap_usd error: %v", asset.AssetCode, err)
		}
	}

	// Always insert a new record into apy_snapshots for chart history.
	hourlySnap := models.APYSnapshot{
		AssetID:         asset.ID,
		ChainID:         asset.ChainID,
		ContractAddress: asset.ContractAddress,
		APYValue:        apy,
		Price:           info.ytPrice,
		TotalAssets:     info.totalAssets,
		TotalSupply:     info.totalSupply,
		SnapshotTime:    time.Now(),
	}
	return database.Create(&hourlySnap).Error
}

// ── Volume snapshot (24h FilterLogs) ─────────────────────────────────────────

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

	database := db.GetDB()
	assets, ok := loadActiveAssets(database, "VolumeSnapshot")
	if !ok {
		return
	}

	todayDate := time.Now().Truncate(24 * time.Hour).Format("2006-01-02")

	for _, asset := range assets {
		rpcURL, _ := getSnapshotClientForChain(asset.ChainID, cfg)
		if rpcURL == "" {
			log.Printf("[VolumeSnapshot] %s: unsupported chain_id=%d, skipping", asset.AssetCode, asset.ChainID)
			continue
		}
		client, err := ethclient.Dial(rpcURL)
		if err != nil {
			log.Printf("[VolumeSnapshot] %s: RPC connect error: %v", asset.AssetCode, err)
			continue
		}

		volume24h := calc24hVolume(client, asset, asset.ChainID)
		client.Close()

		// Update only volume_24h_usd in today's performance row.
		res := database.Model(&models.AssetPerformance{}).
			Where("asset_id = ? AND snapshot_date = ?", asset.ID, todayDate).
			Update("volume_24h_usd", volume24h)
		if res.Error != nil {
			log.Printf("[VolumeSnapshot] %s: DB update error: %v", asset.AssetCode, res.Error)
			continue
		}
		if res.RowsAffected == 0 {
			// Fast snapshot hasn't run yet for today; volume will be set on next cycle.
			log.Printf("[VolumeSnapshot] %s: no row for today yet, will retry next cycle", asset.AssetCode)
			continue
		}
		log.Printf("[VolumeSnapshot] %s: volume=$%.2f updated", asset.AssetCode, volume24h)
	}

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

// ── Shared helpers ────────────────────────────────────────────────────────────

// getSnapshotClientForChain returns the RPC URL and factory address for the given chain ID
func getSnapshotClientForChain(chainID int, cfg *config.Config) (rpcURL, factoryAddr string) {
	switch chainID {
	case 97:
		return cfg.BSCTestnetRPC, ytAssetFactoryAddressBSC
	case 421614:
		return cfg.ArbSepoliaRPC, ytAssetFactoryAddressArb
	default:
		return "", ""
	}
}

// vaultInfo holds decoded output from YTAssetFactory.getVaultInfo()
type vaultInfo struct {
	exists      bool
	totalAssets float64 // USDC, 18 dec on BSC
	totalSupply float64 // YT tokens, 18 dec
	hardCap     float64 // YT tokens, 18 dec
	usdcPrice   float64 // 8 dec
	ytPrice     float64 // 30 dec
}

// calcAPY returns annualized APY (%) based on daily price change vs yesterday.
func calcAPY(database *gorm.DB, assetID uint, currentPrice float64, today time.Time) float64 {
	yesterday := today.AddDate(0, 0, -1).Format("2006-01-02")
	var prev models.AssetPerformance
	if err := database.Where("asset_id = ? AND snapshot_date = ?", assetID, yesterday).First(&prev).Error; err != nil {
		return 0
	}
	if prev.YTPrice <= 0 {
		return 0
	}
	dailyReturn := (currentPrice - prev.YTPrice) / prev.YTPrice
	return dailyReturn * 365 * 100
}

// maxFilterBlockRange is the max block range allowed per FilterLogs call.
// Most public RPC nodes cap this at 10,000 blocks.
const maxFilterBlockRange = int64(9000)

// chunkTimeout is the per-chunk RPC call timeout.
const chunkTimeout = 15 * time.Second

// calc24hVolume scans Buy + Sell events on the YT token contract for the last 24h
// and returns the total USDC volume. Paginates FilterLogs in 9000-block chunks to
// stay within RPC node limits. Each chunk gets its own timeout to prevent a slow
// early chunk from cancelling the rest of the scan.
func calc24hVolume(client *ethclient.Client, asset models.Asset, chainID int) float64 {
	bctx, bcancel := context.WithTimeout(context.Background(), 10*time.Second)
	currentBlock, err := client.BlockNumber(bctx)
	bcancel()
	if err != nil {
		log.Printf("[Volume] %s: get block number error: %v", asset.AssetCode, err)
		return 0
	}

	// Estimate fromBlock covering 24h + 20% buffer to handle block-time variance.
	blockTime := blockTimeByChain(chainID)
	blocksIn24h := int64(float64(86400) / blockTime * 1.2)
	fromBlock := int64(currentBlock) - blocksIn24h
	if fromBlock < 0 {
		fromBlock = 0
	}

	eventABI, err := abi.JSON(strings.NewReader(buySellEventABIJSON))
	if err != nil {
		log.Printf("[Volume] %s: parse event ABI error: %v", asset.AssetCode, err)
		return 0
	}
	buyTopic  := eventABI.Events["Buy"].ID
	sellTopic := eventABI.Events["Sell"].ID

	contractAddr := common.HexToAddress(asset.ContractAddress)
	usdcDecimals := int64(18)
	toBlock := int64(currentBlock)

	log.Printf("[Volume] %s: scanning blocks %d-%d (%d chunks), contract=%s",
		asset.AssetCode, fromBlock, toBlock,
		(toBlock-fromBlock)/maxFilterBlockRange+1,
		contractAddr.Hex())

	var totalVolume float64
	var totalLogs int

	// Paginate in chunks. Each chunk gets its own independent timeout so that a
	// slow or failing chunk does not cancel subsequent chunks.
	for start := fromBlock; start <= toBlock; start += maxFilterBlockRange {
		end := start + maxFilterBlockRange - 1
		if end > toBlock {
			end = toBlock
		}

		query := ethereum.FilterQuery{
			FromBlock: big.NewInt(start),
			ToBlock:   big.NewInt(end),
			Addresses: []common.Address{contractAddr},
			Topics:    [][]common.Hash{{buyTopic, sellTopic}},
		}

		// Retry once on failure, each attempt with its own independent timeout.
		var chunkLogs []types.Log
		var fetchErr error
		for attempt := 0; attempt < 2; attempt++ {
			chunkCtx, chunkCancel := context.WithTimeout(context.Background(), chunkTimeout)
			chunkLogs, fetchErr = client.FilterLogs(chunkCtx, query)
			chunkCancel()
			if fetchErr == nil {
				break
			}
			log.Printf("[Volume] %s: filter logs [%d-%d] attempt %d error: %v",
				asset.AssetCode, start, end, attempt+1, fetchErr)
		}
		if fetchErr != nil {
			log.Printf("[Volume] %s: skipping chunk [%d-%d] after retries", asset.AssetCode, start, end)
			continue
		}

		totalLogs += len(chunkLogs)
		for _, vLog := range chunkLogs {
			var eventDef abi.Event
			switch vLog.Topics[0] {
			case buyTopic:
				eventDef = eventABI.Events["Buy"]
			case sellTopic:
				eventDef = eventABI.Events["Sell"]
			default:
				continue
			}

			decoded, err := eventDef.Inputs.NonIndexed().Unpack(vLog.Data)
			if err != nil || len(decoded) < 2 {
				continue
			}
			// Buy:  [0]=usdcAmount, [1]=ytAmount
			// Sell: [0]=ytAmount,   [1]=usdcAmount
			var usdcAmt *big.Int
			if vLog.Topics[0] == buyTopic {
				usdcAmt = decoded[0].(*big.Int)
			} else {
				usdcAmt = decoded[1].(*big.Int)
			}
			totalVolume += bigIntToFloat(usdcAmt, usdcDecimals)
		}
	}

	log.Printf("[Volume] %s: total logs found=%d, volume=$%.2f", asset.AssetCode, totalLogs, totalVolume)
	return totalVolume
}

// bigIntToFloat converts *big.Int to float64 by dividing by 10^decimals.
func bigIntToFloat(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
}
