package query

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/aws-shared-provider/tools/cloud-instances/cache"
	"github.com/tidbcloud/aws-shared-provider/tools/cloud-instances/output"
	"github.com/tidbcloud/aws-shared-provider/tools/cloud-instances/provider"
	"github.com/tidbcloud/aws-shared-provider/tools/cloud-instances/types"
)

// FilterOptions contains all filtering criteria for queries
type FilterOptions struct {
	Provider           string
	Regions            []string
	TypeRegex          string
	FamilyFilter       string
	VCPUFilter         string
	MemoryFilter       string
	LocalStorageFilter string
	MinPrice           float64
	MaxPrice           float64
	Currency           string
	FromCache          bool
	CacheDir           string
	MaxCacheAge        time.Duration
	ForceDownload      bool
	DownloadOnly       bool
	OutputFormat       string
	OutputStyle        string
	SortBy             string
	ShowDetails        bool
}

// QueryEngine manages queries across multiple cloud providers
type QueryEngine struct {
	Providers map[string]provider.CloudProvider
	CacheMgr  *cache.Manager
}

type rawManifest struct {
	Provider  string            `json:"provider"`
	Scope     string            `json:"scope"`
	Region    string            `json:"region,omitempty"`
	Timestamp time.Time         `json:"timestamp"`
	Params    map[string]string `json:"params,omitempty"`
	Parts     []rawManifestPart `json:"parts"`
}

type rawManifestPart struct {
	Name   string `json:"name"`
	Source string `json:"source,omitempty"`
}

// NewQueryEngine creates a new query engine with registered providers
func NewQueryEngine(cacheDir string) *QueryEngine {
	qe := &QueryEngine{
		Providers: make(map[string]provider.CloudProvider),
		CacheMgr:  cache.NewManager(cacheDir),
	}

	// Register all providers
	qe.Providers["aws"] = provider.NewAWSProvider()
	qe.Providers["azure"] = provider.NewAzureProvider()
	qe.Providers["aliyun"] = provider.NewAliyunProvider("", "", "")
	qe.Providers["gcp"] = provider.NewGCPProvider()

	return qe
}

// RegisterProvider adds a new provider to the engine
func (qe *QueryEngine) RegisterProvider(name string, p provider.CloudProvider) {
	qe.Providers[name] = p
}

// ListProviders returns all registered provider names
func (qe *QueryEngine) ListProviders() []string {
	providers := make([]string, 0, len(qe.Providers))
	for name := range qe.Providers {
		providers = append(providers, name)
	}
	return providers
}

// ListRegions returns all regions for a given provider
func (qe *QueryEngine) ListRegions(providerName string) (map[string]string, error) {
	p, exists := qe.Providers[providerName]
	if !exists {
		return nil, fmt.Errorf("provider '%s' not found", providerName)
	}
	return p.GetRegions(), nil
}

// Download downloads data for specified provider and regions
func (qe *QueryEngine) Download(opts FilterOptions) error {
	providers := qe.getProviders(opts.Provider)
	if len(providers) == 0 {
		return fmt.Errorf("no providers found matching '%s'", opts.Provider)
	}

	for providerName, p := range providers {
		regions := opts.Regions
		if len(regions) == 0 {
			// Get all regions if none specified
			regMap := p.GetRegions()
			for regionID := range regMap {
				regions = append(regions, regionID)
			}
		}

		for _, region := range regions {
			if !p.IsValidRegion(region) {
				log.Printf("Warning: Skipping invalid region '%s' for provider '%s'", region, providerName)
				continue
			}

			var (
				raw           *provider.RawBundle
				rawDownloaded bool
			)

			if !opts.ForceDownload {
				cachedRaw, ok, err := qe.loadRawBundle(providerName, region, opts.MaxCacheAge)
				if err == nil && ok {
					raw = cachedRaw
				}
			}

			if raw == nil {
				log.Printf("Downloading %s data for region %s...", providerName, region)
				downloaded, err := p.DownloadRaw(region)
				if err != nil {
					return fmt.Errorf("failed to download %s data for region %s: %v", providerName, region, err)
				}
				raw = downloaded
				rawDownloaded = true
				if err := qe.saveRawBundle(raw); err != nil {
					return fmt.Errorf("failed to save raw cache for %s/%s: %v", providerName, region, err)
				}
			} else {
				log.Printf("Using cached raw data for %s/%s", providerName, region)
			}

			// Skip unified regeneration if it already exists and raw was not refreshed
			if !rawDownloaded && !opts.ForceDownload && qe.CacheMgr.CacheExists(providerName, region) {
				log.Printf("Using cached unified data for %s/%s", providerName, region)
				continue
			}

			unifiedData, err := p.ParseRaw(raw)
			if err != nil {
				return fmt.Errorf("failed to parse %s data for region %s: %v", providerName, region, err)
			}

			if err := qe.CacheMgr.SaveToCache(unifiedData, providerName, region); err != nil {
				return fmt.Errorf("failed to save cache for %s/%s: %v", providerName, region, err)
			}

			log.Printf("Saved cache: %s_%s.json", providerName, region)

			if opts.DownloadOnly {
				continue
			}
		}
	}

	return nil
}

