package provider

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

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

// AzureProvider implements the CloudProvider interface for Azure
type AzureProvider struct{}

// NewAzureProvider creates a new Azure provider instance
func NewAzureProvider() *AzureProvider {
	return &AzureProvider{}
}

// AzureAttributes represents instance metadata from Azure
type AzureAttributes struct {
	Series       string  `json:"series"`
	NameLocKey   string  `json:"nameLocKey"`
	InstanceName string  `json:"instanceName"`
	Cores        int     `json:"cores"`
	Category     string  `json:"category"`
	RAM          float64 `json:"ram"`
	Tier         string  `json:"tier"`
	DiskSize     int     `json:"diskSize"`
	Type         string  `json:"type"`
}

// AzureData represents the Azure metadata structure
type AzureData struct {
	AttributesByOffer map[string]AzureAttributes `json:"attributesByOffer"`
}

// AzurePricingItem represents an item from the Azure Retail Prices API
type AzurePricingItem struct {
	CurrencyCode    string  `json:"currencyCode"`
	RetailPrice     float64 `json:"retailPrice"`
	ArmRegionName   string  `json:"armRegionName"`
	Location        string  `json:"location"`
	MeterName       string  `json:"meterName"`
	ProductName     string  `json:"productName"`
	SkuName         string  `json:"skuName"`
	ServiceName     string  `json:"serviceName"`
	UnitOfMeasure   string  `json:"unitOfMeasure"`
	Type            string  `json:"type"` // Consumption, Reservation, etc.
	ArmSkuName      string  `json:"armSkuName"`
	ReservationTerm string  `json:"reservationTerm,omitempty"`
}

// AzurePricingResponse represents the response from Azure Retail Prices API
type AzurePricingResponse struct {
	BillingCurrency string             `json:"BillingCurrency"`
	Items           []AzurePricingItem `json:"Items"`
	NextPageLink    string             `json:"NextPageLink"`
	Count           int                `json:"Count"`
}

type AzureSkuCapability struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

type AzureSku struct {
	Name         string               `json:"name"`
	ResourceType string               `json:"resourceType"`
	Locations    []string             `json:"locations"`
	Family       string               `json:"family"`
	Capabilities []AzureSkuCapability `json:"capabilities"`
}

type AzureSkuResponse struct {
	Value    []AzureSku `json:"value"`
	NextLink string     `json:"nextLink"`
}

const (
	azureInstanceTypesURL = "https://azure.microsoft.com/api/v3/pricing/virtual-machines/page/details/linux/?culture=en-us&showLowPriorityOffers=false"
	azurePricingFilter    = "serviceName eq 'Virtual Machines' and contains(productName, 'Linux')"
	azureSkusTimeout      = 10 * time.Minute
)

// downloadPricingItems downloads pricing items with pagination support
func (p *AzureProvider) downloadPricingItems(region string) ([]AzurePricingItem, string, error) {
	client := &http.Client{
		Timeout: 60 * time.Second,
	}

	baseURL := "https://prices.azure.com/api/retail/prices"
	params := url.Values{}
	params.Add("$filter", azurePricingFilter+" and armRegionName eq '"+region+"'")
	initialURL := baseURL + "?" + params.Encode()

	var items []AzurePricingItem
	url := initialURL
	pageNum := 0
	for url != "" {
		pageNum++
		log.Printf("Downloading Azure pricing data (page %d)...", pageNum)

		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			return nil, "", fmt.Errorf("failed to create request: %v", err)
		}

		resp, err := client.Do(req)
		if err != nil {
			return nil, "", fmt.Errorf("download failed: %v", err)
		}

		if resp.StatusCode != http.StatusOK {
			resp.Body.Close()
			return nil, "", fmt.Errorf("HTTP error: %s", resp.Status)
		}

		body, err := io.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			return nil, "", fmt.Errorf("failed to read response: %v", err)
		}

		var pricingResp AzurePricingResponse
		if err := json.Unmarshal(body, &pricingResp); err != nil {
			return nil, "", fmt.Errorf("failed to parse pricing response: %v", err)
		}

		items = append(items, pricingResp.Items...)

		if pricingResp.NextPageLink != "" {
			url = pricingResp.NextPageLink
		} else {
			url = ""
		}
	}

	return items, initialURL, nil
}

// downloadMetadata downloads the Azure metadata
func (p *AzureProvider) downloadMetadata() ([]byte, error) {
	log.Printf("Downloading Azure metadata...")

	client := &http.Client{
		Timeout: 30 * time.Second,
	}

	req, err := http.NewRequest("GET", azureInstanceTypesURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %v", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("download failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("HTTP error: %s", resp.Status)
	}

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %v", err)
	}

	return data, nil
}

