package cache

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"
)

// CacheKey represents a cache key for an activity result
type CacheKey struct {
	ActivityType string // "implementer", "judge", "planner"
	TaskID       string
	InputHash    string // MD5 hash of input
	ModelID      string // LLM model used
}

// String returns a string representation of the cache key
func (ck *CacheKey) String() string {
	return fmt.Sprintf("%s:%s:%s:%s", ck.ActivityType, ck.TaskID, ck.InputHash, ck.ModelID)
}

// CacheEntry represents a cached activity result
type CacheEntry struct {
	Key       CacheKey              `json:"key"`
	Result    map[string]interface{} `json:"result"`
	CreatedAt time.Time             `json:"created_at"`
	HitCount  int                   `json:"hit_count"`
	Metadata  map[string]interface{} `json:"metadata,omitempty"`
}

// ResultCache caches activity results to avoid redundant computations
type ResultCache struct {
	mu        sync.RWMutex
	basePath  string
	cache     map[string]*CacheEntry
	maxSize   int
	ttl       time.Duration
}

// NewResultCache creates a new result cache
func NewResultCache(basePath string, maxSize int, ttl time.Duration) *ResultCache {
	return &ResultCache{
		basePath: basePath,
		cache:    make(map[string]*CacheEntry),
		maxSize:  maxSize,
		ttl:      ttl,
	}
}

// ComputeHash computes a hash of the input data
func ComputeHash(data interface{}) (string, error) {
	jsonData, err := json.Marshal(data)
	if err != nil {
		return "", err
	}

	hash := md5.Sum(jsonData)
	return fmt.Sprintf("%x", hash), nil
}

// Set stores a result in the cache
func (rc *ResultCache) Set(key *CacheKey, result map[string]interface{}) error {
	if key == nil {
		return fmt.Errorf("cache key cannot be nil")
	}

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

	keyStr := key.String()

	entry := &CacheEntry{
		Key:       *key,
		Result:    result,
		CreatedAt: time.Now(),
		Metadata:  make(map[string]interface{}),
	}

	// Check size limit
	if len(rc.cache) >= rc.maxSize && rc.cache[keyStr] == nil {
		// Evict oldest entry (simple FIFO)
		var oldestKey string
		var oldestTime time.Time

		for k, v := range rc.cache {
			if oldestTime.IsZero() || v.CreatedAt.Before(oldestTime) {
				oldestKey = k
				oldestTime = v.CreatedAt
			}
		}

		if oldestKey != "" {
			delete(rc.cache, oldestKey)
		}
	}

	rc.cache[keyStr] = entry
	return rc.persistLocked(keyStr, entry)
}

// Get retrieves a result from the cache
func (rc *ResultCache) Get(key *CacheKey) (map[string]interface{}, bool, error) {
	if key == nil {
		return nil, false, fmt.Errorf("cache key cannot be nil")
	}

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

	keyStr := key.String()
	entry, exists := rc.cache[keyStr]

	if !exists {
		return nil, false, nil
	}

	// Check TTL
	if rc.ttl > 0 && time.Since(entry.CreatedAt) > rc.ttl {
		delete(rc.cache, keyStr)
		return nil, false, nil
	}

	// Increment hit count
	entry.HitCount++
	_ = rc.persistLocked(keyStr, entry)

	return entry.Result, true, nil
}

// Invalidate removes a cache entry
func (rc *ResultCache) Invalidate(key *CacheKey) error {
	if key == nil {
		return fmt.Errorf("cache key cannot be nil")
	}

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

	keyStr := key.String()
	delete(rc.cache, keyStr)

	// Delete from disk
	cacheFile := filepath.Join(rc.basePath, "cache", fmt.Sprintf("%s.json", keyStr))
	_ = os.Remove(cacheFile)

	return nil
}

// Clear clears all cache entries
func (rc *ResultCache) Clear() error {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	rc.cache = make(map[string]*CacheEntry)

	// Clear disk cache
	cacheDir := filepath.Join(rc.basePath, "cache")
	_ = os.RemoveAll(cacheDir)

	return nil
}

