package cache

import (
	"fmt"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	"github.com/bluele/gcache"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"go.uber.org/zap"
)

// FrostfsIDCache provides lru cache for frostfsid contract.
type FrostfsIDCache struct {
	cache  gcache.Cache
	logger *zap.Logger
}

const (
	// DefaultFrostfsIDCacheSize is a default maximum number of entries in cache.
	DefaultFrostfsIDCacheSize = 1e4
	// DefaultFrostfsIDCacheLifetime is a default lifetime of entries in cache.
	DefaultFrostfsIDCacheLifetime = time.Minute
)

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

// NewFrostfsIDCache creates an object of FrostfsIDCache.
func NewFrostfsIDCache(config *Config) *FrostfsIDCache {
	gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
	return &FrostfsIDCache{cache: gc, logger: config.Logger}
}

// GetSubject returns a cached client.SubjectExtended. Returns nil if value is missing.
func (c *FrostfsIDCache) GetSubject(key util.Uint160) *client.SubjectExtended {
	return get[client.SubjectExtended](c, key)
}

// PutSubject puts a client.SubjectExtended to cache.
func (c *FrostfsIDCache) PutSubject(key util.Uint160, subject *client.SubjectExtended) error {
	return c.cache.Set(key, subject)
}

// GetUserKey returns a cached *keys.PublicKey. Returns nil if value is missing.
func (c *FrostfsIDCache) GetUserKey(ns, name string) *keys.PublicKey {
	return get[keys.PublicKey](c, ns+"/"+name)
}

// PutUserKey puts a client.SubjectExtended to cache.
func (c *FrostfsIDCache) PutUserKey(ns, name string, userKey *keys.PublicKey) error {
	return c.cache.Set(ns+"/"+name, userKey)
}

func get[T any](c *FrostfsIDCache, key any) *T {
	entry, err := c.cache.Get(key)
	if err != nil {
		return nil
	}

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

	return result
}