func (p *AzureProvider) getAccessToken() (string, error) {
	if token := strings.TrimSpace(os.Getenv("AZURE_ACCESS_TOKEN")); token != "" {
		return token, nil
	}

	tenantID := strings.TrimSpace(os.Getenv("AZURE_TENANT_ID"))
	clientID := strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID"))
	clientSecret := strings.TrimSpace(os.Getenv("AZURE_CLIENT_SECRET"))
	if tenantID == "" || clientID == "" || clientSecret == "" {
		return "", fmt.Errorf("Azure credentials not found")
	}

	form := url.Values{}
	form.Set("grant_type", "client_credentials")
	form.Set("client_id", clientID)
	form.Set("client_secret", clientSecret)
	form.Set("scope", "https://management.azure.com/.default")

	tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID)
	req, err := http.NewRequest("POST", tokenURL, bytes.NewBufferString(form.Encode()))
	if err != nil {
		return "", fmt.Errorf("failed to create token request: %v", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("token request failed: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return "", fmt.Errorf("token request failed: %s (%s)", resp.Status, string(body))
	}

	var tokenResp struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", fmt.Errorf("failed to parse token response: %v", err)
	}
	if tokenResp.AccessToken == "" {
		return "", fmt.Errorf("empty access token")
	}

	return tokenResp.AccessToken, nil
}

type azureHTTPStatusError struct {
	StatusCode int
	Status     string
	Body       string
}

func (e *azureHTTPStatusError) Error() string {
	return fmt.Sprintf("skus request failed: %s (%s)", e.Status, e.Body)
}

func (p *AzureProvider) downloadComputeSkus(token, subscription, region string) ([]AzureSku, string, error) {
	client := &http.Client{Timeout: azureSkusTimeout}

	baseURL := fmt.Sprintf("https://management.azure.com/subscriptions/%s/providers/Microsoft.Compute/skus", subscription)
	query := url.Values{}
	query.Set("api-version", "2021-07-01")

	filteredURL := baseURL + "?" + query.Encode()
	if region != "" {
		filterQuery := url.Values{}
		for key, values := range query {
			filterQuery[key] = values
		}
		filterQuery.Set("$filter", fmt.Sprintf("location eq '%s' and resourceType eq 'virtualMachines'", region))
		filteredURL = baseURL + "?" + filterQuery.Encode()
	}

	skus, source, err := p.downloadComputeSkusFromURL(client, token, filteredURL)
	if err == nil || region == "" {
		return skus, source, err
	}

	var statusErr *azureHTTPStatusError
	if !errors.As(err, &statusErr) || statusErr.StatusCode != http.StatusBadRequest ||
		!strings.Contains(strings.ToLower(statusErr.Body), "invalidfilter") {
		return nil, "", err
	}

	log.Printf("Azure compute SKUs filter rejected; retrying without filter")
	unfilteredURL := baseURL + "?" + query.Encode()
	skus, source, err = p.downloadComputeSkusFromURL(client, token, unfilteredURL)
	if err != nil {
		return nil, "", err
	}
	return skus, source, nil
}

func (p *AzureProvider) downloadComputeSkusFromURL(client *http.Client, token, startURL string) ([]AzureSku, string, error) {
	var all []AzureSku
	url := startURL
	pageNum := 0
	for url != "" {
		pageNum++
		log.Printf("Downloading Azure compute SKUs (page %d)...", pageNum)

		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			return nil, "", fmt.Errorf("failed to create skus request: %v", err)
		}
		req.Header.Set("Authorization", "Bearer "+token)

		resp, err := client.Do(req)
		if err != nil {
			return nil, "", fmt.Errorf("skus request failed: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(resp.Body)
			resp.Body.Close()
			return nil, "", &azureHTTPStatusError{
				StatusCode: resp.StatusCode,
				Status:     resp.Status,
				Body:       string(body),
			}
		}

		var skuResp AzureSkuResponse
		if err := json.NewDecoder(resp.Body).Decode(&skuResp); err != nil {
			resp.Body.Close()
			return nil, "", fmt.Errorf("failed to parse skus response: %v", err)
		}
		resp.Body.Close()

		all = append(all, skuResp.Value...)
		if skuResp.NextLink != "" {
			url = skuResp.NextLink
		} else {
			url = ""
		}
	}

	return all, startURL, nil
}

func normalizeAzureSkuName(name string) string {
	name = strings.TrimPrefix(name, "Standard_")
	name = strings.ReplaceAll(name, "_", "")
	return strings.ToLower(name)
}

