package provider

import (
	"bytes"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	neturl "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"
)

// GCPProvider implements the CloudProvider interface for GCP
// Pricing uses Cloud Billing Catalog API, specs use Compute Engine API.
type GCPProvider struct{}

// NewGCPProvider creates a new GCP provider instance
func NewGCPProvider() *GCPProvider {
	return &GCPProvider{}
}

type gcpService struct {
	Name        string `json:"name"`
	DisplayName string `json:"displayName"`
}

type gcpServiceList struct {
	Services      []gcpService `json:"services"`
	NextPageToken string       `json:"nextPageToken"`
}

type gcpSkuCategory struct {
	ResourceFamily string `json:"resourceFamily"`
	UsageType      string `json:"usageType"`
	ResourceGroup  string `json:"resourceGroup"`
}

type gcpUnitPrice struct {
	Units string `json:"units"`
	Nanos int64  `json:"nanos"`
}

type gcpTieredRate struct {
	UnitPrice gcpUnitPrice `json:"unitPrice"`
}

type gcpPricingExpression struct {
	UsageUnit   string          `json:"usageUnit"`
	TieredRates []gcpTieredRate `json:"tieredRates"`
}

type gcpPricingInfo struct {
	PricingExpression gcpPricingExpression `json:"pricingExpression"`
}

type gcpSku struct {
	Name           string           `json:"name"`
	Description    string           `json:"description"`
	Category       gcpSkuCategory   `json:"category"`
	ServiceRegions []string         `json:"serviceRegions"`
	PricingInfo    []gcpPricingInfo `json:"pricingInfo"`
}

type gcpSkuList struct {
	Skus          []gcpSku `json:"skus"`
	NextPageToken string   `json:"nextPageToken"`
}

type gcpMachineType struct {
	Name      string `json:"name"`
	GuestCpus int    `json:"guestCpus"`
	MemoryMb  int64  `json:"memoryMb"`
	Zone      string `json:"zone"`
}

type gcpAggregatedMachineTypesResponse struct {
	Items map[string]struct {
		MachineTypes []gcpMachineType `json:"machineTypes"`
	} `json:"items"`
	NextPageToken string `json:"nextPageToken"`
}

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

	credPath := strings.TrimSpace(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"))
	if credPath == "" {
		return "", fmt.Errorf("GCP credentials not found")
	}
	data, err := os.ReadFile(credPath)
	if err != nil {
		return "", fmt.Errorf("failed to read GCP credentials: %v", err)
	}

	var sa struct {
		ClientEmail string `json:"client_email"`
		PrivateKey  string `json:"private_key"`
		TokenURI    string `json:"token_uri"`
	}
	if err := json.Unmarshal(data, &sa); err != nil {
		return "", fmt.Errorf("failed to parse GCP credentials: %v", err)
	}
	if sa.ClientEmail == "" || sa.PrivateKey == "" || sa.TokenURI == "" {
		return "", fmt.Errorf("invalid GCP credentials file")
	}

	now := time.Now().Unix()
	claims := map[string]interface{}{
		"iss":   sa.ClientEmail,
		"scope": "https://www.googleapis.com/auth/compute.readonly https://www.googleapis.com/auth/cloud-billing",
		"aud":   sa.TokenURI,
		"iat":   now,
		"exp":   now + 3600,
	}

	header := map[string]string{"alg": "RS256", "typ": "JWT"}
	headerJSON, _ := json.Marshal(header)
	claimsJSON, _ := json.Marshal(claims)

	unsigned := base64.RawURLEncoding.EncodeToString(headerJSON) + "." + base64.RawURLEncoding.EncodeToString(claimsJSON)

	block, _ := pem.Decode([]byte(sa.PrivateKey))
	if block == nil {
		return "", fmt.Errorf("failed to decode private key")
	}
	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return "", fmt.Errorf("failed to parse private key: %v", err)
	}
	rsaKey, ok := key.(*rsa.PrivateKey)
	if !ok {
		return "", fmt.Errorf("private key is not RSA")
	}

	h := sha256.Sum256([]byte(unsigned))
	sig, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, h[:])
	if err != nil {
		return "", fmt.Errorf("failed to sign JWT: %v", err)
	}

	jwt := unsigned + "." + base64.RawURLEncoding.EncodeToString(sig)

	form := neturl.Values{}
	form.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
	form.Set("assertion", jwt)

	req, err := http.NewRequest("POST", sa.TokenURI, 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
}

