package provider

import (
	"encoding/json"
	"fmt"
	"log"
	"math/rand"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
	"github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
	"github.com/tidbcloud/aws-shared-provider/tools/cloud-instances/regions"
	"github.com/tidbcloud/aws-shared-provider/tools/cloud-instances/types"
)

const (
	aliyunPriceConcurrency = 5
	aliyunPriceRetries     = 3
	aliyunThrottleRetries  = 6
	aliyunBackoffBase      = 500 * time.Millisecond
	aliyunBackoffMax       = 8 * time.Second
)

var aliyunSystemDiskCategories = []string{
	"",
	"cloud_efficiency",
	"cloud",
	"cloud_ssd",
	"cloud_essd",
}

// AliyunProvider implements the CloudProvider interface for Alibaba Cloud
type AliyunProvider struct {
	accessKey string
	secretKey string
	token     string
}

func init() {
	rand.Seed(time.Now().UnixNano())
}

// NewAliyunProvider creates a new Alibaba Cloud provider instance
// Credentials can be provided directly or loaded from environment variables
func NewAliyunProvider(accessKey, secretKey, token string) *AliyunProvider {
	if accessKey == "" {
		accessKey = os.Getenv("ALICLOUD_ACCESS_KEY")
	}
	if secretKey == "" {
		secretKey = os.Getenv("ALICLOUD_SECRET_KEY")
	}
	if token == "" {
		token = os.Getenv("ALICLOUD_SECURITY_TOKEN")
	}

	return &AliyunProvider{
		accessKey: accessKey,
		secretKey: secretKey,
		token:     token,
	}
}

// hasCredentials checks if credentials are available
func (p *AliyunProvider) hasCredentials() bool {
	return p.accessKey != "" && p.secretKey != ""
}

func (p *AliyunProvider) newClient(region string) (*ecs.Client, error) {
	if !p.hasCredentials() {
		return nil, fmt.Errorf("Alibaba Cloud credentials not found. Set ALICLOUD_ACCESS_KEY and ALICLOUD_SECRET_KEY environment variables")
	}

	config := sdk.NewConfig()
	config.WithTimeout(resolveAliyunTimeout())
	config.WithEnableAsync(false)

	credential := credentials.NewStsTokenCredential(p.accessKey, p.secretKey, p.token)
	client, err := ecs.NewClientWithOptions(region, config, credential)
	if err != nil {
		return nil, fmt.Errorf("failed to create ECS client: %v", err)
	}

	return client, nil
}

func resolveAliyunTimeout() time.Duration {
	if raw := strings.TrimSpace(os.Getenv("ALICLOUD_TIMEOUT")); raw != "" {
		if d, err := time.ParseDuration(raw); err == nil && d > 0 {
			return d
		}
	}
	return 60 * time.Second
}

type aliyunPriceEntry struct {
	Price    float64 `json:"price"`
	Currency string  `json:"currency"`
}

func (p *AliyunProvider) fetchInstanceTypes(client *ecs.Client) ([]ecs.InstanceType, error) {
	var all []ecs.InstanceType
	var nextToken string

	for {
		req := ecs.CreateDescribeInstanceTypesRequest()
		req.Scheme = "https"
		req.MaxResults = requests.NewInteger(100)
		req.NextToken = nextToken
		var resp *ecs.DescribeInstanceTypesResponse
		var err error
		for attempt := 0; attempt < aliyunPriceRetries; attempt++ {
			resp, err = client.DescribeInstanceTypes(req)
			if err == nil {
				break
			}
			time.Sleep(jitteredBackoff(attempt))
		}
		if err != nil {
			return nil, fmt.Errorf("DescribeInstanceTypes failed: %v", err)
		}

		all = append(all, resp.InstanceTypes.InstanceType...)
		if resp.NextToken == "" {
			break
		}
		nextToken = resp.NextToken
	}

	return all, nil
}

func (p *AliyunProvider) fetchAvailableInstanceTypes(client *ecs.Client, region string) ([]string, error) {
	req := ecs.CreateDescribeAvailableResourceRequest()
	req.Scheme = "https"
	req.ResourceType = "instance"
	req.DestinationResource = "InstanceType"
	req.InstanceChargeType = "PostPaid"

	var resp *ecs.DescribeAvailableResourceResponse
	var err error
	for attempt := 0; attempt < aliyunPriceRetries; attempt++ {
		resp, err = client.DescribeAvailableResource(req)
		if err == nil {
			break
		}
		time.Sleep(jitteredBackoff(attempt))
	}
	if err != nil {
		return nil, fmt.Errorf("DescribeAvailableResource failed: %v", err)
	}

	set := make(map[string]struct{})
	for _, zone := range resp.AvailableZones.AvailableZone {
		for _, resource := range zone.AvailableResources.AvailableResource {
			if resource.Type != "InstanceType" {
				continue
			}
			for _, supported := range resource.SupportedResources.SupportedResource {
				if supported.Value != "" {
					set[supported.Value] = struct{}{}
				}
			}
		}
	}

	var result []string
	for inst := range set {
		result = append(result, inst)
	}

	log.Printf("Found %d available instance types for %s", len(result), region)
	return result, nil
}

