package cloudproviderhandler

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/utils/httpclient"
)

// ServiceCode for GCP
const (
	GCPNodeCompute               = "GCP_Compute"
	GCPNodeComputeCustom         = "GCP_Compute_Custom"
	GCPNodeStorage               = "GCP_Storage_Size"
	GCPNodeStorageIOPS           = "GCP_Storage_IOPS"
	GCPNodeStorageAdditionalIOPS = "GCP_Storage_IOPS_Addtional"
	GCPNodeStorageThroughPut     = "GCP_Storage_Throughput"
	GCPBackUp                    = "GCP_Regions_Storage"
	GCPNATData                   = "NAT_Data"
	GCPDualRegionStorage         = "GCP_Dual_Regions_Storage"
	GCPDualRegionReplication     = "GCP_Dual_Regions_Replication"
	GCPPrivateLink               = "PrivateLink"
	// extended service code for GCPNodeCompute
	GCPNodeComputeVirtualCPU       = "predefine_runing_cores"
	GCPNodeComputeMemory           = "predefine_running_ram"
	GCPNodeComputeVirtualCPUCustom = "custom_runing_cores"
	GCPNodeComputeMemoryCustom     = "custom_running_ram"
)

// sku group mapper
var NodeComputeSkuGroupMapper = map[string]string{
	"n1":  "8ce814d1-c044-48dd-a703-8644509ce4d7", // On-Demand N1 VMs
	"n2":  "17a30940-be45-4da0-96c7-4754efbaa1f6", // On-Demand N2 VMs
	"n2d": "b40fef10-6d86-4149-966d-badfe50ec373", // On-Demand N2D VMs
	"t2d": "e4c3ff43-afe1-49d5-a023-7f2d27864164", // On-Demand T2D VMs
}
var SkuGroupMapper = map[string]string{
	GCPNodeStorage:       "85f37461-0f73-4430-a9a7-cee7b42807bb", // pd-ssd: On-Demand Persistent Disk SSD
	GCPBackUp:            "93f11c8c-0b8f-4d3e-a3b9-80cce2373885", // Standard Storage (Regional)
	GCPDualRegionStorage: "ab766c43-6906-42c7-8889-a3634f3404a6", // Standard Storage
}

var DualRegionReplicationSkuMapper = map[string]string{
	"asia":      "CA61-E18A-B2D6", // Network Data Transfer GCP Replication within Asia
	"europe":    "CB83-3C2D-160D", // Network Data Transfer GCP Replication within Europe
	"us":        "AED0-3315-7B11", // Network Data Transfer GCP Replication within Northern America
	"australia": "1193-6316-413E", // Network Data Transfer GCP Replication within Oceania
}
var PrivateLinkDataProcessSkuID = "C40A-084F-09B4" // Networking Private Service Connect Consumer Data Processing

// response structure defined in GCP price API
type TaxonomyCategory struct {
	Category string `json:"category"`
}

type ProductTaxonomy struct {
	TaxonomyCategories []TaxonomyCategory `json:"taxonomyCategories"`
}

type Region struct {
	Region string `json:"region"`
}

type Regional struct {
	Region Region `json:"region"`
}

type MultiRegional struct {
	Regions []Region `json:"regions"`
}

type GeoTaxonomy struct {
	Type                  string        `json:"type"`
	RegionalMetadata      Regional      `json:"regionalMetadata"`
	MultiRegionalMetadata MultiRegional `json:"multiRegionalMetadata"`
}

type SkuGroupSku struct {
	Name            string          `json:"name"`
	SkuID           string          `json:"skuId"`
	DisplayName     string          `json:"displayName"`
	ProductTaxonomy ProductTaxonomy `json:"productTaxonomy"`
	GeoTaxonomy     GeoTaxonomy     `json:"geoTaxonomy"`
}

type Decimal struct {
	Value string `json:"value"`
}

type Money struct {
	CurrencyCode string `json:"currencyCode"`
	Units        string `json:"units"`
	Nanos        int    `json:"nanos"`
}

