package kms

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sync"
	"time"
)

// DEKCache implements an in-process cache for unwrapped DEKs per ADR-012.
// - TTL: 15 minutes per entry
// - Max size: 1000 entries (LRU eviction)
// - Scope: In-process only, never serialized to disk
type DEKCache struct {
	mu           sync.RWMutex
	entries      map[string]*cacheEntry
	ttl          time.Duration
	maxEntries   int
	graceWindow  time.Duration // Per ADR-013
	evictionList []string      // Simple LRU: front = oldest, back = newest
}

// cacheEntry holds a cached DEK with metadata.
type cacheEntry struct {
	dek        []byte
	tenantID   string
	artifactID string
	kmsKeyID   string
	createdAt  time.Time
	lastAccess time.Time
	evicted    bool
}

// NewDEKCache creates a new DEK cache with the specified configuration.
func NewDEKCache(cfg CacheConfig) *DEKCache {
	return &DEKCache{
		entries:      make(map[string]*cacheEntry),
		ttl:          cfg.TTL,
		maxEntries:   cfg.MaxEntries,
		graceWindow:  cfg.GraceWindow,
		evictionList: make([]string, 0),
	}
}

// cacheKey generates a unique key for the cache map including KMS key ID.
func cacheKey(tenantID, artifactID, kmsKeyID string) string {
	// Use a hash to avoid storing raw IDs in the key
	h := sha256.New()
	_, _ = h.Write([]byte(tenantID + "/" + artifactID + "/" + kmsKeyID))
	return hex.EncodeToString(h.Sum(nil))
}

// Get retrieves a DEK from the cache if present and not expired.
// Returns nil and false if not found or expired.
// Per ADR-013: During KMS unavailability, expired entries within grace window are still returned.
func (c *DEKCache) Get(tenantID, artifactID, kmsKeyID string, kmsUnavailable bool) ([]byte, bool) {
	key := cacheKey(tenantID, artifactID, kmsKeyID)

	c.mu.RLock()
	entry, exists := c.entries[key]
	c.mu.RUnlock()

	if !exists || entry.evicted {
		return nil, false
	}

	now := time.Now()
	age := now.Sub(entry.createdAt)

	// Check if expired
	if age > c.ttl {
		// Per ADR-013: Grace window only applies during KMS unavailability
		if !kmsUnavailable {
			return nil, false
		}

		// Check grace window
		graceAge := age - c.ttl
		if graceAge > c.graceWindow {
			return nil, false
		}

		// Within grace window - return the DEK but log that we're using grace period
		// (caller should log this appropriately)
	}

	// Update last access time (need write lock)
	c.mu.Lock()
	entry.lastAccess = now
	c.updateLRU(key)
	c.mu.Unlock()

	// Return a copy of the DEK to prevent external modification
	dekCopy := make([]byte, len(entry.dek))
	copy(dekCopy, entry.dek)
	return dekCopy, true
}

// Put stores a DEK in the cache.
func (c *DEKCache) Put(tenantID, artifactID, kmsKeyID string, dek []byte) error {
	if len(dek) == 0 {
		return fmt.Errorf("cannot cache empty DEK")
	}

	key := cacheKey(tenantID, artifactID, kmsKeyID)

	// Copy the DEK to prevent external modification
	dekCopy := make([]byte, len(dek))
	copy(dekCopy, dek)

	c.mu.Lock()
	defer c.mu.Unlock()

	// Check if we need to evict
	if len(c.entries) >= c.maxEntries {
		c.evictLRU()
	}

	// Store the entry
	now := time.Now()
	c.entries[key] = &cacheEntry{
		dek:        dekCopy,
		tenantID:   tenantID,
		artifactID: artifactID,
		kmsKeyID:   kmsKeyID,
		createdAt:  now,
		lastAccess: now,
	}

	// Add to LRU list (newest at back)
	c.evictionList = append(c.evictionList, key)

	return nil
}

// Flush removes all DEKs for a specific tenant from the cache.
// Called on key rotation events and tenant offboarding per ADR-012.
func (c *DEKCache) Flush(tenantID string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Mark entries for eviction
	for key, entry := range c.entries {
		if entry.tenantID == tenantID {
			entry.evicted = true
			delete(c.entries, key)
		}
	}

	// Rebuild eviction list without flushed entries
	newList := make([]string, 0, len(c.evictionList))
	for _, key := range c.evictionList {
		if entry, exists := c.entries[key]; exists && !entry.evicted {
			newList = append(newList, key)
		}
	}
	c.evictionList = newList
}

// Clear removes all entries from the cache.
func (c *DEKCache) Clear() {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Securely wipe DEK bytes before dropping references
	for _, entry := range c.entries {
		for i := range entry.dek {
			entry.dek[i] = 0
		}
	}

	c.entries = make(map[string]*cacheEntry)
	c.evictionList = make([]string, 0)
}

// Stats returns current cache statistics.
func (c *DEKCache) Stats() CacheStats {
	c.mu.RLock()
	defer c.mu.RUnlock()

	return CacheStats{
		Size:        len(c.entries),
		MaxSize:     c.maxEntries,
		TTL:         c.ttl,
		GraceWindow: c.graceWindow,
	}
}

// CacheStats holds cache statistics.
type CacheStats struct {
	Size        int
	MaxSize     int
	TTL         time.Duration
	GraceWindow time.Duration
}

// updateLRU moves the accessed key to the back of the list (most recently used).
func (c *DEKCache) updateLRU(key string) {
	// Find and remove key from current position
	for i, k := range c.evictionList {
		if k == key {
			// Remove from current position
			c.evictionList = append(c.evictionList[:i], c.evictionList[i+1:]...)
			break
		}
	}
	// Add to back (most recent)
	c.evictionList = append(c.evictionList, key)
}

// evictLRU removes the oldest entry (front of list).
func (c *DEKCache) evictLRU() {
	if len(c.evictionList) == 0 {
		return
	}

	// Remove oldest (front of list)
	oldestKey := c.evictionList[0]
	c.evictionList = c.evictionList[1:]

	// Securely wipe DEK bytes
	if entry, exists := c.entries[oldestKey]; exists {
		for i := range entry.dek {
			entry.dek[i] = 0
		}
		entry.evicted = true
	}

	delete(c.entries, oldestKey)
}

// cleanupExpired periodically removes expired entries.
// This should be called periodically (e.g., by a background goroutine).
func (c *DEKCache) cleanupExpired() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	for key, entry := range c.entries {
		if now.Sub(entry.createdAt) > c.ttl+c.graceWindow {
			// Securely wipe
			for i := range entry.dek {
				entry.dek[i] = 0
			}
			entry.evicted = true
			delete(c.entries, key)
		}
	}

	// Rebuild eviction list
	newList := make([]string, 0, len(c.entries))
	for _, key := range c.evictionList {
		if _, exists := c.entries[key]; exists {
			newList = append(newList, key)
		}
	}
	c.evictionList = newList
}

// StartCleanup starts a background goroutine to periodically clean up expired entries.
func (c *DEKCache) StartCleanup(interval time.Duration) chan struct{} {
	stop := make(chan struct{})
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				c.cleanupExpired()
			case <-stop:
				return
			}
		}
	}()
	return stop
}