func jitteredBackoff(attempt int) time.Duration {
	delay := aliyunBackoffBase * time.Duration(1<<attempt)
	if delay > aliyunBackoffMax {
		delay = aliyunBackoffMax
	}
	jitter := time.Duration(rand.Int63n(int64(delay/2) + 1))
	return delay + jitter
}

func (p *AliyunProvider) fetchPrice(client *ecs.Client, instanceType string) (aliyunPriceEntry, error) {
	var lastErr error

	for _, category := range aliyunSystemDiskCategories {
		req := ecs.CreateDescribePriceRequest()
		req.Scheme = "https"
		req.ResourceType = "instance"
		req.InstanceType = instanceType
		req.PriceUnit = "Hour"
		req.Amount = requests.NewInteger(1)
		req.Platform = "Linux"
		if category != "" {
			req.SystemDiskCategory = category
		}

		resp, err := client.DescribePrice(req)
		if err != nil {
			lastErr = err
			if !isSystemDiskCategoryError(err) {
				return aliyunPriceEntry{}, err
			}
			continue
		}

		price := resp.PriceInfo.Price.TradePrice
		if price == 0 {
			price = resp.PriceInfo.Price.DiscountPrice
		}
		if price == 0 {
			price = resp.PriceInfo.Price.OriginalPrice
		}

		return aliyunPriceEntry{
			Price:    price,
			Currency: resp.PriceInfo.Price.Currency,
		}, nil
	}

	if lastErr != nil {
		return aliyunPriceEntry{}, lastErr
	}
	return aliyunPriceEntry{}, fmt.Errorf("DescribePrice failed with no response")
}

func isSystemDiskCategoryError(err error) bool {
	msg := err.Error()
	return strings.Contains(msg, "InvalidSystemDiskCategory") || strings.Contains(msg, "SystemDisk.Category")
}

func isThrottlingError(err error) bool {
	msg := err.Error()
	return strings.Contains(msg, "Throttling") || strings.Contains(msg, "Request was denied due to request throttling")
}

func (p *AliyunProvider) fetchPriceMap(client *ecs.Client, instanceTypes []string) (map[string]aliyunPriceEntry, error) {
	if len(instanceTypes) == 0 {
		return map[string]aliyunPriceEntry{}, nil
	}

	jobs := make(chan string)
	results := make(chan struct {
		instanceType string
		entry        aliyunPriceEntry
		err          error
	})

	var wg sync.WaitGroup
	for i := 0; i < aliyunPriceConcurrency; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for inst := range jobs {
				var entry aliyunPriceEntry
				var err error
				maxAttempts := aliyunPriceRetries
				for attempt := 0; attempt < maxAttempts; attempt++ {
					entry, err = p.fetchPrice(client, inst)
					if err == nil {
						break
					}
					if isThrottlingError(err) && maxAttempts < aliyunThrottleRetries {
						maxAttempts = aliyunThrottleRetries
					}
					delay := jitteredBackoff(attempt)
					if isThrottlingError(err) {
						delay += 1 * time.Second
					}
					time.Sleep(delay)
				}
				results <- struct {
					instanceType string
					entry        aliyunPriceEntry
					err          error
				}{instanceType: inst, entry: entry, err: err}
			}
		}()
	}

	go func() {
		for _, inst := range instanceTypes {
			jobs <- inst
		}
		close(jobs)
		wg.Wait()
		close(results)
	}()

	priceMap := make(map[string]aliyunPriceEntry)
	for res := range results {
		if res.err != nil {
			log.Printf("Warning: failed to fetch price for %s: %v", res.instanceType, res.err)
			continue
		}
		priceMap[res.instanceType] = res.entry
	}

	return priceMap, nil
}