type RateTier struct {
	StartAmount Decimal `json:"startAmount"`
	ListPrice   Money   `json:"listPrice"`
}

type UnitInfo struct {
	Unit            string  `json:"unit"`
	UnitDescription string  `json:"unitDescription"`
	UnitQuantity    Decimal `json:"unitQuantity"`
}

type Rate struct {
	Tiers    []RateTier `json:"tiers"`
	UnitInfo UnitInfo   `json:"unitInfo"`
}

type SkuPriceResponse struct {
	Name         string `json:"name"`
	CurrencyCode string `json:"currencyCode"`
	ValueType    string `json:"valueType"`
	Rate         Rate   `json:"rate"`
}

type SkuGroupListResponse struct {
	SkuGroupSkus  []SkuGroupSku `json:"skuGroupSkus"`
	NextPageToken string        `json:"nextPageToken"`
}

type GcpPriceHandlerImpl struct{}

func (g *GcpPriceHandlerImpl) GetPrice(params map[string]string) (*forms.ProviderPriceRes, error) {
	if !config.Get().GetBool(config.GCPSkuPriceQueryEnable) {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("GCP price query is not allowed"))
	}
	// if it is GCPNodeStorageIOPS, GCPNodeStorageAdditionalIOPS, GCPNodeStorageThroughPut, no cost
	if _, ok := map[string]struct{}{
		NodeStorageThroughPut:        {},
		GCPNodeStorageIOPS:           {},
		GCPNodeStorageAdditionalIOPS: {},
	}[params["service_code"]]; ok {
		price := decimal.Zero
		return &forms.ProviderPriceRes{Price: &price}, nil
	}
	return g.querySkuPrice(params)
}

func (g *GcpPriceHandlerImpl) querySkuPrice(params map[string]string) (*forms.ProviderPriceRes, error) {
	if serviceCode, ok := params["service_code"]; !ok || serviceCode == "" {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("missing service_code"))
	}
	serviceCode := params["service_code"]
	if region, ok := params["region"]; !ok || region == "" {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("missing region"))
	}
	region := params["region"]
	if serviceCode == GCPNodeCompute || serviceCode == GCPNodeComputeCustom {
		// cpu * num + memory * num
		if skuGroupId, ok := NodeComputeSkuGroupMapper[strings.ToLower(params["machineSeries"])]; ok {
			virtualCpuCount, err := strconv.ParseFloat(params["virtualCpuCount"], 32)
			if err != nil || virtualCpuCount <= 0 {
				return nil, billing_errors.NewInvalidInputError(fmt.Errorf("invalid virtual_cpu_count: %v", err))
			}
			newServiceCodeForCPU := GCPNodeComputeVirtualCPU
			newServiceCodeForMemory := GCPNodeComputeMemory
			if serviceCode == GCPNodeComputeCustom {
				newServiceCodeForCPU = GCPNodeComputeVirtualCPUCustom
				newServiceCodeForMemory = GCPNodeComputeMemoryCustom
			}
			runningCoreSkuPrice, err := g.filterAndGetTargetSkuPrice(skuGroupId, newServiceCodeForCPU, region)
			if err != nil {
				return nil, err
			}
			memorySizeGB, err := strconv.ParseFloat(params["memorySizeGB"], 32)
			if err != nil || memorySizeGB <= 0 {
				return nil, billing_errors.NewInvalidInputError(fmt.Errorf("invalid memory_size_gb: %v", err))
			}
			runningRamSkuPrice, err := g.filterAndGetTargetSkuPrice(skuGroupId, newServiceCodeForMemory, region)
			if err != nil {
				return nil, err
			}
			unitPrice := runningCoreSkuPrice.Mul(decimal.NewFromFloat(virtualCpuCount)).Add(runningRamSkuPrice.Mul(decimal.NewFromFloat(memorySizeGB)))
			return &forms.ProviderPriceRes{Price: &unitPrice}, nil
		}
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("unknown machine series: %s", params["machine_series"]))
	} else if serviceCode == GCPDualRegionReplication {
		// global, direct query the sku price
		for region, skuId := range DualRegionReplicationSkuMapper {
			if strings.Contains(strings.ToLower(params["region"]), region+"-") {
				skuPrice, err := querySkuPriceBySkuId(skuId)
				if err != nil {
					return nil, err
				}
				return &forms.ProviderPriceRes{Price: &skuPrice}, nil
			}
		}
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("unknown region: %s", params["region"]))
	} else if serviceCode == GCPPrivateLink {
		// gobal, direct query the sku price
		skuPrice, err := querySkuPriceBySkuId(PrivateLinkDataProcessSkuID)
		if err != nil {
			return nil, err
		}
		return &forms.ProviderPriceRes{Price: &skuPrice}, nil
	} else {
		if _, ok := SkuGroupMapper[serviceCode]; !ok {
			return nil, billing_errors.NewInvalidInputError(fmt.Errorf("unknown service code: %s", params["service_code"]))
		}
		skuPrice, err := g.filterAndGetTargetSkuPrice(SkuGroupMapper[serviceCode], serviceCode, region)
		if err != nil {
			return nil, err
		}
		return &forms.ProviderPriceRes{Price: &skuPrice}, nil
	}
}

