package util

import (
	"sync"
	"time"
)

// LookupCache is a single-value cache with a time-to-live (TTL). The cache has a lookup function
// to retrieve the value and stores it until TTL is reached.
//
// Example:
//
//	lookup := func() (string, error) {
//	   r, _ := http.Get("...")
//	   s, _ := io.ReadAll(r.Body)
//	   return string(s), nil
//	}
//	c := NewLookupCache[string](lookup, time.Hour)
//	fmt.Println(c.Get()) // Fetches the string via HTTP
//	fmt.Println(c.Get()) // Uses cached value
type LookupCache[T any] struct {
	value   *T
	lookup  func() (T, error)
	ttl     time.Duration
	updated time.Time
	mu      sync.Mutex
}

// LookupFunc is a function that is called by the LookupCache if the underlying
// value is out-of-date. It returns the new value, or an error.
type LookupFunc[T any] func() (T, error)

// NewLookupCache creates a new LookupCache with a given time-to-live (TTL)
func NewLookupCache[T any](lookup LookupFunc[T], ttl time.Duration) *LookupCache[T] {
	return &LookupCache[T]{
		value:  nil,
		lookup: lookup,
		ttl:    ttl,
	}
}

// Value returns the cached value, or retrieves it via the lookup function
func (c *LookupCache[T]) Value() (T, error) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.value == nil || (c.ttl > 0 && time.Since(c.updated) > c.ttl) {
		value, err := c.lookup()
		if err != nil {
			var t T
			return t, err
		}
		c.value = &value
		c.updated = time.Now()
	}
	return *c.value, nil
}