func (p *GCPProvider) downloadServices(apiKey, token string) ([]gcpService, error) {
	client := &http.Client{Timeout: 30 * time.Second}
	baseURL := "https://cloudbilling.googleapis.com/v1/services"

	var services []gcpService
	pageURL := baseURL
	for pageURL != "" {
		req, err := http.NewRequest("GET", pageURL, nil)
		if err != nil {
			return nil, err
		}
		q := req.URL.Query()
		if apiKey != "" {
			q.Set("key", apiKey)
		}
		req.URL.RawQuery = q.Encode()
		if token != "" {
			req.Header.Set("Authorization", "Bearer "+token)
		}

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

		var list gcpServiceList
		if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
			resp.Body.Close()
			return nil, err
		}
		resp.Body.Close()

		services = append(services, list.Services...)
		if list.NextPageToken != "" {
			pageURL = baseURL + "?pageToken=" + neturl.QueryEscape(list.NextPageToken)
		} else {
			pageURL = ""
		}
	}

	return services, nil
}

func (p *GCPProvider) downloadSkus(serviceName, apiKey, token string) ([]gcpSku, error) {
	client := &http.Client{Timeout: 60 * time.Second}
	baseURL := fmt.Sprintf("https://cloudbilling.googleapis.com/v1/%s/skus", serviceName)

	var skus []gcpSku
	pageURL := baseURL
	for pageURL != "" {
		req, err := http.NewRequest("GET", pageURL, nil)
		if err != nil {
			return nil, err
		}
		q := req.URL.Query()
		if apiKey != "" {
			q.Set("key", apiKey)
		}
		req.URL.RawQuery = q.Encode()
		if token != "" {
			req.Header.Set("Authorization", "Bearer "+token)
		}

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

		var list gcpSkuList
		if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
			resp.Body.Close()
			return nil, err
		}
		resp.Body.Close()

		skus = append(skus, list.Skus...)
		if list.NextPageToken != "" {
			pageURL = baseURL + "?pageToken=" + neturl.QueryEscape(list.NextPageToken)
		} else {
			pageURL = ""
		}
	}

	return skus, nil
}

func (p *GCPProvider) downloadMachineTypes(project, token string) ([]gcpMachineType, error) {
	client := &http.Client{Timeout: 60 * time.Second}
	baseURL := fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/machineTypes", project)

	var all []gcpMachineType
	pageURL := baseURL
	for pageURL != "" {
		req, err := http.NewRequest("GET", pageURL, nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+token)

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

		var list gcpAggregatedMachineTypesResponse
		if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
			resp.Body.Close()
			return nil, err
		}
		resp.Body.Close()

		for key, item := range list.Items {
			zone := strings.TrimPrefix(key, "zones/")
			for _, mt := range item.MachineTypes {
				mt.Zone = zone
				all = append(all, mt)
			}
		}

		if list.NextPageToken != "" {
			pageURL = baseURL + "?pageToken=" + neturl.QueryEscape(list.NextPageToken)
		} else {
			pageURL = ""
		}
	}

	return all, nil
}

func unitPriceToFloat(price gcpUnitPrice) float64 {
	units, _ := strconv.ParseFloat(price.Units, 64)
	return units + float64(price.Nanos)/1e9
}

func zoneToRegion(zone string) string {
	parts := strings.Split(zone, "-")
	if len(parts) < 2 {
		return zone
	}
	return strings.Join(parts[:len(parts)-1], "-")
}

func (p *GCPProvider) DownloadRaw(region string) (*RawBundle, error) {
	project := strings.TrimSpace(os.Getenv("GCP_PROJECT_ID"))
	if project == "" {
		return nil, fmt.Errorf("GCP_PROJECT_ID is required")
	}

	apiKey := strings.TrimSpace(os.Getenv("GCP_API_KEY"))
	token, tokenErr := p.getAccessToken()
	if tokenErr != nil {
		return nil, fmt.Errorf("GCP metadata requires authentication; set GCP_ACCESS_TOKEN or GOOGLE_APPLICATION_CREDENTIALS")
	}

	services, err := p.downloadServices(apiKey, token)
	if err != nil {
		return nil, err
	}

	serviceName := ""
	for _, svc := range services {
		if strings.Contains(strings.ToLower(svc.DisplayName), "compute engine") {
			serviceName = svc.Name
			break
		}
	}
	if serviceName == "" {
		return nil, fmt.Errorf("Compute Engine service not found in billing catalog")
	}

	skus, err := p.downloadSkus(serviceName, apiKey, token)
	if err != nil {
		return nil, err
	}
	machineTypes, err := p.downloadMachineTypes(project, token)
	if err != nil {
		return nil, err
	}

	skuBytes, err := json.Marshal(skus)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal billing skus: %v", err)
	}
	machineBytes, err := json.Marshal(machineTypes)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal machine types: %v", err)
	}

	return &RawBundle{
		Provider: "gcp",
		Region:   region,
		Parts: []RawPart{
			{Scope: "global", Name: "billing_skus", Data: skuBytes, Source: "CloudBilling"},
			{Scope: region, Name: "machine_types", Data: machineBytes, Source: "ComputeEngine"},
		},
	}, nil
}