func (*GcpPriceHandlerImpl) filterAndGetTargetSkuPrice(skuGroupId string, serviceCode string, region string) (decimal.Decimal, error) {
	pageToken := ""
	targetSkuIds := []string{}
	for {
		// list all skus by skuGroupId
		skuGroupListResponse, err := listSkusBySkuGroupId(skuGroupId, pageToken)
		if err != nil {
			return decimal.Zero, err
		}
		log.Info("list skus by skuGroupId", zap.String("skuGroupId", skuGroupId), zap.String("serviceCode", serviceCode), zap.String("region", region))
		var targetSkuId string
		// filter out the target skuId
		switch serviceCode {
		case GCPNodeComputeVirtualCPU, GCPNodeComputeMemory, GCPNodeComputeVirtualCPUCustom, GCPNodeComputeMemoryCustom:
			targetSkuId, err = filterTargetSkuIdForComputeMemoryOrVirtualCPU(skuGroupListResponse, region, serviceCode)
		case GCPNodeStorage:
			targetSkuId, err = filterTargetSkuIdForNodeStorage(skuGroupListResponse, region)
		case GCPBackUp:
			targetSkuId, err = filterTargetSkuIdForBackup(skuGroupListResponse, region)
		case GCPDualRegionStorage:
			targetSkuId, err = filterTargetSkuIdForDualRegionStorage(skuGroupListResponse, region)
		default:
			return decimal.Zero, billing_errors.NewInvalidInputError(fmt.Errorf("unknown service code: %s", serviceCode))
		}
		if err != nil {
			return decimal.Zero, err
		}
		if targetSkuId != "" {
			targetSkuIds = append(targetSkuIds, targetSkuId)
		}
		pageToken = skuGroupListResponse.NextPageToken
		if pageToken == "" {
			break
		}
	}
	log.Info("filter out target sku success", zap.String("skuGroupId", skuGroupId), zap.String("serviceCode", serviceCode), zap.String("region", region), zap.Any("targetSkuIds", targetSkuIds))
	if len(targetSkuIds) == 0 {
		return decimal.Zero, billing_errors.NewInvalidInputError(fmt.Errorf("no target skus found"))
	}
	if len(targetSkuIds) > 1 {
		return decimal.Zero, billing_errors.NewInvalidInputError(fmt.Errorf("multiple target skus found"))
	}
	// query the sku price
	return querySkuPriceBySkuId(targetSkuIds[0])
}

func listSkusBySkuGroupId(skuGroupId string, pageToken string) (*SkuGroupListResponse, error) {
	// https://cloudbilling.googleapis.com/v1beta/skuGroups/[SKU_GROUP_ID]/skus?pageSize=500&pageToken=[PAGE_TOKEN]&key=[YOUR_API_KEY]
	var skuGroupListResponse SkuGroupListResponse
	query := map[string]string{
		"pageSize": "500",
		"key":      config.Get().GetString(config.GCPPriceAPIKey),
	}
	if pageToken != "" {
		query["pageToken"] = pageToken
	}
	err := sendGCPRequestWithRetry("/skuGroups", fmt.Sprintf("/%s/skus", skuGroupId), query, &skuGroupListResponse, 3)
	if err != nil {
		return nil, err
	}
	return &skuGroupListResponse, nil
}

