package cache

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

// PackageIndex represents a cached index of Debian packages
type PackageIndex struct {
	mu         sync.RWMutex
	packages   map[string]*PackageEntry
	binaries   map[string]string // binary package -> source package mapping
	lastUpdate time.Time
	version    string
}

// PackageEntry represents a single package entry in the index
type PackageEntry struct {
	Package      string
	Version      string
	Directory    string
	DSCName      string
	DSCSHA256    string
	Binaries     []string
	BuildDepends []string
}

// IndexCache manages persistent package indices
type IndexCache struct {
	cacheDir string
	indices  map[string]*PackageIndex // key: mirror:suite:component
	mu       sync.RWMutex
}

// NewIndexCache creates a new index cache manager
func NewIndexCache(cacheDir string) *IndexCache {
	if cacheDir == "" {
		cacheDir = defaultCacheDir()
	}
	return &IndexCache{
		cacheDir: cacheDir,
		indices:  make(map[string]*PackageIndex),
	}
}

// GetIndex retrieves or creates a package index for the given mirror/suite/component
func (c *IndexCache) GetIndex(mirror, suite, component string) (*PackageIndex, error) {
	key := fmt.Sprintf("%s:%s:%s", mirror, suite, component)

	// Fast path: check if already loaded
	c.mu.RLock()
	if idx, exists := c.indices[key]; exists {
		c.mu.RUnlock()
		return idx, nil
	}
	c.mu.RUnlock()

	// Slow path: load from disk or create new
	c.mu.Lock()
	defer c.mu.Unlock()

	// Double-check after acquiring write lock
	if idx, exists := c.indices[key]; exists {
		return idx, nil
	}

	// Try to load from disk
	cacheFile := c.cacheFilePath(mirror, suite, component)
	if idx, err := c.loadIndexFromFile(cacheFile); err == nil {
		c.indices[key] = idx
		return idx, nil
	}

	// Create new empty index
	idx := &PackageIndex{
		packages:   make(map[string]*PackageEntry),
		binaries:   make(map[string]string),
		lastUpdate: time.Time{},
		version:    "1.0",
	}
	c.indices[key] = idx
	return idx, nil
}

// LookupPackage finds a package by name in the index (O(1) lookup)
func (idx *PackageIndex) LookupPackage(name string) (*PackageEntry, bool) {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	entry, exists := idx.packages[name]
	return entry, exists
}

// LookupBinary finds a binary package and returns its source package
func (idx *PackageIndex) LookupBinary(binaryName string) (string, bool) {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	source, exists := idx.binaries[binaryName]
	return source, exists
}

// AddPackage adds a package to the index
func (idx *PackageIndex) AddPackage(entry *PackageEntry) {
	idx.mu.Lock()
	defer idx.mu.Unlock()

	idx.packages[entry.Package] = entry

	// Index binaries for fast lookup
	for _, binary := range entry.Binaries {
		if binary != "" && binary != entry.Package {
			idx.binaries[binary] = entry.Package
		}
	}
}

// Save persists the index to disk
func (idx *PackageIndex) Save(cacheFile string) error {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	// Ensure cache directory exists
	if err := os.MkdirAll(filepath.Dir(cacheFile), 0755); err != nil {
		return err
	}

	// Create temporary file
	tempFile := cacheFile + ".tmp"
	f, err := os.Create(tempFile)
	if err != nil {
		return err
	}
	defer f.Close()

	// Write JSON
	encoder := json.NewEncoder(f)
	encoder.SetIndent("", "  ")

	data := struct {
		Packages   map[string]*PackageEntry
		Binaries   map[string]string
		LastUpdate time.Time
		Version    string
	}{
		Packages:   idx.packages,
		Binaries:   idx.binaries,
		LastUpdate: time.Now(),
		Version:    idx.version,
	}

	if err := encoder.Encode(data); err != nil {
		os.Remove(tempFile)
		return err
	}

	// Atomic rename
	if err := os.Rename(tempFile, cacheFile); err != nil {
		os.Remove(tempFile)
		return err
	}

	return nil
}

// IsExpired checks if the cache is older than maxAge
func (idx *PackageIndex) IsExpired(maxAge time.Duration) bool {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	return time.Since(idx.lastUpdate) > maxAge
}

// GetStats returns index statistics
func (idx *PackageIndex) GetStats() (packageCount int, binaryCount int) {
	idx.mu.RLock()
	defer idx.mu.RUnlock()

	return len(idx.packages), len(idx.binaries)
}

// loadIndexFromFile loads an index from disk
func (c *IndexCache) loadIndexFromFile(cacheFile string) (*PackageIndex, error) {
	f, err := os.Open(cacheFile)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var data struct {
		Packages   map[string]*PackageEntry
		Binaries   map[string]string
		LastUpdate time.Time
		Version    string
	}

	if err := json.NewDecoder(f).Decode(&data); err != nil {
		return nil, err
	}

	idx := &PackageIndex{
		packages:   data.Packages,
		binaries:   data.Binaries,
		lastUpdate: data.LastUpdate,
		version:    data.Version,
	}

	// Validate integrity
	if idx.packages == nil || idx.binaries == nil {
		return nil, fmt.Errorf("invalid index file: missing maps")
	}

	return idx, nil
}

// CacheFilePath returns the cache file path for given parameters (public method)
func (c *IndexCache) CacheFilePath(mirror, suite, component string) string {
	return c.cacheFilePath(mirror, suite, component)
}

// cacheFilePath returns the cache file path for given parameters
func (c *IndexCache) cacheFilePath(mirror, suite, component string) string {
	// Create safe filename from mirror URL
	mirrorHash := sha256.Sum256([]byte(mirror))
	mirrorShort := fmt.Sprintf("%x", mirrorHash)[:8]

	filename := fmt.Sprintf("package_index_%s_%s_%s.json", mirrorShort, suite, component)
	return filepath.Join(c.cacheDir, filename)
}

// defaultCacheDir returns the default cache directory
func defaultCacheDir() string {
	if dir := os.Getenv("ZSVO_CACHE"); dir != "" {
		return dir
	}
	if home, err := os.UserHomeDir(); err == nil {
		return filepath.Join(home, ".cache", "zsvo")
	}
	return "/var/cache/zsvo"
}

// Clear removes all cached indices
func (c *IndexCache) Clear() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.indices = make(map[string]*PackageIndex)
	return os.RemoveAll(c.cacheDir)
}

// CleanupExpired removes expired cache files
func (c *IndexCache) CleanupExpired(maxAge time.Duration) error {
	entries, err := os.ReadDir(c.cacheDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}

	cutoff := time.Now().Add(-maxAge)
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		info, err := entry.Info()
		if err != nil {
			continue
		}

		if info.ModTime().Before(cutoff) {
			os.Remove(filepath.Join(c.cacheDir, entry.Name()))
		}
	}

	return nil
}
