package blobovniczatree

import (
	"context"
	"sync"
	"time"

	utilSync "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/sync"
	cache "github.com/go-pkgz/expirable-cache/v3"
)

// dbCache caches sharedDB instances that are NOT open for Put.
//
// Uses dbManager for opening/closing sharedDB instances.
// Stores a reference to a cached sharedDB, so dbManager does not close it.
type dbCache struct {
	cacheGuard *sync.Mutex
	cache      cache.Cache[string, *sharedDB]
	pathLock   *utilSync.KeyLocker[string] // the order of locks is important: pathLock first, cacheGuard second
	closed     bool
	nonCached  map[string]struct{}
	wg         sync.WaitGroup
	cancel     context.CancelFunc

	dbManager *dbManager
}

func newDBCache(parentCtx context.Context, size int,
	ttl time.Duration, expInterval time.Duration,
	dbManager *dbManager,
) *dbCache {
	ch := cache.NewCache[string, *sharedDB]().
		WithTTL(ttl).WithLRU().WithMaxKeys(size).
		WithOnEvicted(func(_ string, db *sharedDB) {
			db.Close()
		})
	ctx, cancel := context.WithCancel(parentCtx)
	res := &dbCache{
		cacheGuard: &sync.Mutex{},
		wg:         sync.WaitGroup{},
		cancel:     cancel,
		cache:      ch,
		dbManager:  dbManager,
		pathLock:   utilSync.NewKeyLocker[string](),
		nonCached:  make(map[string]struct{}),
	}
	if ttl > 0 {
		res.wg.Add(1)
		go func() {
			ticker := time.NewTicker(expInterval)
			defer ticker.Stop()
			for {
				select {
				case <-ctx.Done():
					res.wg.Done()
					return
				case <-ticker.C:
					res.cacheGuard.Lock()
					res.cache.DeleteExpired()
					res.cacheGuard.Unlock()
				}
			}
		}()
	}
	return res
}

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.cancel()
	c.wg.Wait()
	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) EvictAndMarkNonCached(path string) {
	c.pathLock.Lock(path)
	defer c.pathLock.Unlock(path)

	c.cacheGuard.Lock()
	defer c.cacheGuard.Unlock()

	c.cache.Remove(path)
	c.nonCached[path] = struct{}{}
}

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

	c.cacheGuard.Lock()
	defer c.cacheGuard.Unlock()

	delete(c.nonCached, 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
	} else if value != nil {
		c.cache.Invalidate(path)
	}
	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()

	_, isNonCached := c.nonCached[path]

	if isNonCached || c.closed {
		return false
	}
	c.cache.Add(path, db)
	return true
}