func querySkuPriceBySkuId(skuId string) (decimal.Decimal, error) {
	// https://cloudbilling.googleapis.com/v1beta/skus/[SKU_ID]/price?currencyCode=USD&key=[API_KEY]
	var skuPriceResponse SkuPriceResponse
	query := map[string]string{
		"currencyCode": "USD",
		"key":          config.Get().GetString(config.GCPPriceAPIKey),
	}
	err := sendGCPRequestWithRetry("/skus", fmt.Sprintf("/%s/price", skuId), query, &skuPriceResponse, 3)
	if err != nil {
		return decimal.Zero, err
	}
	for _, tier := range skuPriceResponse.Rate.Tiers {
		if tier.ListPrice.Nanos != 0 {
			skuPrice := decimal.NewFromInt(int64(tier.ListPrice.Nanos)).Div(decimal.NewFromInt(1000000000))
			// format
			formatSkuPrice := skuPrice.Div(decimal.NewFromInt(getGCPUnitDiv(skuPriceResponse.Rate.UnitInfo.Unit)))
			return formatSkuPrice, nil
		}
	}
	return decimal.Zero, nil
}

func isSkuInRegion(skuGroupSku SkuGroupSku, region string) bool {
	if skuGroupSku.GeoTaxonomy.Type == "TYPE_REGIONAL" && strings.EqualFold(skuGroupSku.GeoTaxonomy.RegionalMetadata.Region.Region, region) {
		return true
	}
	if skuGroupSku.GeoTaxonomy.Type == "TYPE_MULTI_REGIONAL" {
		for _, skuRegion := range skuGroupSku.GeoTaxonomy.MultiRegionalMetadata.Regions {
			if strings.EqualFold(skuRegion.Region, region) {
				return true
			}
		}
	}
	return false
}

func filterTargetSkuIdForComputeMemoryOrVirtualCPU(skuList *SkuGroupListResponse, region string, serviceCode string) (string, error) {
	targetSkuIds := []string{}
	for _, sku := range skuList.SkuGroupSkus {
		if !isSkuInRegion(sku, region) {
			continue
		}
		if serviceCode == GCPNodeComputeMemoryCustom || serviceCode == GCPNodeComputeVirtualCPUCustom {
			if !strings.Contains(strings.ToLower(sku.DisplayName), "custom") {
				continue
			}
			if strings.Contains(strings.ToLower(sku.DisplayName), "custom extended") {
				continue
			}
		} else {
			if strings.Contains(strings.ToLower(sku.DisplayName), "custom") || strings.Contains(strings.ToLower(sku.DisplayName), "sole tenancy") {
				continue
			}
		}
		for _, category := range sku.ProductTaxonomy.TaxonomyCategories {
			if category.Category == "Memory: Per GB" && (serviceCode == GCPNodeComputeMemoryCustom || serviceCode == GCPNodeComputeMemory) {
				targetSkuIds = append(targetSkuIds, sku.SkuID)
			}
			if category.Category == "Cores: Per Core" && (serviceCode == GCPNodeComputeVirtualCPUCustom || serviceCode == GCPNodeComputeVirtualCPU) {
				targetSkuIds = append(targetSkuIds, sku.SkuID)
			}
		}
	}
	if len(targetSkuIds) > 1 {
		return "", billing_errors.NewInvalidInputError(fmt.Errorf("multiple target skus found"))
	}
	if len(targetSkuIds) == 0 {
		return "", nil
	}
	return targetSkuIds[0], nil
}

