package core

import (
	"bufio"
	"encoding/csv"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"sync"
	"sync/atomic"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/lru"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/log"
)

// XdcBulkSyncMode is set to true during initial block sync (when chain head is far behind peers).
// When true, signing tx processing is skipped (ECDSA recovery, logs, bloom) for faster sync.
// Set by the downloader when syncing, cleared when reaching chain tip.
var XdcBulkSyncMode atomic.Bool

const (
	// XDC state root cache size - 10M to handle mainnet (99M+ blocks)
	// With EVERY block having different state roots due to BigBalance/uint256 encoding,
	// we need a massive cache to prevent "missing trie node" errors during sync.
	xdcStateRootCacheSize = 100_000  // LRU cache size - prevents OOM at V2

	// Backward scan range on startup (last 10K blocks)
	backwardScanRange = 10_000
)

// xdcStateRootCache maps block numbers and remote state roots to GP5's locally computed state roots.
// This is needed because GP5 (uint256 + BigBalance) produces different state roots
// than v2.6.8 (native big.Int) for XDC chains with overflow balances (Apothem).
// When processing block N+1, we need to load state from GP5's computed root for
// block N, not the stored header's root (which comes from v2.6.8).
// This mirrors Nethermind's XdcStateRootCache approach.
var xdcStateRootCache = struct {
	sync.RWMutex
	blockRoots    *lru.Cache[uint64, common.Hash]      // LRU: blockNum → local root
	remoteToLocal *lru.Cache[common.Hash, common.Hash] // LRU: remote → local
	blockToRemote *lru.Cache[uint64, common.Hash]      // LRU: blockNum → remote
	db            ethdb.Database
	initialized   bool
	// Guard to prevent repeated backward scans causing initialization loops
	scanInProgress bool
	lastScanBlock  uint64
}{
	blockRoots:    lru.NewCache[uint64, common.Hash](xdcStateRootCacheSize),
	remoteToLocal: lru.NewCache[common.Hash, common.Hash](xdcStateRootCacheSize),
	blockToRemote: lru.NewCache[uint64, common.Hash](xdcStateRootCacheSize),
}

// initXdcCache initializes the cache and loads persisted data from LevelDB
func initXdcCache(db ethdb.Database) {
	xdcStateRootCache.Lock()
	defer xdcStateRootCache.Unlock()

	if xdcStateRootCache.initialized {
		return
	}

	xdcStateRootCache.db = db
	xdcStateRootCache.initialized = true

	// Backfill cache from LevelDB
	blockRoots := rawdb.ReadAllXdcLocalStateRoots(db)
	remoteToLocal := rawdb.ReadAllXdcRemoteToLocal(db)

	// Reconstruct blockToRemote from what we have in DB
	// We don't store blockToRemote explicitly; derive it during backfill
	count := 0
	for blockNum, localRoot := range blockRoots {
		xdcStateRootCache.blockRoots.Add(blockNum, localRoot)
		count++
		if count >= xdcStateRootCacheSize {
			break // Stop at LRU limit
		}
	}
	for remoteRoot, localRoot := range remoteToLocal {
		xdcStateRootCache.remoteToLocal.Add(remoteRoot, localRoot)
	}

	// One-time migration: if LevelDB cache is empty but old CSV exists, import it
	if len(blockRoots) == 0 && len(remoteToLocal) == 0 {
		migrated := migrateXdcCacheFromCSV(db)
		if migrated > 0 {
			log.Info("XDC cache: migrated entries from old CSV file", "count", migrated)
		}
	}

	log.Info("XDC state root cache initialized",
		"size", xdcStateRootCacheSize,
		"loadedBlockRoots", len(blockRoots),
		"loadedRemoteMappings", len(remoteToLocal))
}

// XdcCacheStateRoot stores GP5's computed state root for a given block number.
func XdcCacheStateRoot(blockNum uint64, localRoot common.Hash, remoteRoot common.Hash) error {
	// Don't cache if roots are identical (no divergence)
	if localRoot == remoteRoot {
		return nil
	}

	xdcStateRootCache.Lock()
	defer xdcStateRootCache.Unlock()

	// Store in memory caches (LRU handles eviction automatically)
	xdcStateRootCache.blockRoots.Add(blockNum, localRoot)
	xdcStateRootCache.remoteToLocal.Add(remoteRoot, localRoot)
	xdcStateRootCache.blockToRemote.Add(blockNum, remoteRoot)

	// Persist to LevelDB atomically via batch
	if xdcStateRootCache.db != nil {
		batch := xdcStateRootCache.db.NewBatch()
		rawdb.WriteXdcLocalStateRoot(batch, blockNum, localRoot)
		rawdb.WriteXdcRemoteToLocal(batch, remoteRoot, localRoot)
		if err := batch.Write(); err != nil {
			return err
		}
	}
	return nil
}

