package proxy

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"sync"
	"time"

	"torrentGateway/internal/config"
	"torrentGateway/internal/storage"
)

// SmartProxy provides intelligent proxy functionality for serving chunked files
type SmartProxy struct {
	storage      *storage.Backend
	gatewayURL   string
	cache        *LRUCache
	config       *config.Config
	mu           sync.RWMutex
}

// NewSmartProxy creates a new smart proxy instance
func NewSmartProxy(storage *storage.Backend, cfg *config.Config) *SmartProxy {
	gatewayURL := fmt.Sprintf("http://localhost:%d", cfg.Gateway.Port)
	cache := NewLRUCache(cfg.Proxy.CacheSize, cfg.Proxy.CacheMaxAge)
	
	proxy := &SmartProxy{
		storage:    storage,
		gatewayURL: gatewayURL,
		cache:      cache,
		config:     cfg,
	}
	
	// Start automatic cache cleanup
	go proxy.startCacheCleanup()
	
	return proxy
}

// ServeBlob attempts to serve a blob by hash, reassembling from chunks if necessary
func (p *SmartProxy) ServeBlob(w http.ResponseWriter, hash string) error {
	// First check cache
	if cachedData := p.cache.Get(hash); cachedData != nil {
		log.Printf("Serving cached reassembled file for hash: %s", hash)
		p.serveCachedData(w, hash, cachedData)
		return nil
	}
	
	// Check if this hash exists as chunked file in metadata
	metadata, err := p.storage.GetFileMetadata(hash)
	if err != nil {
		return fmt.Errorf("error checking metadata for hash %s: %v", hash, err)
	}
	
	if metadata == nil {
		return fmt.Errorf("hash %s not found as chunked file", hash)
	}
	
	// Only proceed if this is a torrent/chunked file
	if metadata.StorageType != "torrent" {
		return fmt.Errorf("hash %s is not a chunked file (storage type: %s)", hash, metadata.StorageType)
	}
	
	// Get chunk hashes for this file
	chunkHashes, err := p.storage.GetChunkHashes(hash)
	if err != nil {
		return fmt.Errorf("error getting chunk hashes for %s: %v", hash, err)
	}
	
	if len(chunkHashes) == 0 {
		return fmt.Errorf("no chunks found for file %s", hash)
	}
	
	log.Printf("Found chunked file for hash %s, reassembling %d chunks", hash, len(chunkHashes))
	
	// Reassemble the file from chunks
	reassembledData, err := p.reassembleFile(metadata, chunkHashes)
	if err != nil {
		return fmt.Errorf("error reassembling file %s: %v", hash, err)
	}
	
	// Cache the reassembled data
	p.cache.Put(hash, &CachedBlob{
		Data:     reassembledData,
		MimeType: metadata.ContentType,
		Size:     metadata.Size,
		Hash:     hash,
	})
	
	// Serve the reassembled data
	w.Header().Set("Content-Type", metadata.ContentType)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(reassembledData)))
	w.Header().Set("ETag", fmt.Sprintf(`"%s"`, hash))
	w.Header().Set("Cache-Control", "public, max-age=31536000")
	
	if _, err := w.Write(reassembledData); err != nil {
		return fmt.Errorf("error writing response: %v", err)
	}
	
	log.Printf("Successfully served reassembled file for hash: %s (%d bytes)", hash, len(reassembledData))
	return nil
}

// reassembleFile reassembles a file from its chunks
func (p *SmartProxy) reassembleFile(metadata *storage.FileMetadata, chunkHashes []string) ([]byte, error) {
	if len(chunkHashes) == 0 {
		return nil, fmt.Errorf("no chunks found in metadata")
	}
	
	var buf bytes.Buffer
	buf.Grow(int(metadata.Size)) // Pre-allocate buffer
	
	// Process chunks in order
	for i, chunkHash := range chunkHashes {
		chunkData, err := p.storage.GetChunkData(chunkHash)
		if err != nil {
			return nil, fmt.Errorf("error getting chunk %d (%s): %v", i, chunkHash, err)
		}
		
		if chunkData == nil {
			return nil, fmt.Errorf("chunk %d (%s) not found", i, chunkHash)
		}
		
		if _, err := buf.Write(chunkData); err != nil {
			return nil, fmt.Errorf("error writing chunk %d to buffer: %v", i, err)
		}
	}
	
	return buf.Bytes(), nil
}