func filterTargetSkuIdForNodeStorage(skuList *SkuGroupListResponse, region string) (string, error) {
	targetSkuIds := []string{}
	for _, sku := range skuList.SkuGroupSkus {
		if !isSkuInRegion(sku, region) {
			continue
		}
		isTargetSku := true
		for _, category := range sku.ProductTaxonomy.TaxonomyCategories {
			switch strings.ToLower(category.Category) {
			case "regional lite", "regional", "extreme", "lite":
				isTargetSku = false
			}
		}
		if isTargetSku {
			targetSkuIds = append(targetSkuIds, sku.SkuID)
		}
	}
	if len(targetSkuIds) > 1 {
		return "", billing_errors.NewInvalidInputError(fmt.Errorf("multiple target skus found"))
	}
	if len(targetSkuIds) == 0 {
		return "", nil
	}
	return targetSkuIds[0], nil
}

func filterTargetSkuIdForBackup(skuList *SkuGroupListResponse, region string) (string, error) {
	targetSkuIds := []string{}
	targetSkuIdsForRegional := []string{}
	for _, sku := range skuList.SkuGroupSkus {
		if !isSkuInRegion(sku, region) {
			continue
		}
		if sku.GeoTaxonomy.Type == "TYPE_REGIONAL" {
			targetSkuIds = append(targetSkuIds, sku.SkuID)
		}
		if sku.GeoTaxonomy.Type == "TYPE_MULTI_REGIONAL" {
			targetSkuIdsForRegional = append(targetSkuIdsForRegional, sku.SkuID)
		}
	}
	if len(targetSkuIds) > 1 {
		return "", billing_errors.NewInvalidInputError(fmt.Errorf("multiple target skus found"))
	}
	if len(targetSkuIds) == 0 {
		if len(targetSkuIdsForRegional) > 1 {
			return "", billing_errors.NewInvalidInputError(fmt.Errorf("multiple target skus found"))
		}
		if len(targetSkuIdsForRegional) == 0 {
			return "", nil
		}
		return targetSkuIdsForRegional[0], nil
	}
	return targetSkuIds[0], nil
}

func filterTargetSkuIdForDualRegionStorage(skuList *SkuGroupListResponse, region string) (string, error) {
	targetSkuIds := []string{}
	for _, sku := range skuList.SkuGroupSkus {
		if !isSkuInRegion(sku, region) {
			continue
		}
		isTargetSku := false
		for _, category := range sku.ProductTaxonomy.TaxonomyCategories {
			if strings.EqualFold(category.Category, "Ops") {
				isTargetSku = false
				break
			}
			if strings.EqualFold(category.Category, "Dual-Region") {
				isTargetSku = true
			}
		}
		if isTargetSku {
			targetSkuIds = append(targetSkuIds, sku.SkuID)
		}
	}
	if len(targetSkuIds) > 1 {
		return "", billing_errors.NewInvalidInputError(fmt.Errorf("multiple target skus found"))
	}
	if len(targetSkuIds) == 0 {
		return "", nil
	}
	return targetSkuIds[0], nil
}

func getGCPUnitDiv(unitOfMeasure string) int64 {
	// NodeComputeVirtualCPU: "h" -> per min
	// NodeComputeMemory: "GiBy.h" -> per min per GiB
	// storage size: GiBy.mo  ->  per min per GiB
	// Backup: GiBy.mo -> per min per GB
	// DualRegionStorage: GiBy.mo -> per min per GB
	// DualRegionReplication: GiBy -> no change
	if unitOfMeasure == "h" || unitOfMeasure == "GiBy.h" {
		return 60
	}
	if unitOfMeasure == "GiBy.mo" {
		return 730 * 60
	}
	return 1
}

func sendGCPRequestWithRetry(urlPrefix string, apiPath string, query map[string]string, output interface{}, retryTimes int) error {
	var lastErr error
	for i := 0; i < retryTimes; i++ {
		lastErr = httpclient.NewClient(nil, config.Get().GetString(config.GCPSkuPriceQueryUrl), urlPrefix).Request("GET", apiPath, query, nil, output)
		if lastErr == nil {
			return nil
		}
	}
	return billing_errors.NewSystemError(fmt.Errorf("request gcp price api failed after %d retries, api_path = %s, error = %v", retryTimes, apiPath, lastErr))
}