func (p *GCPProvider) 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 skuBytes []byte
	var machineBytes []byte
	for _, part := range raw.Parts {
		switch part.Name {
		case "billing_skus":
			skuBytes = part.Data
		case "machine_types":
			machineBytes = part.Data
		}
	}

	if len(skuBytes) == 0 || len(machineBytes) == 0 {
		return nil, fmt.Errorf("missing billing skus or machine types")
	}

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

	var machineTypes []gcpMachineType
	if err := json.Unmarshal(machineBytes, &machineTypes); err != nil {
		return nil, fmt.Errorf("failed to parse machine types: %v", err)
	}

	cpuPrice := make(map[string]float64)
	memPrice := make(map[string]float64)

	for _, sku := range skus {
		if sku.Category.ResourceFamily != "Compute" || sku.Category.UsageType != "OnDemand" {
			continue
		}
		desc := strings.ToLower(sku.Description)
		if strings.Contains(desc, "preemptible") {
			continue
		}
		if len(sku.PricingInfo) == 0 || len(sku.PricingInfo[0].PricingExpression.TieredRates) == 0 {
			continue
		}

		price := unitPriceToFloat(sku.PricingInfo[0].PricingExpression.TieredRates[0].UnitPrice)
		for _, region := range sku.ServiceRegions {
			if strings.Contains(desc, "cpu") {
				if cpuPrice[region] == 0 || price < cpuPrice[region] {
					cpuPrice[region] = price
				}
			}
			if strings.Contains(desc, "ram") || strings.Contains(desc, "memory") {
				if memPrice[region] == 0 || price < memPrice[region] {
					memPrice[region] = price
				}
			}
		}
	}

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

	for _, mt := range machineTypes {
		region := zoneToRegion(mt.Zone)
		if region != raw.Region {
			continue
		}

		cpuRate := cpuPrice[region]
		memRate := memPrice[region]
		if cpuRate == 0 || memRate == 0 {
			continue
		}

		memGiB := float64(mt.MemoryMb) / 1024.0
		price := float64(mt.GuestCpus)*cpuRate + memGiB*memRate

		family := strings.Split(mt.Name, "-")[0]
		instance := types.UnifiedInstance{
			Provider:       "gcp",
			InstanceType:   mt.Name,
			InstanceFamily: family,
			VCPU:           fmt.Sprintf("%d", mt.GuestCpus),
			Memory:         fmt.Sprintf("%.1f GiB", memGiB),
			Storage:        gcpStorageLabel(mt.Name),
			Price:          price,
			Currency:       "USD",
			Location:       regions.GetGCPRegionName(region),
			Region:         region,
		}

		instances = append(instances, instance)
	}

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

	return unifiedData, nil
}

func gcpStorageLabel(machineType string) string {
	if gcpHasLocalSSD(machineType) {
		return "Local SSD"
	}
	return "Varies"
}

func gcpHasLocalSSD(machineType string) bool {
	name := strings.ToLower(machineType)
	if strings.Contains(name, "-lssd") {
		return true
	}
	if strings.HasPrefix(name, "z3-") || strings.HasPrefix(name, "a4-") || strings.HasPrefix(name, "a4x-") || strings.HasPrefix(name, "a3-") {
		return true
	}
	if strings.HasPrefix(name, "a2-ultra") || strings.HasPrefix(name, "a2-ultragpu") {
		return true
	}
	return false
}

func (p *GCPProvider) GetRegions() map[string]string {
	return regions.GCPRegions
}

func (p *GCPProvider) IsValidRegion(regionID string) bool {
	return regions.IsValidGCPRegion(regionID)
}

func (p *GCPProvider) GetRegionName(regionID string) string {
	return regions.GetGCPRegionName(regionID)
}

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

func (p *GCPProvider) GetProviderName() string {
	return "gcp"
}