func capabilityValue(sku AzureSku, key string) string {
	for _, c := range sku.Capabilities {
		if strings.EqualFold(c.Name, key) {
			return c.Value
		}
	}
	return ""
}

func hasPositiveCapability(sku AzureSku, key string) bool {
	val := strings.TrimSpace(capabilityValue(sku, key))
	if val == "" {
		return false
	}
	parsed, err := strconv.ParseFloat(val, 64)
	if err != nil {
		return false
	}
	return parsed > 0
}

func azureSeriesHasLocalSSD(series string) bool {
	series = strings.ToLower(strings.TrimSpace(series))
	if series == "" {
		return false
	}
	return strings.Contains(series, "lsv") || strings.Contains(series, "lasv")
}

func azureSkuHasLocalSSD(sku AzureSku) bool {
	if hasPositiveCapability(sku, "NvmeDiskSizeInMiB") || hasPositiveCapability(sku, "NvmeSizePerDiskInMiB") {
		return true
	}
	return azureSeriesHasLocalSSD(sku.Family)
}

func containsString(list []string, value string) bool {
	for _, v := range list {
		if strings.EqualFold(v, value) {
			return true
		}
	}
	return false
}

func (p *AzureProvider) DownloadRaw(region string) (*RawBundle, error) {
	var parts []RawPart

	items, pricingSource, err := p.downloadPricingItems(region)
	if err != nil {
		return nil, err
	}
	pricingBytes, err := json.Marshal(items)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal pricing items: %v", err)
	}
	parts = append(parts, RawPart{
		Scope:  region,
		Name:   "pricing",
		Data:   pricingBytes,
		Source: pricingSource,
	})

	subscription := strings.TrimSpace(os.Getenv("AZURE_SUBSCRIPTION_ID"))
	var token string
	var tokenErr error
	if subscription != "" {
		token, tokenErr = p.getAccessToken()
	} else {
		tokenErr = fmt.Errorf("AZURE_SUBSCRIPTION_ID not set")
	}
	var skuErr error
	hasComputeSkus := false
	if tokenErr == nil {
		skus, skuSource, err := p.downloadComputeSkus(token, subscription, region)
		if err != nil {
			log.Printf("Azure compute SKUs download failed: %v", err)
			skuErr = err
		} else if len(skus) == 0 {
			log.Printf("Azure compute SKUs download returned no records; falling back to public metadata")
			skuErr = fmt.Errorf("compute skus response empty")
		} else {
			skuBytes, err := json.Marshal(skus)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal compute skus: %v", err)
			}
			parts = append(parts, RawPart{
				Scope:  "global",
				Name:   "compute_skus",
				Data:   skuBytes,
				Source: skuSource,
			})
			hasComputeSkus = true
		}
	} else {
		skuErr = tokenErr
		log.Printf("Azure compute SKUs auth unavailable; falling back to public metadata: %v", skuErr)
	}

	if !hasComputeSkus {
		metadataBytes, err := p.downloadMetadata()
		if err != nil {
			if skuErr != nil {
				return nil, fmt.Errorf("failed to download compute skus (%v) and metadata: %w", skuErr, err)
			}
			return nil, err
		}
		parts = append(parts, RawPart{
			Scope:  "global",
			Name:   "metadata",
			Data:   metadataBytes,
			Source: azureInstanceTypesURL,
		})
	}

	return &RawBundle{
		Provider: "azure",
		Region:   region,
		Parts:    parts,
	}, nil
}