// Query executes a query with filtering options
func (qe *QueryEngine) Query(opts FilterOptions) ([]types.UnifiedInstance, error) {
	providers := qe.getProviders(opts.Provider)
	if len(providers) == 0 {
		return nil, fmt.Errorf("no providers found matching '%s'", opts.Provider)
	}

	var allInstances []types.UnifiedInstance

	for providerName, p := range providers {
		// Get regions to query
		regions := opts.Regions
		if len(regions) == 0 {
			regMap := p.GetRegions()
			for regionID := range regMap {
				regions = append(regions, regionID)
			}
		}

		for _, region := range regions {
			if !p.IsValidRegion(region) {
				continue
			}

			var (
				unifiedData *types.UnifiedData
				raw         *provider.RawBundle
				err         error
			)

			if !opts.ForceDownload {
				cachedRaw, ok, cacheErr := qe.loadRawBundle(providerName, region, opts.MaxCacheAge)
				if cacheErr != nil {
					log.Printf("Warning: Failed to load raw cache for %s/%s: %v", providerName, region, cacheErr)
				}
				if ok {
					raw = cachedRaw
					unifiedData, err = qe.CacheMgr.LoadFromCache(providerName, region)
					if err == nil && unifiedData != nil && unifiedData.Instances != nil {
						if _, exists := unifiedData.Instances[region]; exists {
							// Use unified cache directly
						} else {
							unifiedData = nil
						}
					} else {
						unifiedData = nil
					}

					if unifiedData == nil {
						unifiedData, err = p.ParseRaw(raw)
						if err != nil {
							log.Printf("Warning: Failed to parse cached raw data for %s/%s: %v", providerName, region, err)
							unifiedData = nil
						} else if saveErr := qe.CacheMgr.SaveToCache(unifiedData, providerName, region); saveErr != nil {
							log.Printf("Warning: Failed to save cache: %v", saveErr)
						}
					}
				}
			}

			if unifiedData == nil {
				if opts.FromCache {
					return nil, fmt.Errorf("cache not found or invalid for %s/%s", providerName, region)
				}

				log.Printf("Downloading %s/%s...", providerName, region)
				raw, err = p.DownloadRaw(region)
				if err != nil {
					log.Printf("Warning: Failed to download %s/%s: %v", providerName, region, err)
					continue
				}
				if err := qe.saveRawBundle(raw); err != nil {
					log.Printf("Warning: Failed to save raw cache: %v", err)
				}
				unifiedData, err = p.ParseRaw(raw)
				if err != nil {
					log.Printf("Warning: Failed to parse %s/%s: %v", providerName, region, err)
					continue
				}
				if saveErr := qe.CacheMgr.SaveToCache(unifiedData, providerName, region); saveErr != nil {
					log.Printf("Warning: Failed to save cache: %v", saveErr)
				}
			}

			// Extract instances for this region
			if instances, exists := unifiedData.Instances[region]; exists {
				allInstances = append(allInstances, instances...)
			}
		}
	}

	// Apply filters
	filteredInstances := qe.applyFilters(allInstances, opts)
	if err := sortInstances(filteredInstances, opts.SortBy); err != nil {
		return nil, err
	}

	return filteredInstances, nil
}

// QueryAndOutput executes query and formats output
func (qe *QueryEngine) QueryAndOutput(opts FilterOptions) error {
	instances, err := qe.Query(opts)
	if err != nil {
		return err
	}

	if len(instances) == 0 {
		fmt.Println("No instances found matching the criteria")
		return nil
	}

	// Format and output
	outputOpts := output.Options{
		Format:  opts.OutputFormat,
		Style:   opts.OutputStyle,
		Details: opts.ShowDetails,
	}
	formatter := output.NewFormatter(outputOpts)
	return formatter.FormatOutput(instances, opts.Provider)
}

