package das

import (
	"bytes"

	"github.com/OffchainLabs/prysm/v7/beacon-chain/db/filesystem"
	fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	"github.com/pkg/errors"
)

var errIndexOutOfBounds = errors.New("sidecar.index > MAX_BLOBS_PER_BLOCK")

// cacheKey includes the slot so that we can easily iterate through the cache and compare
// slots for eviction purposes. Whether the input is the block or the sidecar, we always have
// the root+slot when interacting with the cache, so it isn't an inconvenience to use both.
type cacheKey struct {
	slot primitives.Slot
	root [fieldparams.RootLength]byte
}

type blobCache struct {
	entries map[cacheKey]*blobCacheEntry
}

func newBlobCache() *blobCache {
	return &blobCache{entries: make(map[cacheKey]*blobCacheEntry)}
}

// keyFromSidecar is a convenience method for constructing a cacheKey from a BlobSidecar value.
func keyFromSidecar(sc blocks.ROBlob) cacheKey {
	return cacheKey{slot: sc.Slot(), root: sc.BlockRoot()}
}

// keyFromBlock is a convenience method for constructing a cacheKey from a ROBlock value.
func keyFromBlock(b blocks.ROBlock) cacheKey {
	return cacheKey{slot: b.Block().Slot(), root: b.Root()}
}

// ensure returns the entry for the given key, creating it if it isn't already present.
func (c *blobCache) ensure(key cacheKey) *blobCacheEntry {
	e, ok := c.entries[key]
	if !ok {
		e = &blobCacheEntry{}
		c.entries[key] = e
	}
	return e
}

// delete removes the cache entry from the cache.
func (c *blobCache) delete(key cacheKey) {
	delete(c.entries, key)
}

// blobCacheEntry holds a fixed-length cache of BlobSidecars.
type blobCacheEntry struct {
	scs         []*blocks.ROBlob
	diskSummary filesystem.BlobStorageSummary
}

func (e *blobCacheEntry) setDiskSummary(sum filesystem.BlobStorageSummary) {
	e.diskSummary = sum
}

// stash adds an item to the in-memory cache of BlobSidecars.
// Only the first BlobSidecar of a given Index will be kept in the cache.
// stash will return an error if the given blob is already in the cache, or if the Index is out of bounds.
func (e *blobCacheEntry) stash(sc *blocks.ROBlob) error {
	maxBlobsPerBlock := params.BeaconConfig().MaxBlobsPerBlock(sc.Slot())
	if sc.Index >= uint64(maxBlobsPerBlock) {
		return errors.Wrapf(errIndexOutOfBounds, "index=%d", sc.Index)
	}
	if e.scs == nil {
		e.scs = make([]*blocks.ROBlob, maxBlobsPerBlock)
	}
	if e.scs[sc.Index] != nil {
		return errors.Wrapf(errDuplicateSidecar, "root=%#x, index=%d, commitment=%#x", sc.BlockRoot(), sc.Index, sc.KzgCommitment)
	}
	e.scs[sc.Index] = sc
	return nil
}

// filter evicts sidecars that are not committed to by the block and returns custom
// errors if the cache is missing any of the commitments, or if the commitments in
// the cache do not match those found in the block. If err is nil, then all expected
// commitments were found in the cache and the sidecar slice return value can be used
// to perform a DA check against the cached sidecars.
// filter only returns blobs that need to be checked. Blobs already available on disk will be excluded.
func (e *blobCacheEntry) filter(root [32]byte, kc [][]byte) ([]blocks.ROBlob, error) {
	count := len(kc)
	if e.diskSummary.AllAvailable(count) {
		return nil, nil
	}
	scs := make([]blocks.ROBlob, 0, count)
	for i := uint64(0); i < uint64(count); i++ {
		// We already have this blob, we don't need to write it or validate it.
		if e.diskSummary.HasIndex(i) {
			continue
		}
		// Check if e.scs has this index before accessing
		var sidecar *blocks.ROBlob
		if i < uint64(len(e.scs)) {
			sidecar = e.scs[i]
		}

		if kc[i] == nil {
			if sidecar != nil {
				return nil, errors.Wrapf(errCommitmentMismatch, "root=%#x, index=%#x, commitment=%#x, no block commitment", root, i, sidecar.KzgCommitment)
			}
			continue
		}

		if sidecar == nil {
			return nil, errors.Wrapf(errMissingSidecar, "root=%#x, index=%#x", root, i)
		}
		if !bytes.Equal(kc[i], sidecar.KzgCommitment) {
			return nil, errors.Wrapf(errCommitmentMismatch, "root=%#x, index=%#x, commitment=%#x, block commitment=%#x", root, i, sidecar.KzgCommitment, kc[i])
		}
		scs = append(scs, *sidecar)
	}

	return scs, nil
}