// DownloadRaw downloads instance data from Alibaba Cloud APIs
func (p *AliyunProvider) DownloadRaw(region string) (*RawBundle, error) {
	client, err := p.newClient(region)
	if err != nil {
		return nil, err
	}

	log.Printf("Downloading Alibaba Cloud data for region %s...", region)

	instanceTypes, err := p.fetchInstanceTypes(client)
	if err != nil {
		return nil, err
	}

	availableTypes, err := p.fetchAvailableInstanceTypes(client, region)
	if err != nil {
		return nil, err
	}

	priceMap, err := p.fetchPriceMap(client, availableTypes)
	if err != nil {
		return nil, err
	}

	instanceTypesBytes, err := json.Marshal(instanceTypes)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal instance types: %v", err)
	}
	availableBytes, err := json.Marshal(availableTypes)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal available instance types: %v", err)
	}
	priceBytes, err := json.Marshal(priceMap)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal price map: %v", err)
	}

	return &RawBundle{
		Provider: "aliyun",
		Region:   region,
		Parts: []RawPart{
			{
				Scope:  "global",
				Name:   "instance_types",
				Data:   instanceTypesBytes,
				Source: "DescribeInstanceTypes",
			},
			{
				Scope:  region,
				Name:   "available_instance_types",
				Data:   availableBytes,
				Source: "DescribeAvailableResource",
			},
			{
				Scope:  region,
				Name:   "price_map",
				Data:   priceBytes,
				Source: "DescribePrice",
			},
		},
	}, nil
}

// ParseRaw converts SDK response to unified format
func (p *AliyunProvider) ParseRaw(raw *RawBundle) (*types.UnifiedData, error) {
	if raw == nil {
		return nil, fmt.Errorf("raw bundle is nil")
	}
	if raw.Region == "" {
		return nil, fmt.Errorf("missing region in raw bundle")
	}

	var instanceTypesBytes []byte
	var availableBytes []byte
	var priceBytes []byte
	for _, part := range raw.Parts {
		switch part.Name {
		case "instance_types":
			instanceTypesBytes = part.Data
		case "available_instance_types":
			availableBytes = part.Data
		case "price_map":
			priceBytes = part.Data
		}
	}

	if len(instanceTypesBytes) == 0 {
		return nil, fmt.Errorf("missing instance types data")
	}

	var instanceTypes []ecs.InstanceType
	if err := json.Unmarshal(instanceTypesBytes, &instanceTypes); err != nil {
		return nil, fmt.Errorf("failed to parse instance types: %v", err)
	}

	availableSet := make(map[string]struct{})
	if len(availableBytes) > 0 {
		var available []string
		if err := json.Unmarshal(availableBytes, &available); err != nil {
			return nil, fmt.Errorf("failed to parse available instance types: %v", err)
		}
		for _, inst := range available {
			availableSet[inst] = struct{}{}
		}
	}

	priceMap := make(map[string]aliyunPriceEntry)
	if len(priceBytes) > 0 {
		if err := json.Unmarshal(priceBytes, &priceMap); err != nil {
			return nil, fmt.Errorf("failed to parse price map: %v", err)
		}
	}

	unifiedData := types.NewUnifiedData("aliyun")
	var instances []types.UnifiedInstance

	for _, it := range instanceTypes {
		if len(availableSet) > 0 {
			if _, ok := availableSet[it.InstanceTypeId]; !ok {
				continue
			}
		}

		price := 0.0
		currency := ""
		if entry, ok := priceMap[it.InstanceTypeId]; ok {
			price = entry.Price
			currency = entry.Currency
		}

		storage := "Varies"
		if it.LocalStorageAmount > 0 && it.LocalStorageCapacity > 0 {
			storage = fmt.Sprintf("%d x %d GB", it.LocalStorageAmount, it.LocalStorageCapacity)
		}

		instance := types.UnifiedInstance{
			Provider:       "aliyun",
			InstanceType:   it.InstanceTypeId,
			InstanceFamily: it.InstanceTypeFamily,
			VCPU:           fmt.Sprintf("%d", it.CpuCoreCount),
			Memory:         fmt.Sprintf("%.1f GiB", it.MemorySize),
			Storage:        storage,
			Price:          price,
			Currency:       currency,
			Location:       regions.GetAliyunRegionName(raw.Region),
			Region:         raw.Region,
		}
		instances = append(instances, instance)
	}

	if len(instances) > 0 {
		unifiedData.Instances[raw.Region] = instances
	}

	return unifiedData, nil
}

func (p *AliyunProvider) GetRegions() map[string]string {
	return regions.AliyunRegions
}

func (p *AliyunProvider) IsValidRegion(regionID string) bool {
	return regions.IsValidAliyunRegion(regionID)
}

func (p *AliyunProvider) GetRegionName(regionID string) string {
	return regions.GetAliyunRegionName(regionID)
}

func (p *AliyunProvider) GetCacheFileName(region string) string {
	return fmt.Sprintf("aliyun_%s.json", region)
}

func (p *AliyunProvider) GetProviderName() string {
	return "aliyun"
}