func (qe *QueryEngine) loadManifest(providerName, scope string, maxAge time.Duration) (*rawManifest, bool, error) {
	path := qe.CacheMgr.RawPath(providerName, scope, "manifest")
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return nil, false, nil
		}
		return nil, false, err
	}

	if maxAge > 0 && !qe.CacheMgr.IsCacheValid(path, maxAge) {
		return nil, false, nil
	}

	data, err := qe.CacheMgr.LoadRaw(providerName, scope, "manifest")
	if err != nil {
		return nil, false, err
	}

	var manifest rawManifest
	if err := json.Unmarshal(data, &manifest); err != nil {
		return nil, false, err
	}

	return &manifest, true, nil
}

func (qe *QueryEngine) loadRawBundle(providerName, region string, maxAge time.Duration) (*provider.RawBundle, bool, error) {
	manifest, ok, err := qe.loadManifest(providerName, region, maxAge)
	if err != nil || !ok {
		return nil, false, err
	}

	bundle := &provider.RawBundle{
		Provider: providerName,
		Region:   region,
	}

	for _, part := range manifest.Parts {
		data, err := qe.CacheMgr.LoadRaw(providerName, region, part.Name)
		if err != nil {
			return nil, false, err
		}
		bundle.Parts = append(bundle.Parts, provider.RawPart{
			Scope:  region,
			Name:   part.Name,
			Data:   data,
			Source: part.Source,
		})
	}

	globalManifest, ok, err := qe.loadManifest(providerName, "global", maxAge)
	if err != nil {
		return nil, false, err
	}
	if ok {
		for _, part := range globalManifest.Parts {
			data, err := qe.CacheMgr.LoadRaw(providerName, "global", part.Name)
			if err != nil {
				return nil, false, err
			}
			bundle.Parts = append(bundle.Parts, provider.RawPart{
				Scope:  "global",
				Name:   part.Name,
				Data:   data,
				Source: part.Source,
			})
		}
	}

	return bundle, true, nil
}

func (qe *QueryEngine) saveRawBundle(raw *provider.RawBundle) error {
	if raw == nil {
		return fmt.Errorf("raw bundle is nil")
	}
	providerName := raw.Provider
	if providerName == "" {
		return fmt.Errorf("raw bundle missing provider name")
	}
	if raw.Region == "" {
		return fmt.Errorf("raw bundle missing region")
	}

	grouped := make(map[string][]provider.RawPart)
	for _, part := range raw.Parts {
		scope := part.Scope
		if scope == "" {
			scope = raw.Region
		}
		part.Scope = scope
		grouped[scope] = append(grouped[scope], part)
	}

	for scope, parts := range grouped {
		for _, part := range parts {
			if part.Name == "" {
				return fmt.Errorf("raw part missing name for %s/%s", providerName, scope)
			}
			if err := qe.CacheMgr.SaveRaw(providerName, scope, part.Name, part.Data); err != nil {
				return err
			}
		}

		manifest := rawManifest{
			Provider:  providerName,
			Scope:     scope,
			Timestamp: time.Now(),
			Parts:     make([]rawManifestPart, 0, len(parts)),
		}
		if scope != "global" {
			manifest.Region = raw.Region
			manifest.Params = map[string]string{"region": raw.Region}
		}
		for _, part := range parts {
			manifest.Parts = append(manifest.Parts, rawManifestPart{
				Name:   part.Name,
				Source: part.Source,
			})
		}

		payload, err := json.MarshalIndent(manifest, "", "  ")
		if err != nil {
			return err
		}
		if err := qe.CacheMgr.SaveRaw(providerName, scope, "manifest", payload); err != nil {
			return err
		}
	}

	return nil
}

// getProviders returns the providers to use based on the filter
func (qe *QueryEngine) getProviders(providerFilter string) map[string]provider.CloudProvider {
	if providerFilter == "all" {
		return qe.Providers
	}

	result := make(map[string]provider.CloudProvider)
	if p, exists := qe.Providers[providerFilter]; exists {
		result[providerFilter] = p
	}
	return result
}