// GetStats returns cache statistics
func (rc *ResultCache) GetStats() map[string]interface{} {
	rc.mu.RLock()
	defer rc.mu.RUnlock()

	totalHits := 0
	for _, entry := range rc.cache {
		totalHits += entry.HitCount
	}

	return map[string]interface{}{
		"size":        len(rc.cache),
		"max_size":    rc.maxSize,
		"total_hits":  totalHits,
		"usage_ratio": float64(len(rc.cache)) / float64(rc.maxSize),
	}
}

// GetSize returns the current cache size
func (rc *ResultCache) GetSize() int {
	rc.mu.RLock()
	defer rc.mu.RUnlock()

	return len(rc.cache)
}

// persistLocked saves a cache entry to disk (must be called with lock held)
func (rc *ResultCache) persistLocked(keyStr string, entry *CacheEntry) error {
	cacheDir := filepath.Join(rc.basePath, "cache")

	// Create directory if it doesn't exist
	if err := os.MkdirAll(cacheDir, 0755); err != nil {
		return err
	}

	cacheFile := filepath.Join(cacheDir, fmt.Sprintf("%s.json", keyStr))

	data, err := json.MarshalIndent(entry, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(cacheFile, data, 0644)
}

// Load loads cache from disk
func (rc *ResultCache) Load() error {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	cacheDir := filepath.Join(rc.basePath, "cache")
	entries, err := os.ReadDir(cacheDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // Cache doesn't exist yet
		}
		return err
	}

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		filePath := filepath.Join(cacheDir, entry.Name())
		data, err := os.ReadFile(filePath)
		if err != nil {
			continue
		}

		var cacheEntry CacheEntry
		if err := json.Unmarshal(data, &cacheEntry); err != nil {
			continue
		}

		// Skip expired entries
		if rc.ttl > 0 && time.Since(cacheEntry.CreatedAt) > rc.ttl {
			continue
		}

		keyStr := cacheEntry.Key.String()
		rc.cache[keyStr] = &cacheEntry
	}

	return nil
}

// InvalidateByActivity invalidates all cache entries for an activity type
func (rc *ResultCache) InvalidateByActivity(activityType string) error {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	keysToDelete := make([]string, 0)
	for keyStr, entry := range rc.cache {
		if entry.Key.ActivityType == activityType {
			keysToDelete = append(keysToDelete, keyStr)
		}
	}

	for _, keyStr := range keysToDelete {
		delete(rc.cache, keyStr)

		// Delete from disk
		cacheFile := filepath.Join(rc.basePath, "cache", fmt.Sprintf("%s.json", keyStr))
		_ = os.Remove(cacheFile)
	}

	return nil
}

// InvalidateByTask invalidates all cache entries for a task
func (rc *ResultCache) InvalidateByTask(taskID string) error {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	keysToDelete := make([]string, 0)
	for keyStr, entry := range rc.cache {
		if entry.Key.TaskID == taskID {
			keysToDelete = append(keysToDelete, keyStr)
		}
	}

	for _, keyStr := range keysToDelete {
		delete(rc.cache, keyStr)

		// Delete from disk
		cacheFile := filepath.Join(rc.basePath, "cache", fmt.Sprintf("%s.json", keyStr))
		_ = os.Remove(cacheFile)
	}

	return nil
}

// GetHitRate returns the cache hit rate
func (rc *ResultCache) GetHitRate() (float64, int) {
	rc.mu.RLock()
	defer rc.mu.RUnlock()

	if len(rc.cache) == 0 {
		return 0, 0
	}

	totalHits := 0
	for _, entry := range rc.cache {
		totalHits += entry.HitCount
	}

	if totalHits == 0 {
		return 0, len(rc.cache)
	}

	return float64(totalHits) / float64(len(rc.cache)), len(rc.cache)
}
