package v1

import (
	"crypto/x509"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"
)

type cacheEntry struct {
	err       error // if nil, the certificate is not revocated
	timestamp time.Time
}

type RevocationCache struct {
	mu         sync.RWMutex
	cache      map[string]cacheEntry
	expiration time.Duration
	lastPurge  time.Time
	logger     *log.Entry
}

func NewRevocationCache(expiration time.Duration, logger *log.Entry) *RevocationCache {
	return &RevocationCache{
		cache:      make(map[string]cacheEntry),
		expiration: expiration,
		lastPurge:  time.Now(),
		logger:     logger,
	}
}

func (*RevocationCache) generateKey(cert *x509.Certificate) string {
	return cert.SerialNumber.String() + "-" + cert.Issuer.String()
}

// purge removes expired entries from the cache
func (rc *RevocationCache) purgeExpired() {
	// we don't keep a separate interval for the full sweep, we'll just double the expiration
	if time.Since(rc.lastPurge) < rc.expiration {
		return
	}

	rc.mu.Lock()
	defer rc.mu.Unlock()

	for key, entry := range rc.cache {
		if time.Since(entry.timestamp) > rc.expiration {
			rc.logger.Debugf("purging expired entry for cert %s", key)
			delete(rc.cache, key)
		}
	}
}

//revive:disable-next-line:error-return
func (rc *RevocationCache) Get(cert *x509.Certificate) (error, bool) {
	rc.purgeExpired()
	key := rc.generateKey(cert)
	rc.mu.RLock()
	entry, exists := rc.cache[key]
	rc.mu.RUnlock()

	if !exists {
		rc.logger.Tracef("no cached value for cert %s", key)
		return nil, false
	}

	// Upgrade to write lock to potentially modify the cache
	rc.mu.Lock()
	defer rc.mu.Unlock()

	if entry.timestamp.Add(rc.expiration).Before(time.Now()) {
		rc.logger.Debugf("cached value for %s expired, removing from cache", key)
		delete(rc.cache, key)

		return nil, false
	}

	rc.logger.Debugf("using cached value for cert %s: %v", key, entry.err)

	return entry.err, true
}

func (rc *RevocationCache) Set(cert *x509.Certificate, err error) {
	key := rc.generateKey(cert)

	rc.mu.Lock()
	defer rc.mu.Unlock()

	rc.cache[key] = cacheEntry{
		err:       err,
		timestamp: time.Now(),
	}
}

func (rc *RevocationCache) Empty() {
	rc.mu.Lock()
	defer rc.mu.Unlock()
	rc.cache = make(map[string]cacheEntry)
}
