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"
)

// BucketCache contains cache with objects and the lifetime of cache entries.
type BucketCache struct {
	cache    gcache.Cache
	cidCache gcache.Cache
	logger   *zap.Logger
}

const (
	// DefaultBucketCacheSize is a default maximum number of entries in cache.
	DefaultBucketCacheSize = 1e3
	// DefaultBucketCacheLifetime is a default lifetime of entries in cache.
	DefaultBucketCacheLifetime = time.Minute
)

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

// NewBucketCache creates an object of BucketCache.
func NewBucketCache(config *Config, cidCache bool) *BucketCache {
	cache := &BucketCache{
		cache:  gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build(),
		logger: config.Logger,
	}

	if cidCache {
		cache.cidCache = gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
	}
	return cache
}

// Get returns a cached object.
func (o *BucketCache) Get(ns, bktName string) *data.BucketInfo {
	return o.get(formKey(ns, bktName))
}

func (o *BucketCache) GetByCID(cnrID cid.ID) *data.BucketInfo {
	if o.cidCache == nil {
		return nil
	}

	entry, err := o.cidCache.Get(cnrID)
	if err != nil {
		return nil
	}

	key, ok := entry.(string)
	if !ok {
		o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
			zap.String("expected", fmt.Sprintf("%T", key)), logs.TagField(logs.TagDatapath))
		return nil
	}

	return o.get(key)
}

func (o *BucketCache) get(key string) *data.BucketInfo {
	entry, err := o.cache.Get(key)
	if err != nil {
		return nil
	}

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

	return result
}

// Put puts an object to cache.
func (o *BucketCache) Put(bkt *data.BucketInfo) error {
	if o.cidCache != nil {
		if err := o.cidCache.Set(bkt.CID, formKey(bkt.Zone, bkt.Name)); err != nil {
			return err
		}
	}

	return o.cache.Set(formKey(bkt.Zone, bkt.Name), bkt)
}

// Delete deletes an object from cache.
func (o *BucketCache) Delete(bkt *data.BucketInfo) bool {
	if o.cidCache != nil {
		o.cidCache.Remove(bkt.CID)
	}

	return o.cache.Remove(formKey(bkt.Zone, bkt.Name))
}

func formKey(zone, name string) string {
	return name + "." + zone
}