package cache

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/go-logr/logr"
	"github.com/prometheus/client_golang/prometheus"

	"github.com/tidbcloud/infra-provider/quota-exporter/config"
	"github.com/tidbcloud/infra-provider/quota-exporter/exporter"
)

// Cache is a helper to cache metrics for the exporter.
type Cache struct {
	source exporter.Exporter
	config *config.CacheConfig
	logger logr.Logger
	ctx    context.Context

	mu          sync.RWMutex
	lastMetrics []prometheus.Metric
}

func NewAndRun(
	ctx context.Context,
	source exporter.Exporter,
	cfg *config.CacheConfig,
	logger logr.Logger,
) (*Cache, error) {
	if source == nil {
		return nil, fmt.Errorf("source exporter is nil")
	}

	err := cfg.Complete()
	if err != nil {
		return nil, err
	}

	cache := &Cache{
		source: source,
		config: cfg,
		ctx:    ctx,
		logger: logger.WithName("Cache").WithValues("source", source.Name()),
	}

	cache.logger.Info("using cache, populating metrics firstly")
	cache.populate()
	cache.logger.Info("metrics populated, started cache loop in background")
	go cache.populateLoop()
	return cache, nil
}

func (e *Cache) Name() string {
	return e.source.Name()
}

func (e *Cache) MetricPath() string {
	return e.source.MetricPath()
}

func (e *Cache) Describe(ch chan<- *prometheus.Desc) {
	e.source.Describe(ch)
}

func (e *Cache) Collect(ch chan<- prometheus.Metric) {
	e.collect(ch)
}

func (e *Cache) populateLoop() {
	e.logger.Info("starting cache")
	ticker := time.NewTicker(e.config.TTL.Duration)
	for {
		select {
		case <-ticker.C:
			e.populate()
		case <-e.ctx.Done():
			ticker.Stop()
			e.logger.Info("stopped cache")
			return
		}
	}
}

func (e *Cache) populate() {
	e.logger.Info("populating new metrics to cache")
	cacheCh := make(chan prometheus.Metric)
	go func() {
		e.source.Collect(cacheCh)
		close(cacheCh)
	}()

	var metrics []prometheus.Metric
	for metric := range cacheCh {
		metrics = append(metrics, metric)
	}

	e.mu.Lock()
	defer e.mu.Unlock()
	e.lastMetrics = metrics
	e.logger.Info("populated new metrics to cache", "metrics-count", len(e.lastMetrics))
}

func (e *Cache) collect(ch chan<- prometheus.Metric) {
	e.mu.RLock()
	defer e.mu.RUnlock()
	for _, metric := range e.lastMetrics {
		ch <- metric
	}
	e.logger.V(4).Info("returned cached metrics")
}