// XdcGetCachedStateRoot returns GP5's computed state root for a block, if cached.
func XdcGetCachedStateRoot(blockNum uint64) (common.Hash, bool) {
	xdcStateRootCache.RLock()
	root, ok := xdcStateRootCache.blockRoots.Get(blockNum)
	xdcStateRootCache.RUnlock()
	if ok {
		return root, true
	}

	// Fallback to LevelDB
	if xdcStateRootCache.db == nil {
		return common.Hash{}, false
	}
	root = rawdb.ReadXdcLocalStateRoot(xdcStateRootCache.db, blockNum)
	if root == (common.Hash{}) {
		return common.Hash{}, false
	}

	// Backfill to memory
	xdcStateRootCache.Lock()
	xdcStateRootCache.blockRoots.Add(blockNum, root)
	xdcStateRootCache.Unlock()
	return root, true
}

// XdcFindCachedRootForRemote returns the locally-computed root for a given remote root.
func XdcFindCachedRootForRemote(remoteRoot common.Hash) (common.Hash, bool) {
	xdcStateRootCache.RLock()
	localRoot, ok := xdcStateRootCache.remoteToLocal.Get(remoteRoot)
	xdcStateRootCache.RUnlock()
	if ok {
		return localRoot, true
	}

	// Fallback to LevelDB
	if xdcStateRootCache.db == nil {
		return common.Hash{}, false
	}
	localRoot = rawdb.ReadXdcRemoteToLocal(xdcStateRootCache.db, remoteRoot)
	if localRoot == (common.Hash{}) {
		return common.Hash{}, false
	}

	// Backfill to memory
	xdcStateRootCache.Lock()
	xdcStateRootCache.remoteToLocal.Add(remoteRoot, localRoot)
	xdcStateRootCache.Unlock()
	return localRoot, true
}

// XdcBackwardScanForValidRoot scans backward from fromBlock up to scanRange blocks
// to find a cached state root. Returns (blockNum, localRoot, true) if found.
// This prevents genesis rewind on restart when the head block's root isn't cached.
func XdcBackwardScanForValidRoot(fromBlock uint64, scanRange uint64) (uint64, common.Hash, bool) {
	xdcStateRootCache.RLock()
	defer xdcStateRootCache.RUnlock()

	startBlock := uint64(0)
	if fromBlock > scanRange {
		startBlock = fromBlock - scanRange
	}

	// Scan backward from fromBlock to startBlock
	for blockNum := fromBlock; blockNum >= startBlock; blockNum-- {
		if root, ok := xdcStateRootCache.blockRoots.Get(blockNum); ok {
			log.Info("XDC backward scan: found valid state root",
				"block", blockNum,
				"scannedBack", fromBlock-blockNum,
				"root", root)
			return blockNum, root, true
		}
		if blockNum == 0 {
			break
		}
	}

	log.Warn("XDC backward scan: no valid state root found",
		"fromBlock", fromBlock,
		"scanRange", scanRange)
	return 0, common.Hash{}, false
}

// XdcFindBestCachedRoot scans the entire cache to find the highest block number
// with a valid state root that is <= fromBlock. Unlike XdcBackwardScanForValidRoot,
// this is not limited to a fixed window and will find cached roots regardless of
// how long ago they were written (e.g. across restarts where cache was not flushed).
// This prevents the "ancient chain truncation" bug where the node resets to genesis
// after a crash because the fixed 10K scan window misses older cache entries.
func XdcFindBestCachedRoot(fromBlock uint64) (uint64, common.Hash, bool) {
	xdcStateRootCache.RLock()
	defer xdcStateRootCache.RUnlock()

	if xdcStateRootCache.blockRoots.Len() == 0 {
		log.Warn("XDC cache: no cached roots available", "fromBlock", fromBlock)
		return 0, common.Hash{}, false
	}

	bestBlock := uint64(0)
	bestRoot := common.Hash{}
	found := false

	for _, blockNum := range xdcStateRootCache.blockRoots.Keys() {
		root, _ := xdcStateRootCache.blockRoots.Peek(blockNum)
		if blockNum <= fromBlock && blockNum > bestBlock {
			bestBlock = blockNum
			bestRoot = root
			found = true
		}
	}

	if found {
		log.Info("XDC cache: found best cached root",
			"block", bestBlock,
			"fromBlock", fromBlock,
			"gap", fromBlock-bestBlock,
			"totalCacheEntries", xdcStateRootCache.blockRoots.Len())
	} else {
		log.Warn("XDC cache: no cached root found at or below head",
			"fromBlock", fromBlock,
			"cacheSize", xdcStateRootCache.blockRoots.Len())
	}
	return bestBlock, bestRoot, found
}

