package endpointcache

import (
	"dataservices/commons/metrics"
	"sync"
	"time"
)

var globalCacheLimiter *CacheLimiter

const (
	defaultCacheSizeLimitPerDataappId = 20 * 1024 * 1024 // 20MB
	defaultCacheSizeReportInterval    = 15 * time.Second
)

func init() {
	globalCacheLimiter = NewCacheLimiter(defaultCacheSizeLimitPerDataappId)
	go globalCacheLimiter.reportCacheSizePeriodically()
}

func GlobalCacheLimiter() *CacheLimiter {
	return globalCacheLimiter
}

type CacheItem struct {
	ExternalAppId string
	WrittenTime   time.Time
	WrittenBytes  int
	TTL           int
}

type CacheLimiter struct {
	mu                       sync.RWMutex
	items                    []*CacheItem
	cacheSizePerDataappId    map[string]int
	lastCompactTime          time.Time
	maxCacheSizePerDataappId int
}

func NewCacheLimiter(maxCacheSizePerDataappId int) *CacheLimiter {
	return &CacheLimiter{
		mu:                       sync.RWMutex{},
		items:                    make([]*CacheItem, 0, 10),
		maxCacheSizePerDataappId: maxCacheSizePerDataappId,
	}
}

func (l *CacheLimiter) PutItem(item *CacheItem) bool {
	l.mu.Lock()
	defer l.mu.Unlock()
	l.compactAndRecalculate(item.WrittenTime)
	if l.cacheSizePerDataappId[item.ExternalAppId]+item.WrittenBytes > l.maxCacheSizePerDataappId {
		return false
	}
	l.cacheSizePerDataappId[item.ExternalAppId] += item.WrittenBytes
	l.items = append(l.items, item)
	return true
}

// compact avoid too much items
func (l *CacheLimiter) compactAndRecalculate(now time.Time) {
	newItems := make([]*CacheItem, 0, 10)
	cacheSizePerDataappId := make(map[string]int)
	for _, item := range l.items {
		if item.WrittenTime.Add(time.Duration(item.TTL)*time.Second).Sub(now) > 0 {
			newItems = append(newItems, item)
			cacheSizePerDataappId[item.ExternalAppId] += item.WrittenBytes
		}
	}
	l.items = newItems
	l.cacheSizePerDataappId = cacheSizePerDataappId
	l.lastCompactTime = now
}

func (l *CacheLimiter) reportCacheSizePeriodically() {
	ticker := time.NewTicker(defaultCacheSizeReportInterval)
	for {
		<-ticker.C
		l.reportCacheSize()
	}
}

func (l *CacheLimiter) reportCacheSize() {
	l.mu.RLock()
	defer l.mu.RUnlock()
	for externalAppId, size := range l.cacheSizePerDataappId {
		metrics.CacheSizeGuage.WithLabelValues(externalAppId).Set(float64(size))
	}
}
