package common

import (
	"bytes"
	"sync"
	"time"

	"git.hush.is/hush/lightwalletd/walletrpc"
	"github.com/golang/protobuf/proto"
)

type BlockCacheEntry struct {
	data []byte
	hash []byte
}

type BlockCache struct {
	MaxEntries int

	FirstBlock int
	LastBlock  int

	m map[int]*BlockCacheEntry

	mutex sync.RWMutex

	// Adaptive confirmation lag. lightwalletd advertises a tip that trails the
	// real tip by a number of blocks that scales with the recent reorg rate, so
	// wallets anchor shielded spends at a settled height while the chain is
	// churning, and at (near) the real tip when it is stable. reorgTimes holds
	// the times of recent reorg detections, pruned to lagWindow.
	reorgTimes  []time.Time
	lagWindow   time.Duration
	lagMin      int
	lagMax      int
	adaptiveLag bool

	// advTip is the cached advertised (lag-adjusted) tip, kept fresh by Add so
	// the per-block serving cap (HeightAllowed) is a cheap read. -1 until ready.
	advTip int
}

func NewBlockCache(maxEntries int) *BlockCache {
	return &BlockCache{
		MaxEntries:  maxEntries,
		FirstBlock:  -1,
		LastBlock:   -1,
		m:           make(map[int]*BlockCacheEntry),
		adaptiveLag: true,
		lagWindow:   30 * time.Minute,
		lagMin:      1,
		lagMax:      12,
		advTip:      -1,
	}
}

func (c *BlockCache) Add(height int, block *walletrpc.CompactBlock) (error, bool) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	//println("Cache add", height)
	if c.FirstBlock == -1 && c.LastBlock == -1 {
		// If this is the first block, prep the data structure
		c.FirstBlock = height
		c.LastBlock = height - 1
	}

	// If we're adding a block in the middle of the cache, remove all
	// blocks after it, since this might be a reorg, and we don't want
	// Any outdated blocks returned
	if height >= c.FirstBlock && height <= c.LastBlock {
		for i := height; i <= c.LastBlock; i++ {
			delete(c.m, i)
		}
		c.LastBlock = height - 1
	}

	// Don't allow out-of-order blocks. This is more of a sanity check than anything
	// If there is a reorg, then the ingestor needs to handle it.
	if c.m[height-1] != nil && !bytes.Equal(block.PrevHash, c.m[height-1].hash) {
		// Record the reorg so the adaptive confirmation lag can react to it.
		c.reorgTimes = append(c.reorgTimes, time.Now())
		return nil, true
	}

	// Add the entry and update the counters
	data, err := proto.Marshal(block)
	if err != nil {
		println("Error marshalling block!")
		return err, false
	}

	c.m[height] = &BlockCacheEntry{
		data: data,
		hash: block.GetHash(),
	}

	c.LastBlock = height

	// If the cache is full, remove the oldest block
	if c.LastBlock-c.FirstBlock+1 > c.MaxEntries {
		//println("Deleteing at height", c.FirstBlock)
		delete(c.m, c.FirstBlock)
		c.FirstBlock = c.FirstBlock + 1
	}

	// Keep the advertised (lag-adjusted) tip fresh for the block-serving cap.
	c.advTip = c.LastBlock - c.computeLagLocked()
	if c.advTip < c.FirstBlock {
		c.advTip = c.FirstBlock
	}

	//println("Cache size is ", len(c.m))
	return nil, false
}

func (c *BlockCache) Get(height int) *walletrpc.CompactBlock {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	//println("Cache get", height)
	if c.LastBlock == -1 || c.FirstBlock == -1 {
		return nil
	}

	if height < c.FirstBlock || height > c.LastBlock {
		//println("Cache miss: index out of range")
		return nil
	}

	//println("Cache returned")
	serialized := &walletrpc.CompactBlock{}
	err := proto.Unmarshal(c.m[height].data, serialized)
	if err != nil {
		println("Error unmarshalling compact block")
		return nil
	}

	return serialized
}

func (c *BlockCache) GetLatestBlock() int {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	return c.LastBlock
}

// ConfigureLag sets the adaptive-confirmation-lag parameters. Called once at
// startup from the command-line flags.
func (c *BlockCache) ConfigureLag(adaptive bool, windowMinutes, min, max int) {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.adaptiveLag = adaptive
	c.lagWindow = time.Duration(windowMinutes) * time.Minute
	c.lagMin = min
	c.lagMax = max
}

// computeLagLocked returns the confirmation lag (in blocks) to apply right now
// and prunes reorg events that have aged out of the window. The lag is the
// configured floor plus the number of reorgs seen within lagWindow, capped at
// lagMax. Caller must hold c.mutex.
func (c *BlockCache) computeLagLocked() int {
	if !c.adaptiveLag {
		return c.lagMin
	}
	cutoff := time.Now().Add(-c.lagWindow)
	kept := c.reorgTimes[:0]
	for _, t := range c.reorgTimes {
		if t.After(cutoff) {
			kept = append(kept, t)
		}
	}
	c.reorgTimes = kept
	lag := c.lagMin + len(kept)
	if lag > c.lagMax {
		lag = c.lagMax
	}
	if lag < c.lagMin {
		lag = c.lagMin
	}
	return lag
}

// CurrentLag returns the confirmation lag currently being applied.
func (c *BlockCache) CurrentLag() int {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	return c.computeLagLocked()
}

// AdvertisedLatestBlock is the tip height lightwalletd reports to wallets: the
// real cache tip minus the adaptive confirmation lag. Wallets sync to and
// anchor shielded spends at this settled height, immune to tip reorgs. The
// internal cache and ingestor continue to track the real tip.
func (c *BlockCache) AdvertisedLatestBlock() int {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	if c.LastBlock < 0 {
		return c.LastBlock
	}
	adv := c.LastBlock - c.computeLagLocked()
	if adv < c.FirstBlock {
		adv = c.FirstBlock
	}
	c.advTip = adv
	return adv
}

// HeightAllowed reports whether height is at or below the advertised
// (lag-adjusted) tip. lightwalletd refuses to serve blocks above it so wallets
// cannot sync or anchor shielded spends into the unstable reorg zone. Cheap
// (read lock, no recompute); advTip is kept fresh by Add.
func (c *BlockCache) HeightAllowed(height int) bool {
	c.mutex.RLock()
	defer c.mutex.RUnlock()
	if c.advTip < 0 {
		return true
	}
	return height <= c.advTip
}
