package blobovniczatree

import (
	"fmt"
	"sync"

	utilSync "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/sync"
	"github.com/hashicorp/golang-lru/v2/simplelru"
)

// dbCache caches sharedDB instances that are NOT open for Put.
//
// Uses dbManager for opening/closing sharedDB instances.
// Stores a reference to an cached sharedDB, so dbManager does not close it.
type dbCache struct {
	cacheGuard *sync.RWMutex
	cache      simplelru.LRUCache[string, *sharedDB]
	pathLock   *utilSync.KeyLocker[string]
	closed     bool

	dbManager *dbManager
}

func newDBCache(size int, dbManager *dbManager) *dbCache {
	cache, err := simplelru.NewLRU[string, *sharedDB](size, func(_ string, evictedDB *sharedDB) {
		evictedDB.Close()
	})
	if err != nil {
		// occurs only if the size is not positive
		panic(fmt.Errorf("could not create LRU cache of size %d: %w", size, err))
	}
	return &dbCache{
		cacheGuard: &sync.RWMutex{},
		cache:      cache,
		dbManager:  dbManager,
		pathLock:   utilSync.NewKeyLocker[string](),
	}
}

func (c *dbCache) Open() {
	c.cacheGuard.Lock()
	defer c.cacheGuard.Unlock()

	c.closed = false
}

func (c *dbCache) Close() {
	c.cacheGuard.Lock()
	defer c.cacheGuard.Unlock()
	c.cache.Purge()
	c.closed = true
}

func (c *dbCache) GetOrCreate(path string) *sharedDB {
	value := c.getExisted(path)
	if value != nil {
		return value
	}
	return c.create(path)
}

func (c *dbCache) getExisted(path string) *sharedDB {
	c.cacheGuard.Lock()
	defer c.cacheGuard.Unlock()

	if value, ok := c.cache.Get(path); ok {
		return value
	}
	return nil
}

func (c *dbCache) create(path string) *sharedDB {
	c.pathLock.Lock(path)
	defer c.pathLock.Unlock(path)

	value := c.getExisted(path)
	if value != nil {
		return value
	}

	value = c.dbManager.GetByPath(path)

	_, err := value.Open() //open db to hold reference, closed by evictedDB.Close() or if cache closed
	if err != nil {
		return value
	}
	if added := c.put(path, value); !added {
		value.Close()
	}
	return value
}

func (c *dbCache) put(path string, db *sharedDB) bool {
	c.cacheGuard.Lock()
	defer c.cacheGuard.Unlock()

	if !c.closed {
		c.cache.Add(path, db)
		return true
	}

	return false
}