package core

import (
	"fmt"
	"slices"
	"sync"

	lru "github.com/hashicorp/golang-lru/v2"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/block"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/dao"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
)

const (
	headerBatchCount = 2000
	pagesCache       = 8
)

// HeaderHashes is a header hash manager part of the Blockchain. It can't be used
// without Blockchain.
type HeaderHashes struct {
	// Backing storage.
	dao *dao.Simple

	// Lock for all internal state fields.
	lock sync.RWMutex

	// The latest header hashes (storedHeaderCount+).
	latest []util.Uint256

	// Previously completed page of header hashes (pre-storedHeaderCount).
	previous []util.Uint256

	// Number of headers stored in the chain file.
	storedHeaderCount uint32

	// Cache for accessed pages of header hashes.
	cache *lru.Cache[uint32, []util.Uint256]
}

func (h *HeaderHashes) initMinTrustedHeader(dao *dao.Simple, trusted config.HashIndex) {
	h.dao = dao
	h.cache, _ = lru.New[uint32, []util.Uint256](pagesCache) // Never errors for positive size.
	h.previous = make([]util.Uint256, headerBatchCount)
	h.latest = make([]util.Uint256, 0, headerBatchCount)

	// For non-genesis block, trusted header is not yet in the storage. Use a
	// stub and pretend that the latest header is `trusted.Index-1` to fetch
	// the next one (trusted) from the network.
	if trusted.Index > 0 {
		trusted.Index--
		trusted.Hash = util.Uint256{}
	}

	for range trusted.Index % headerBatchCount {
		h.latest = append(h.latest, util.Uint256{})
	}
	h.latest = append(h.latest, trusted.Hash)
	dao.PutCurrentHeader(trusted.Hash, trusted.Index)
	h.storedHeaderCount = (trusted.Index /*trusted header is not yet in the storage*/ / headerBatchCount) * headerBatchCount

	// Store trusted header if it's the last header in the batch and update storedHeaderCount.
	_ = h.tryStoreBatch(dao) // ignore serialization error.

	updateHeaderHeightMetric(trusted.Index)
}

func (h *HeaderHashes) init(dao *dao.Simple, trusted config.HashIndex) error {
	currHeaderHeight, currHeaderHash, err := dao.GetCurrentHeaderHeight()
	if err != nil {
		return fmt.Errorf("failed to retrieve current header info: %w", err)
	}
	if currHeaderHeight < trusted.Index {
		h.initMinTrustedHeader(dao, trusted)
		return nil
	}

	h.dao = dao
	h.cache, _ = lru.New[uint32, []util.Uint256](pagesCache) // Never errors for positive size.
	h.storedHeaderCount = ((currHeaderHeight + 1) / headerBatchCount) * headerBatchCount
	missingHeaderCount := ((trusted.Index + 1) / headerBatchCount) * headerBatchCount
	if h.storedHeaderCount >= headerBatchCount &&
		((h.storedHeaderCount > missingHeaderCount && h.storedHeaderCount-missingHeaderCount >= headerBatchCount) ||
			currHeaderHeight%headerBatchCount != trusted.Index%headerBatchCount) {
		h.previous, err = h.dao.GetHeaderHashes(h.storedHeaderCount - headerBatchCount)
		if err != nil {
			return fmt.Errorf("failed to retrieve header hash page %d: %w; stored: %d, missing: %d, trusted: %d, curr: %d", h.storedHeaderCount-headerBatchCount, err, h.storedHeaderCount, missingHeaderCount, trusted.Index, currHeaderHeight)
		}
	} else {
		h.previous = make([]util.Uint256, headerBatchCount)
	}
	h.latest = make([]util.Uint256, 0, headerBatchCount)

	// There is a high chance that the Node is stopped before the next
	// batch of 2000 headers was stored. Via the currentHeaders stored we can sync
	// that with stored blocks.
	if currHeaderHeight >= h.storedHeaderCount {
		var (
			hash       = currHeaderHash
			targetHash util.Uint256
			padLeft    bool
		)
		if h.storedHeaderCount >= headerBatchCount {
			targetHash = h.previous[len(h.previous)-1]
		}
		if targetHash.Equals(util.Uint256{}) && trusted.Index > 0 {
			// Don't retrieve header hashes prior to trusted header (if set) since sometimes
			// these blocks may be missing from the storage (if previously node was started
			// from existing DB with some trusted point higher than header height). These hashes
			// are useless for further node operation anyway.
			targetHash = trusted.Hash
			padLeft = true
		}
		headers := make([]util.Uint256, 0, headerBatchCount)

		for hash != targetHash {
			blk, err := h.dao.GetBlock(hash)
			if err != nil {
				return fmt.Errorf("could not get header %s: %w", hash.StringLE(), err)
			}
			headers = append(headers, blk.Hash())
			hash = blk.PrevHash
		}
		slices.Reverse(headers)
		if padLeft {
			h.latest = h.latest[:currHeaderHeight-uint32(len(headers))]
			h.latest = append(h.latest, trusted.Hash)
		}
		h.latest = append(h.latest, headers...)
	}
	return nil
}