// applyFilters applies all filter criteria to the instances
func (qe *QueryEngine) applyFilters(instances []types.UnifiedInstance, opts FilterOptions) []types.UnifiedInstance {
	var filtered []types.UnifiedInstance

	for _, inst := range instances {
		// Type regex filter
		if opts.TypeRegex != "" {
			matched, err := regexp.MatchString(strings.ToLower(opts.TypeRegex), strings.ToLower(inst.InstanceType))
			if err != nil || !matched {
				continue
			}
		}

		// Family filter
		if opts.FamilyFilter != "" {
			if !strings.Contains(strings.ToLower(inst.InstanceFamily), strings.ToLower(opts.FamilyFilter)) {
				continue
			}
		}

		// vCPU filter
		if opts.VCPUFilter != "" {
			if inst.VCPU != opts.VCPUFilter {
				continue
			}
		}

		// Memory filter
		if opts.MemoryFilter != "" {
			// Parse memory from instance (format: "X GiB")
			instMem := parseMemory(inst.Memory)
			filterMem := parseMemory(opts.MemoryFilter)
			if filterMem > 0 && instMem != filterMem {
				continue
			}
		}

		// Local storage filter
		if opts.LocalStorageFilter != "" {
			hasLocal, known := localStorageStatus(inst.Storage)
			switch opts.LocalStorageFilter {
			case "with":
				if !known || !hasLocal {
					continue
				}
			case "without":
				if !known || hasLocal {
					continue
				}
			}
		}

		// Price filters
		if (opts.MinPrice > 0 || opts.MaxPrice > 0) && opts.Currency != "" {
			if !strings.EqualFold(inst.Currency, opts.Currency) {
				continue
			}
		}
		if opts.MinPrice > 0 && inst.Price < opts.MinPrice {
			continue
		}
		if opts.MaxPrice > 0 && inst.Price > opts.MaxPrice {
			continue
		}

		filtered = append(filtered, inst)
	}

	return filtered
}

func sortInstances(instances []types.UnifiedInstance, sortBy string) error {
	sortBy = strings.ToLower(strings.TrimSpace(sortBy))
	if sortBy == "" || sortBy == "none" {
		return nil
	}

	type sortableInstance struct {
		inst  types.UnifiedInstance
		value float64
		known bool
	}

	list := make([]sortableInstance, len(instances))

	switch sortBy {
	case "price":
		for i, inst := range instances {
			list[i] = sortableInstance{
				inst:  inst,
				value: inst.Price,
				known: inst.Price > 0,
			}
		}
		sort.SliceStable(list, func(i, j int) bool {
			if list[i].known != list[j].known {
				return list[i].known
			}
			if !list[i].known {
				return false
			}
			if list[i].value == list[j].value {
				return false
			}
			return list[i].value < list[j].value
		})
	case "vcpu":
		for i, inst := range instances {
			value, known := parseVCPU(inst.VCPU)
			list[i] = sortableInstance{
				inst:  inst,
				value: value,
				known: known,
			}
		}
		sort.SliceStable(list, func(i, j int) bool {
			if list[i].known != list[j].known {
				return list[i].known
			}
			if !list[i].known {
				return false
			}
			if list[i].value == list[j].value {
				return false
			}
			return list[i].value < list[j].value
		})
	case "memory":
		for i, inst := range instances {
			value := parseMemory(inst.Memory)
			list[i] = sortableInstance{
				inst:  inst,
				value: value,
				known: value > 0,
			}
		}
		sort.SliceStable(list, func(i, j int) bool {
			if list[i].known != list[j].known {
				return list[i].known
			}
			if !list[i].known {
				return false
			}
			if list[i].value == list[j].value {
				return false
			}
			return list[i].value < list[j].value
		})
	default:
		return fmt.Errorf("invalid sort option %q (expected price, vcpu, memory)", sortBy)
	}

	for i := range instances {
		instances[i] = list[i].inst
	}

	return nil
}

// parseMemory extracts memory value in GiB from string like "16 GiB"
func parseMemory(memoryStr string) float64 {
	if memoryStr == "" {
		return 0
	}

	// Try to extract number
	parts := strings.Fields(memoryStr)
	if len(parts) == 0 {
		return 0
	}

	val, err := strconv.ParseFloat(parts[0], 64)
	if err != nil {
		return 0
	}

	// If unit is MB, convert to GB
	if len(parts) > 1 && strings.Contains(strings.ToLower(parts[1]), "mb") {
		val = val / 1024.0
	}

	return val
}

var firstNumberRE = regexp.MustCompile(`\d+(\.\d+)?`)
var localStorageCountRE = regexp.MustCompile(`\d+\s*x\s*\d+`)

func parseVCPU(vcpu string) (float64, bool) {
	return parseFirstNumber(vcpu)
}

func parseFirstNumber(value string) (float64, bool) {
	match := firstNumberRE.FindString(value)
	if match == "" {
		return 0, false
	}
	val, err := strconv.ParseFloat(match, 64)
	if err != nil {
		return 0, false
	}
	return val, true
}