// migrateXdcCacheFromCSV imports entries from the legacy CSV file into LevelDB.
// Returns the number of entries migrated. Caller must NOT hold the cache lock.
func migrateXdcCacheFromCSV(db ethdb.Database) int {
	// Determine the legacy CSV path from the database ancient dir (same logic as before)
	var csvPath string
	if dbWithAncient, ok := db.(interface{ AncientDatadir() (string, error) }); ok {
		if ancientDir, err := dbWithAncient.AncientDatadir(); err == nil && ancientDir != "" {
			datadir := filepath.Dir(filepath.Dir(ancientDir))
			csvPath = filepath.Join(datadir, "XDC", "xdc-state-root-cache.csv")
		}
	}
	if csvPath == "" {
		csvPath = filepath.Join("XDC", "xdc-state-root-cache.csv")
	}

	file, err := os.Open(csvPath)
	if err != nil {
		if !os.IsNotExist(err) {
			log.Warn("XDC cache: failed to open legacy CSV for migration", "path", csvPath, "err", err)
		}
		return 0
	}
	defer file.Close()

	reader := csv.NewReader(bufio.NewReader(file))
	records, err := reader.ReadAll()
	if err != nil {
		log.Error("XDC cache: failed to read legacy CSV", "path", csvPath, "err", err)
		return 0
	}

	batch := db.NewBatch()
	migrated := 0
	for i, record := range records {
		if i == 0 && len(record) > 0 && record[0] == "block_number" {
			continue
		}
		if len(record) != 3 {
			continue
		}
		blockNum, err := strconv.ParseUint(record[0], 10, 64)
		if err != nil {
			continue
		}
		remoteRoot := common.HexToHash(record[1])
		localRoot := common.HexToHash(record[2])
		rawdb.WriteXdcLocalStateRoot(batch, blockNum, localRoot)
		rawdb.WriteXdcRemoteToLocal(batch, remoteRoot, localRoot)
		migrated++
		if migrated%10000 == 0 {
			if err := batch.Write(); err != nil {
				log.Error("XDC cache: failed to write migration batch", "err", err)
				return migrated
			}
			batch = db.NewBatch()
		}
	}
	if migrated%10000 != 0 {
		if err := batch.Write(); err != nil {
			log.Error("XDC cache: failed to write final migration batch", "err", err)
			return migrated
		}
	}

	// Rename the CSV so we don't migrate again
	if err := os.Rename(csvPath, csvPath+".migrated"); err != nil {
		log.Warn("XDC cache: failed to rename legacy CSV after migration", "path", csvPath, "err", err)
	}
	return migrated
}

// XdcFlushCache writes the entire memory cache to LevelDB (called on shutdown)
func XdcFlushCache() {
	xdcStateRootCache.Lock()
	defer xdcStateRootCache.Unlock()

	if !xdcStateRootCache.initialized || xdcStateRootCache.db == nil {
		return
	}

	log.Info("XDC cache: flushing to LevelDB on shutdown", "entries", xdcStateRootCache.blockRoots.Len())
	batch := xdcStateRootCache.db.NewBatch()
	count := 0
	for _, blockNum := range xdcStateRootCache.blockRoots.Keys() {
		localRoot, _ := xdcStateRootCache.blockRoots.Peek(blockNum)
		rawdb.WriteXdcLocalStateRoot(batch, blockNum, localRoot)
		if remoteRoot, ok := xdcStateRootCache.blockToRemote.Peek(blockNum); ok {
			rawdb.WriteXdcRemoteToLocal(batch, remoteRoot, localRoot)
		}
		count++
		if count >= 10000 {
			if err := batch.Write(); err != nil {
				log.Error("XDC cache: failed to write flush batch", "err", err)
				return
			}
			batch = xdcStateRootCache.db.NewBatch()
			count = 0
		}
	}
	if count > 0 {
		if err := batch.Write(); err != nil {
			log.Error("XDC cache: failed to write final flush batch", "err", err)
		}
	}
}

// XdcCacheStats returns statistics about the cache
func XdcCacheStats() map[string]interface{} {
	xdcStateRootCache.RLock()
	defer xdcStateRootCache.RUnlock()

	return map[string]interface{}{
		"blockRoots":    xdcStateRootCache.blockRoots.Len(),
		"remoteToLocal": xdcStateRootCache.remoteToLocal.Len(),
	}
}

// evictOldest removes the oldest N entries from the cache
// Caller must hold lock
func evictOldest(count int) {
	if count <= 0 {
		return
	}

	blockNums := xdcStateRootCache.blockRoots.Keys()
	sort.Slice(blockNums, func(i, j int) bool { return blockNums[i] < blockNums[j] })

	evictCount := count
	if evictCount > len(blockNums) {
		evictCount = len(blockNums)
	}

	for i := 0; i < evictCount; i++ {
		blockNum := blockNums[i]
		xdcStateRootCache.blockRoots.Remove(blockNum)

		if remoteRoot, ok := xdcStateRootCache.blockToRemote.Get(blockNum); ok {
			xdcStateRootCache.remoteToLocal.Remove(remoteRoot)
			xdcStateRootCache.blockToRemote.Remove(blockNum)
		}
	}

	log.Debug("XDC cache: evicted old entries", "count", evictCount)
}