func (h *HeaderHashes) lastHeaderIndex() uint32 {
	return h.storedHeaderCount + uint32(len(h.latest)) - 1
}

// HeaderHeight returns the index/height of the highest header.
func (h *HeaderHashes) HeaderHeight() uint32 {
	h.lock.RLock()
	n := h.lastHeaderIndex()
	h.lock.RUnlock()
	return n
}

func (h *HeaderHashes) addHeaders(headers ...*block.Header) error {
	var (
		batch      = h.dao.GetPrivate()
		lastHeader *block.Header
		err        error
	)

	h.lock.Lock()
	defer h.lock.Unlock()

	for _, head := range headers {
		if head.Index != h.lastHeaderIndex()+1 {
			continue
		}
		err = batch.StoreHeader(head)
		if err != nil {
			return err
		}
		lastHeader = head
		h.latest = append(h.latest, head.Hash())
		err = h.tryStoreBatch(batch)
		if err != nil {
			return fmt.Errorf("failed to store batch of header hashes: %w", err)
		}
	}
	if lastHeader != nil {
		batch.PutCurrentHeader(lastHeader.Hash(), lastHeader.Index)
		updateHeaderHeightMetric(lastHeader.Index)
		if _, err = batch.Persist(); err != nil {
			return err
		}
	}
	return nil
}

// tryStoreBatch stores the current batch of header hashes to the storage in
// case if batch is full. It also initializes HeaderHashes for the next batch
// processing. It does not persist dao.
func (h *HeaderHashes) tryStoreBatch(d *dao.Simple) error {
	if len(h.latest) == headerBatchCount {
		err := d.StoreHeaderHashes(h.latest, h.storedHeaderCount)
		if err != nil {
			return err
		}
		copy(h.previous, h.latest)
		h.latest = h.latest[:0]
		h.storedHeaderCount += headerBatchCount
	}
	return nil
}

// CurrentHeaderHash returns the hash of the latest known header.
func (h *HeaderHashes) CurrentHeaderHash() util.Uint256 {
	var hash util.Uint256

	h.lock.RLock()
	if len(h.latest) > 0 {
		hash = h.latest[len(h.latest)-1]
	} else {
		hash = h.previous[len(h.previous)-1]
	}
	h.lock.RUnlock()
	return hash
}

// GetHeaderHash returns hash of the header/block with specified index, if
// HeaderHashes doesn't have a hash for this height, zero Uint256 value is returned.
func (h *HeaderHashes) GetHeaderHash(i uint32) util.Uint256 {
	h.lock.RLock()
	res, ok := h.getLocalHeaderHash(i)
	h.lock.RUnlock()
	if ok {
		return res
	}
	// If it's not in the latest/previous, then it's in the cache or DB, those
	// need no additional locks.
	page := (i / headerBatchCount) * headerBatchCount
	hashes, ok := h.cache.Get(page)
	if ok {
		return hashes[i-page]
	}
	hashes, err := h.dao.GetHeaderHashes(page)
	if err != nil {
		return util.Uint256{}
	}
	_ = h.cache.Add(page, hashes)
	return hashes[i-page]
}

// getLocalHeaderHash looks for the index in the latest and previous caches.
// Locking is left to the user.
func (h *HeaderHashes) getLocalHeaderHash(i uint32) (util.Uint256, bool) {
	if i > h.lastHeaderIndex() {
		return util.Uint256{}, false
	}
	if i >= h.storedHeaderCount {
		return h.latest[i-h.storedHeaderCount], true
	}
	previousStored := h.storedHeaderCount - headerBatchCount
	if i >= previousStored {
		return h.previous[i-previousStored], true
	}
	return util.Uint256{}, false
}

func (h *HeaderHashes) haveRecentHash(hash util.Uint256, i uint32) bool {
	h.lock.RLock()
	defer h.lock.RUnlock()
	for ; i > 0; i-- {
		lh, ok := h.getLocalHeaderHash(i)
		if ok && hash.Equals(lh) {
			return true
		}
	}
	return false
}