func localStorageStatus(storage string) (hasLocal bool, known bool) {
	s := strings.TrimSpace(strings.ToLower(storage))
	if s == "" {
		return false, false
	}
	if strings.Contains(s, "varies") || strings.Contains(s, "unknown") {
		return false, false
	}
	if strings.Contains(s, "ebs only") || strings.Contains(s, "ebs-only") {
		return false, true
	}
	if strings.Contains(s, "no local") || s == "none" || s == "n/a" {
		return false, true
	}
	if strings.Contains(s, "instance store") || strings.Contains(s, "local") {
		return true, true
	}
	if strings.Contains(s, "nvme") || strings.Contains(s, "ssd") || strings.Contains(s, "hdd") {
		return true, true
	}
	if localStorageCountRE.MatchString(s) && (strings.Contains(s, "gb") || strings.Contains(s, "tb")) {
		return true, true
	}
	return false, false
}

// GetAvailableRegions returns all available regions for specified providers
func (qe *QueryEngine) GetAvailableRegions(providerFilter string) map[string]map[string]string {
	result := make(map[string]map[string]string)
	providers := qe.getProviders(providerFilter)

	for name, p := range providers {
		result[name] = p.GetRegions()
	}

	return result
}

// ValidateRegion checks if a region is valid for a provider
func (qe *QueryEngine) ValidateRegion(providerName, region string) bool {
	p, exists := qe.Providers[providerName]
	if !exists {
		return false
	}
	return p.IsValidRegion(region)
}

// GetCacheInfo returns information about cached data
func (qe *QueryEngine) GetCacheInfo(providerName string) ([]string, error) {
	p, exists := qe.Providers[providerName]
	if !exists {
		return nil, fmt.Errorf("provider '%s' not found", providerName)
	}

	regions := p.GetRegions()
	var cacheFiles []string

	for region := range regions {
		if qe.CacheMgr.CacheExists(providerName, region) {
			cacheFiles = append(cacheFiles, fmt.Sprintf("%s_%s.json", providerName, region))
		}
	}

	return cacheFiles, nil
}

// ClearCache removes cached data for specified provider/region
func (qe *QueryEngine) ClearCache(providerName string, regions []string) error {
	p, exists := qe.Providers[providerName]
	if !exists {
		return fmt.Errorf("provider '%s' not found", providerName)
	}

	if len(regions) == 0 {
		// Clear all raw data for this provider
		rawProviderDir := filepath.Join(qe.CacheMgr.CacheDir, "raw", providerName)
		if err := os.RemoveAll(rawProviderDir); err == nil {
			log.Printf("Deleted raw cache: %s", rawProviderDir)
		}

		// Clear all unified data for this provider
		pattern := filepath.Join(qe.CacheMgr.CacheDir, "unified", fmt.Sprintf("%s_*.json", providerName))
		if matches, err := filepath.Glob(pattern); err == nil {
			for _, file := range matches {
				if err := os.Remove(file); err == nil {
					log.Printf("Deleted cache: %s", file)
				}
			}
		}

		return nil
	}

	for _, region := range regions {
		if !p.IsValidRegion(region) {
			continue
		}
		// Delete unified cache
		fullPath := qe.CacheMgr.UnifiedPath(providerName, region)
		if err := os.Remove(fullPath); err == nil {
			log.Printf("Deleted cache: %s", fullPath)
		}

		// Delete raw cache for this region
		rawDir := filepath.Join(qe.CacheMgr.CacheDir, "raw", providerName, region)
		if err := os.RemoveAll(rawDir); err == nil {
			log.Printf("Deleted raw cache: %s", rawDir)
		}
	}

	return nil
}

// GetCacheAge returns the age of cached data for a provider/region
func (qe *QueryEngine) GetCacheAge(providerName, region string) (time.Duration, error) {
	p, exists := qe.Providers[providerName]
	if !exists {
		return 0, fmt.Errorf("provider '%s' not found", providerName)
	}

	if !p.IsValidRegion(region) {
		return 0, fmt.Errorf("invalid region '%s' for provider '%s'", region, providerName)
	}

	manifestPath := qe.CacheMgr.RawPath(providerName, region, "manifest")
	if info, err := os.Stat(manifestPath); err == nil {
		return time.Since(info.ModTime()), nil
	}

	fullPath := qe.CacheMgr.UnifiedPath(providerName, region)
	info, err := os.Stat(fullPath)
	if err != nil {
		return 0, err
	}

	return time.Since(info.ModTime()), nil
}