// serveCachedData serves cached blob data
func (p *SmartProxy) serveCachedData(w http.ResponseWriter, hash string, cached *CachedBlob) {
	w.Header().Set("Content-Type", cached.MimeType)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", len(cached.Data)))
	w.Header().Set("ETag", fmt.Sprintf(`"%s"`, hash))
	w.Header().Set("Cache-Control", "public, max-age=31536000")
	w.Header().Set("X-Proxy-Cache", "HIT")
	
	w.Write(cached.Data)
}

// startCacheCleanup starts automatic cache cleanup routine
func (p *SmartProxy) startCacheCleanup() {
	// Clean expired entries every 10 minutes
	ticker := time.NewTicker(10 * time.Minute)
	defer ticker.Stop()

	for range ticker.C {
		p.cache.CleanExpired()
	}
}

// CachedBlob represents a cached reassembled blob
type CachedBlob struct {
	Data     []byte
	MimeType string
	Size     int64
	Hash     string
	CachedAt time.Time
}

// LRUCache implements a simple LRU cache for reassembled blobs
type LRUCache struct {
	capacity   int
	maxAge     time.Duration
	cache      map[string]*CacheEntry
	lruList    []*CacheEntry
	mu         sync.RWMutex
}

// CacheEntry represents an entry in the cache
type CacheEntry struct {
	Key      string
	Value    *CachedBlob
	CachedAt time.Time
}

// NewLRUCache creates a new LRU cache
func NewLRUCache(capacity int, maxAge time.Duration) *LRUCache {
	if capacity <= 0 {
		capacity = 100 // Default capacity
	}
	if maxAge <= 0 {
		maxAge = 1 * time.Hour // Default max age
	}
	
	return &LRUCache{
		capacity: capacity,
		maxAge:   maxAge,
		cache:    make(map[string]*CacheEntry),
		lruList:  make([]*CacheEntry, 0, capacity),
	}
}

// Get retrieves a value from the cache
func (c *LRUCache) Get(key string) *CachedBlob {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	entry, exists := c.cache[key]
	if !exists {
		return nil
	}
	
	// Check if entry is expired
	if time.Since(entry.CachedAt) > c.maxAge {
		c.removeEntry(key)
		return nil
	}
	
	// Move to front (most recently used)
	c.moveToFront(entry)
	return entry.Value
}

// Put adds a value to the cache
func (c *LRUCache) Put(key string, value *CachedBlob) {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	// Check if entry already exists
	if entry, exists := c.cache[key]; exists {
		entry.Value = value
		entry.CachedAt = time.Now()
		c.moveToFront(entry)
		return
	}
	
	// Create new entry
	entry := &CacheEntry{
		Key:      key,
		Value:    value,
		CachedAt: time.Now(),
	}
	
	// Check capacity
	if len(c.cache) >= c.capacity {
		c.evictLRU()
	}
	
	// Add to cache
	c.cache[key] = entry
	c.lruList = append([]*CacheEntry{entry}, c.lruList...)
}

// moveToFront moves an entry to the front of the LRU list
func (c *LRUCache) moveToFront(entry *CacheEntry) {
	// Find and remove entry from current position
	for i, e := range c.lruList {
		if e == entry {
			c.lruList = append(c.lruList[:i], c.lruList[i+1:]...)
			break
		}
	}
	
	// Add to front
	c.lruList = append([]*CacheEntry{entry}, c.lruList...)
}

// evictLRU removes the least recently used entry
func (c *LRUCache) evictLRU() {
	if len(c.lruList) == 0 {
		return
	}
	
	// Remove last entry (LRU)
	lru := c.lruList[len(c.lruList)-1]
	c.lruList = c.lruList[:len(c.lruList)-1]
	delete(c.cache, lru.Key)
	
	log.Printf("Evicted cached blob: %s", lru.Key)
}

// removeEntry removes an entry from the cache
func (c *LRUCache) removeEntry(key string) {
	entry, exists := c.cache[key]
	if !exists {
		return
	}
	
	// Remove from cache map
	delete(c.cache, key)
	
	// Remove from LRU list
	for i, e := range c.lruList {
		if e == entry {
			c.lruList = append(c.lruList[:i], c.lruList[i+1:]...)
			break
		}
	}
}

// CleanExpired removes expired entries from the cache
func (c *LRUCache) CleanExpired() {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	now := time.Now()
	var toRemove []string
	
	for key, entry := range c.cache {
		if now.Sub(entry.CachedAt) > c.maxAge {
			toRemove = append(toRemove, key)
		}
	}
	
	for _, key := range toRemove {
		c.removeEntry(key)
	}
	
	if len(toRemove) > 0 {
		log.Printf("Cleaned %d expired cache entries", len(toRemove))
	}
}

// Size returns the current cache size
func (c *LRUCache) Size() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.cache)
}