func (p *AzureProvider) 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 pricingBytes []byte
	var metadataBytes []byte
	var skuBytes []byte
	for _, part := range raw.Parts {
		switch part.Name {
		case "pricing":
			pricingBytes = part.Data
		case "metadata":
			metadataBytes = part.Data
		case "compute_skus":
			skuBytes = part.Data
		}
	}

	if len(pricingBytes) == 0 {
		return nil, fmt.Errorf("missing pricing data")
	}

	var pricingItems []AzurePricingItem
	if err := json.Unmarshal(pricingBytes, &pricingItems); err != nil {
		return nil, fmt.Errorf("failed to parse pricing items: %v", err)
	}

	type azurePrice struct {
		Price    float64
		Currency string
	}

	pricingMap := make(map[string]azurePrice)
	for _, item := range pricingItems {
		if item.Type != "Consumption" {
			continue
		}
		if strings.Contains(strings.ToLower(item.SkuName), "spot") ||
			strings.Contains(strings.ToLower(item.SkuName), "low priority") {
			continue
		}
		if item.ArmRegionName != raw.Region {
			continue
		}

		instanceType := item.ArmSkuName
		if instanceType == "" {
			instanceType = item.SkuName
		}
		instanceType = normalizeAzureSkuName(instanceType)

		if existing, ok := pricingMap[instanceType]; !ok || item.RetailPrice < existing.Price {
			pricingMap[instanceType] = azurePrice{Price: item.RetailPrice, Currency: item.CurrencyCode}
		}
	}

	unifiedData := types.NewUnifiedData("azure")

	categoryToFamilies := map[string]string{
		"generalpurpose":     "General purpose",
		"computeoptimized":   "Compute optimized",
		"memoryoptimized":    "Memory optimized",
		"storageoptimized":   "Storage optimized",
		"gpu":                "GPU instance",
		"acceleratedcompute": "Accelerated Computing",
	}

	var instances []types.UnifiedInstance

	if len(skuBytes) > 0 {
		var skus []AzureSku
		if err := json.Unmarshal(skuBytes, &skus); err != nil {
			return nil, fmt.Errorf("failed to parse compute skus: %v", err)
		}

		for _, sku := range skus {
			if sku.ResourceType != "virtualMachines" {
				continue
			}
			if !containsString(sku.Locations, raw.Region) {
				continue
			}

			instanceType := normalizeAzureSkuName(sku.Name)
			price, ok := pricingMap[instanceType]
			if !ok {
				continue
			}

			vcpu := capabilityValue(sku, "vCPUs")
			mem := capabilityValue(sku, "MemoryGB")

			memory := ""
			if mem != "" {
				if val, err := strconv.ParseFloat(mem, 64); err == nil {
					memory = fmt.Sprintf("%.0f GiB", val)
				}
			}

			instanceFamily := sku.Family
			if instanceFamily == "" {
				instanceFamily = "General purpose"
			}

			storage := "Varies"
			if azureSkuHasLocalSSD(sku) {
				storage = "Local SSD"
			}

			instance := types.UnifiedInstance{
				Provider:           "azure",
				InstanceType:       instanceType,
				InstanceFamily:     instanceFamily,
				VCPU:               vcpu,
				Memory:             memory,
				Storage:            storage,
				NetworkPerformance: "Unknown",
				Price:              price.Price,
				Currency:           price.Currency,
				Location:           regions.GetAzureRegionName(raw.Region),
				Region:             raw.Region,
			}

			instances = append(instances, instance)
		}
	} else if len(metadataBytes) > 0 {
		var azureData AzureData
		if err := json.Unmarshal(metadataBytes, &azureData); err != nil {
			return nil, fmt.Errorf("failed to parse Azure metadata: %v", err)
		}

		for offerKey, attrs := range azureData.AttributesByOffer {
			if attrs.Type != "linux" {
				continue
			}

			instanceType := strings.TrimPrefix(offerKey, "linux-")
			instanceType = strings.TrimSuffix(instanceType, "-standard")
			instanceType = strings.ReplaceAll(instanceType, "-", ".")

			price, ok := pricingMap[instanceType]
			if !ok {
				continue
			}

			instanceFamily := categoryToFamilies[attrs.Category]
			if instanceFamily == "" {
				instanceFamily = attrs.Category
			}

			memory := fmt.Sprintf("%.0f GiB", attrs.RAM)
			storage := "Varies"
			if azureSeriesHasLocalSSD(attrs.Series) {
				storage = "Local SSD"
			} else if attrs.DiskSize > 0 {
				storage = fmt.Sprintf("%d GB", attrs.DiskSize)
			}

			instance := types.UnifiedInstance{
				Provider:           "azure",
				InstanceType:       instanceType,
				InstanceFamily:     instanceFamily,
				VCPU:               fmt.Sprintf("%d", attrs.Cores),
				Memory:             memory,
				Storage:            storage,
				NetworkPerformance: "Unknown",
				Price:              price.Price,
				Currency:           price.Currency,
				Location:           regions.GetAzureRegionName(raw.Region),
				Region:             raw.Region,
			}

			instances = append(instances, instance)
		}
	} else {
		return nil, fmt.Errorf("missing metadata or compute skus")
	}

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

	return unifiedData, nil
}

func (p *AzureProvider) GetRegions() map[string]string {
	return regions.AzureRegions
}

func (p *AzureProvider) IsValidRegion(regionID string) bool {
	return regions.IsValidAzureRegion(regionID)
}

func (p *AzureProvider) GetRegionName(regionID string) string {
	return regions.GetAzureRegionName(regionID)
}

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

func (p *AzureProvider) GetProviderName() string {
	return "azure"
}
