package storage

import (
	"crypto/md5"
	"encoding/gob"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/tanagram/cli/parser"
)

const (
	cacheDir  = ".tanagram"
	cacheFile = "cache.gob"
)

// SerializablePolicy is a version of parser.Policy that can be encoded with gob
// Simplified: No types or patterns needed for LLM-only detection
type SerializablePolicy struct {
	Name         string
	Message      string
	OriginalText string
}

// Cache stores parsed policies and their MD5 hashes
type Cache struct {
	FileMD5s  map[string]string               // filepath -> MD5 hash
	Policies  map[string][]SerializablePolicy // filepath -> policies
	CachePath string                           // where the cache file is stored
}

// NewCache creates a new empty cache
func NewCache(cachePath string) *Cache {
	return &Cache{
		FileMD5s:  make(map[string]string),
		Policies:  make(map[string][]SerializablePolicy),
		CachePath: cachePath,
	}
}

// LoadCache loads the cache from disk, returns empty cache if not found
func LoadCache(gitRoot string) (*Cache, error) {
	cachePath := filepath.Join(gitRoot, cacheDir, cacheFile)

	// If cache doesn't exist, return empty cache
	if _, err := os.Stat(cachePath); os.IsNotExist(err) {
		return NewCache(cachePath), nil
	}

	file, err := os.Open(cachePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open cache: %w", err)
	}
	defer file.Close()

	var cache Cache
	decoder := gob.NewDecoder(file)
	if err := decoder.Decode(&cache); err != nil {
		return nil, fmt.Errorf("failed to decode cache: %w", err)
	}

	cache.CachePath = cachePath
	return &cache, nil
}

// Save writes the cache to disk
func (c *Cache) Save() error {
	// Create cache directory if it doesn't exist
	dir := filepath.Dir(c.CachePath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("failed to create cache directory: %w", err)
	}

	file, err := os.Create(c.CachePath)
	if err != nil {
		return fmt.Errorf("failed to create cache file: %w", err)
	}
	defer file.Close()

	encoder := gob.NewEncoder(file)
	if err := encoder.Encode(c); err != nil {
		return fmt.Errorf("failed to encode cache: %w", err)
	}

	return nil
}

// CalculateMD5 computes the MD5 hash of a file
func CalculateMD5(filepath string) (string, error) {
	file, err := os.Open(filepath)
	if err != nil {
		return "", fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	hash := md5.New()
	if _, err := io.Copy(hash, file); err != nil {
		return "", fmt.Errorf("failed to hash file: %w", err)
	}

	return fmt.Sprintf("%x", hash.Sum(nil)), nil
}

// HasChanged checks if a file's MD5 has changed since last sync
func (c *Cache) HasChanged(filepath string) (bool, error) {
	currentMD5, err := CalculateMD5(filepath)
	if err != nil {
		return false, err
	}

	cachedMD5, exists := c.FileMD5s[filepath]
	if !exists {
		return true, nil // File not in cache, consider it changed
	}

	return currentMD5 != cachedMD5, nil
}

// UpdateFile updates the cache with new policies and MD5 for a file
func (c *Cache) UpdateFile(filepath string, policies []parser.Policy) error {
	md5Hash, err := CalculateMD5(filepath)
	if err != nil {
		return err
	}

	// Convert policies to serializable format
	serializablePolicies := make([]SerializablePolicy, len(policies))
	for i, p := range policies {
		serializablePolicies[i] = SerializablePolicy{
			Name:         p.Name,
			Message:      p.Message,
			OriginalText: p.OriginalText,
		}
	}

	c.FileMD5s[filepath] = md5Hash
	c.Policies[filepath] = serializablePolicies

	return nil
}

// GetPolicies retrieves cached policies for a file and converts them back to parser.Policy
func (c *Cache) GetPolicies(filepath string) ([]parser.Policy, error) {
	serializablePolicies, exists := c.Policies[filepath]
	if !exists {
		return nil, fmt.Errorf("no cached policies for file: %s", filepath)
	}

	// Convert back to parser.Policy
	policies := make([]parser.Policy, len(serializablePolicies))
	for i, sp := range serializablePolicies {
		policies[i] = parser.Policy{
			Name:         sp.Name,
			Message:      sp.Message,
			OriginalText: sp.OriginalText,
		}
	}

	return policies, nil
}

// GetAllPolicies returns all cached policies from all files combined
func (c *Cache) GetAllPolicies() ([]parser.Policy, error) {
	var allPolicies []parser.Policy

	for filepath := range c.Policies {
		policies, err := c.GetPolicies(filepath)
		if err != nil {
			return nil, err
		}
		allPolicies = append(allPolicies, policies...)
	}

	return allPolicies, nil
}

// FindGitRoot walks up from the current directory to find the git root
func FindGitRoot() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}

	for {
		gitPath := filepath.Join(dir, ".git")
		if _, err := os.Stat(gitPath); err == nil {
			return dir, nil
		}

		parent := filepath.Dir(dir)
		if parent == dir {
			return "", fmt.Errorf("not in a git repository")
		}
		dir = parent
	}
}
