package cache

import (
	"fmt"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"github.com/bluele/gcache"
	"go.uber.org/zap"
)

type (
	// ListSessionCache contains cache for list session (during pagination).
	ListSessionCache struct {
		cache  gcache.Cache
		logger *zap.Logger
	}

	// ListSessionKey is a key to find a ListSessionCache's entry.
	ListSessionKey struct {
		cid    cid.ID
		prefix string
		token  string
	}
)

const (
	// DefaultListSessionCacheLifetime is a default lifetime of entries in cache of ListObjects.
	DefaultListSessionCacheLifetime = time.Second * 60
	// DefaultListSessionCacheSize is a default size of cache of ListObjects.
	DefaultListSessionCacheSize = 100
)

// DefaultListSessionConfig returns new default cache expiration values.
func DefaultListSessionConfig(logger *zap.Logger) *Config {
	return &Config{
		Size:     DefaultListSessionCacheSize,
		Lifetime: DefaultListSessionCacheLifetime,
		Logger:   logger,
	}
}

func (k *ListSessionKey) String() string {
	return k.cid.EncodeToString() + k.prefix + k.token
}

// NewListSessionCache is a constructor which creates an object of ListObjectsCache with the given lifetime of entries.
func NewListSessionCache(config *Config) *ListSessionCache {
	gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).EvictedFunc(func(_ interface{}, val interface{}) {
		session, ok := val.(*data.ListSession)
		if !ok {
			config.Logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", val)),
				zap.String("expected", fmt.Sprintf("%T", session)))
		}

		if !session.Acquired.Load() {
			session.Cancel()
		}
	}).Build()
	return &ListSessionCache{cache: gc, logger: config.Logger}
}

// GetListSession returns a list of ObjectInfo.
func (l *ListSessionCache) GetListSession(key ListSessionKey) *data.ListSession {
	entry, err := l.cache.Get(key)
	if err != nil {
		return nil
	}

	result, ok := entry.(*data.ListSession)
	if !ok {
		l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
			zap.String("expected", fmt.Sprintf("%T", result)))
		return nil
	}

	return result
}

// PutListSession puts a list of object versions to cache.
func (l *ListSessionCache) PutListSession(key ListSessionKey, session *data.ListSession) error {
	s := l.GetListSession(key)
	if s != nil && s != session {
		if !s.Acquired.Load() {
			s.Cancel()
		}
	}
	return l.cache.Set(key, session)
}

// DeleteListSession removes key from cache.
func (l *ListSessionCache) DeleteListSession(key ListSessionKey) {
	l.cache.Remove(key)
}

// CreateListSessionCacheKey returns ListSessionKey with the given CID, prefix and token.
func CreateListSessionCacheKey(cnr cid.ID, prefix, token string) ListSessionKey {
	p := ListSessionKey{
		cid:    cnr,
		prefix: prefix,
		token:  token,
	}

	return p
}