package routing

import (
	"context"
	"sync"
	"time"

	"go.uber.org/zap"
)

// RuleCache is an in-memory, per-tenant cache of routing rules backed by a RuleStore.
// It performs lazy loading on first access and refreshes all loaded tenants every TTL.
// Mutations (Create/Update/Delete via admin API) should call Invalidate() for immediate
// consistency without waiting for the next refresh cycle.
type RuleCache struct {
	mu      sync.RWMutex
	entries map[string]cacheEntry // tenantID → cached rules

	store  RuleStore
	ttl    time.Duration
	stopCh chan struct{}
	logger *zap.Logger
}

type cacheEntry struct {
	rules     []RoutingRule
	loadedAt  time.Time
}

// NewRuleCache creates a RuleCache wrapping store with the given TTL.
func NewRuleCache(store RuleStore, ttl time.Duration, logger *zap.Logger) *RuleCache {
	if ttl <= 0 {
		ttl = 30 * time.Second
	}
	return &RuleCache{
		entries: make(map[string]cacheEntry),
		store:   store,
		ttl:     ttl,
		stopCh:  make(chan struct{}),
		logger:  logger,
	}
}

// Start launches the background refresh goroutine.
// Call Stop() to shut it down gracefully.
func (c *RuleCache) Start() {
	go c.refreshLoop()
}

// Stop signals the background goroutine to stop.
func (c *RuleCache) Stop() {
	close(c.stopCh)
}

// Get returns active rules for tenantID, loading from the store on cache miss
// or when the entry is older than the TTL.
func (c *RuleCache) Get(ctx context.Context, tenantID string) ([]RoutingRule, error) {
	c.mu.RLock()
	entry, ok := c.entries[tenantID]
	c.mu.RUnlock()

	if ok && time.Since(entry.loadedAt) < c.ttl {
		return entry.rules, nil
	}
	return c.load(ctx, tenantID)
}

// Invalidate removes the cached rules for tenantID, forcing a reload on next Get().
// Call this after any Create/Update/Delete operation on routing rules.
func (c *RuleCache) Invalidate(tenantID string) {
	c.mu.Lock()
	delete(c.entries, tenantID)
	c.mu.Unlock()
}

// load fetches rules from the store and updates the cache entry.
func (c *RuleCache) load(ctx context.Context, tenantID string) ([]RoutingRule, error) {
	rules, err := c.store.ListActive(ctx, tenantID)
	if err != nil {
		c.logger.Error("rule cache: failed to load rules", zap.String("tenant_id", tenantID), zap.Error(err))
		return nil, err
	}

	c.mu.Lock()
	c.entries[tenantID] = cacheEntry{rules: rules, loadedAt: time.Now()}
	c.mu.Unlock()

	c.logger.Debug("rule cache: loaded rules", zap.String("tenant_id", tenantID), zap.Int("count", len(rules)))
	return rules, nil
}

// refreshLoop periodically reloads all cached tenants.
func (c *RuleCache) refreshLoop() {
	ticker := time.NewTicker(c.ttl)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			c.refreshAll()
		case <-c.stopCh:
			return
		}
	}
}

func (c *RuleCache) refreshAll() {
	c.mu.RLock()
	tenants := make([]string, 0, len(c.entries))
	for tid := range c.entries {
		tenants = append(tenants, tid)
	}
	c.mu.RUnlock()

	for _, tid := range tenants {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		_, _ = c.load(ctx, tid) //nolint:errcheck — logged inside load()
		cancel()
	}
}
