package cloudproviderhandler

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/pingcap/dbaas/billing2/config"

	"github.com/pingcap/dbaas/billing2/forms"

	"github.com/shopspring/decimal"

	"github.com/pingcap/dbaas/billing2/utils/log"
	"go.uber.org/zap"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/pricing"
)

type AwsPriceHandlerImpl struct{}

const (
	FilterTypeTermMatch = "TERM_MATCH"

	ServiceCodeEC2      = "AmazonEC2"
	ServiceCodeS3       = "AmazonS3"
	ServiceDataTransfer = "AWSDataTransfer"
	ServiceCodeVPC      = "AmazonVPC"

	UsageTypeBoxUsage         = "BoxUsage"
	UsageTypeEBSVolume        = "EBS:VolumeUsage"
	UsageTypeEBSIOPS          = "EBS:VolumeP-IOPS"
	UsageTypeEBSThroughput    = "EBS:VolumeP-Throughput"
	UsageTypeS3Storage        = "TimedStorage-ByteHrs"
	UsageTypePrivateLink      = "VpcEndpoint-Bytes"
	UsageTypeRegionalTransfer = "DataTransfer-Regional-Bytes"
	UsageTypeNatGateway       = "NatGateway-Bytes"
	UsageTypeInterRegionOut   = "AWS-Out-Bytes"

	FilterFieldRegionCode       = "regionCode"
	FilterFieldInstanceType     = "instanceType"
	FilterFieldOS               = "operatingSystem"
	FilterFieldMarketOption     = "marketoption"
	FilterFieldTenancy          = "tenancy"
	FilterFieldOperation        = "operation"
	FilterFieldVolumeType       = "volumeType"
	FilterFieldVolumeAPIName    = "volumeApiName"
	FilterFieldFromRegionCode   = "fromRegionCode"
	FilterFieldFromLocationType = "fromLocationType"
	FilterFieldToLocationType   = "toLocationType"
	FilterFieldTransferType     = "transferType"
	FilterFieldToRegionCode     = "toRegionCode"
	FilterFieldEndpointType     = "endpointType"

	FilterValueAWSRegion      = "AWS Region"
	FilterValueLinux          = "Linux"
	FilterValueOnDemand       = "OnDemand"
	FilterValueShared         = "Shared"
	FilterValueRunInstances   = "RunInstances"
	FilterValueStandard       = "Standard"
	FilterValueInterRegionOut = "InterRegion Outbound"
	FilterValuePrivateLink    = "PrivateLink"
	FilterValueNatGateway     = "NatGateway"
)

var (
	serviceCodeMap = map[string]string{
		"EC2_Compute":                     ServiceCodeEC2,
		"EC2_Storage_Size":                ServiceCodeEC2,
		"EC2_Storage_IOPS":                ServiceCodeEC2,
		"EC2_Storage_Throughput":          ServiceCodeEC2,
		"S3":                              ServiceCodeS3,
		"S3_RegionOut_MAX":                ServiceDataTransfer,
		"Data_Transfer":                   ServiceCodeEC2,
		"PrivateLink":                     ServiceCodeVPC,
		"Data_Transfer_Within_SameRegion": ServiceDataTransfer,
		"NatGateway":                      ServiceCodeEC2,
		"Data_Transfer_RegionOut_MAX":     ServiceDataTransfer,
	}

	usageTypeMap = map[string]string{
		"EC2_Compute":                     UsageTypeBoxUsage,
		"EC2_Storage_Size":                UsageTypeEBSVolume,
		"EC2_Storage_IOPS":                UsageTypeEBSIOPS,
		"EC2_Storage_Throughput":          UsageTypeEBSThroughput,
		"S3":                              UsageTypeS3Storage,
		"PrivateLink":                     UsageTypePrivateLink,
		"Data_Transfer_Within_SameRegion": UsageTypeRegionalTransfer,
		"NatGateway":                      UsageTypeNatGateway,
		"Data_Transfer_RegionOut_MAX":     UsageTypeInterRegionOut,
		"S3_RegionOut_MAX":                UsageTypeInterRegionOut,
	}

	unitDivMap = map[string]int64{
		"PrivateLink":                     1024 * 1024 * 1024,
		"Data_Transfer_Within_SameRegion": 1024 * 1024 * 1024,
		"NatGateway":                      60,
		"Data_Transfer_RegionOut_MAX":     1024 * 1024 * 1024,
	}
)

func getAWSServiceCode(param string) string {
	if serviceCode, ok := serviceCodeMap[param]; ok {
		return serviceCode
	}
	return param
}

func (aws *AwsPriceHandlerImpl) GetPrice(params map[string]string) (res *forms.ProviderPriceRes, err error) {
	c, err := aws.GetAwsPricingClient()
	if err != nil {
		log.Error("cloudproviderhandler Failed to get AWS Pricing Client", zap.Error(err))
		return nil, err
	}
	inputCode := params["service_code"]
	serviceCode := getAWSServiceCode(inputCode)
	filters, usageType := getFilters(params, usageTypeMap[inputCode])
	if filters == nil {
		log.Error("cloudproviderhandler param is err", zap.Any("params", params))
		return nil, err
	}
	resFromAws, err := c.GetProducts(&pricing.GetProductsInput{
		ServiceCode: &serviceCode,
		Filters:     filters,
	})

	if err != nil {
		log.Error("cloudproviderhandler Error fetching products", zap.Error(err))
		return nil, err
	}
	if resFromAws == nil || len(resFromAws.PriceList) == 0 {
		log.Error("cloudproviderhandler fetching products res is nil ", zap.Any("filters", filters))

		return nil, errors.New("result is nil")

	}
	log.Info("cloudproviderhandler GetPriceFromAWSRes ", zap.Any("res.PriceList", resFromAws.PriceList))
	return aws.GetPriceFromAWSRes(resFromAws.PriceList, usageType, inputCode)

}

func getUnitDiv(unit string, inputCode string) int64 {
	if div, ok := unitDivMap[inputCode]; ok {
		return div
	}
	if unit == "Hrs" {
		return 60
	} else if unit == "GiBps-mo" || unit == "GiBps-month" {
		return 730 * 60 * 1024

	} else if strings.HasSuffix(strings.ToLower(unit), "mo") || strings.HasSuffix(strings.ToLower(unit), "month") {
		return 730 * 60
	} else {
		return 1
	}
}
func (aws *AwsPriceHandlerImpl) GetPriceFromAWSRes(jsonValueList []aws.JSONValue, usageSub string, inputCode string) (*forms.ProviderPriceRes, error) {
	var providerPriceResList []*forms.ProviderPriceRes
	for _, jsonValue := range jsonValueList {
		jsonBytes, err := json.Marshal(jsonValue)
		if err != nil {
			continue
			//return nil, fmt.Errorf("failed to marshal JSONValue: %v", err)
		}
		var priceFromProduct PriceFromProducts
		err = json.Unmarshal(jsonBytes, &priceFromProduct)
		usage := priceFromProduct.Product.Attributes["usagetype"]
		if !strings.HasSuffix(usage, usageSub) {
			log.Info("usage", zap.String("usage", usage), zap.Any("usageSub", usageSub))

			continue
		}
		if err != nil {
			return nil, fmt.Errorf("failed to unmarshal into PriceFromProducts: %v", err)
		}
		sku := priceFromProduct.Product.Sku
		priceTerms := priceFromProduct.Terms["OnDemand"]
		for key, priceTerm := range priceTerms {
			log.Info("price", zap.String("sku", sku), zap.String("key", key), zap.Any("priceTerm", priceTerm))
			//pp.Print(priceTerm)
			var price, unit string
			for _sku, p := range priceTerm.PriceDimensions {
				if p.BeginRange != "0" {
					continue
				}
				price = p.PricePerUnit["USD"]
				unit = p.Unit
				priceC, err := decimal.NewFromString(price)
				if err != nil {
					log.Error("cloudproviderhandler GetPriceFromAWSRes ", zap.Error(err))
					//return nil, err
				}

				unitDiv := getUnitDiv(unit, inputCode)
				if unitDiv == 0 {
					log.Error("cloudproviderhandler GetPriceFromAWSRes  unit error", zap.String("unit", unit))
					//	return nil, errors.New("unit is error")
				}
				priceF := priceC.Div(decimal.NewFromInt(unitDiv))

				log.Info("price", zap.String("USD", price), zap.String("unit", unit))
				providerPriceResList = append(providerPriceResList, &forms.ProviderPriceRes{
					SKU:     _sku,
					Version: priceTerm.EffectiveDate,
					Price:   &priceF,
				})
			}
		}
	}
	log.Info("cloudproviderhandler GetPriceFromAWSRes Result ", zap.Any("providerPriceResList", providerPriceResList))

	if len(providerPriceResList) == 0 {
		log.Error("cloudproviderhandler GetPriceFromAWSRes Result ", zap.Int("providerPriceResList", len(providerPriceResList)))
		return nil, errors.New("result is nil")
	}
	if len(providerPriceResList) > 1 {
		if strings.HasSuffix(inputCode, "_MAX") {
			var maxPriceRes *forms.ProviderPriceRes
			for _, providerPrice := range providerPriceResList {
				if maxPriceRes == nil || providerPrice.Price.Cmp(*maxPriceRes.Price) > 0 {
					maxPriceRes = providerPrice
				}
			}
			log.Info("cloudproviderhandler GetPriceFromAWSRes Result Max", zap.Any("maxPriceRes", maxPriceRes))
			return maxPriceRes, nil
		}
		log.Error("cloudproviderhandler GetPriceFromAWSRes Result ", zap.Int("providerPriceResList", len(providerPriceResList)))
	}
	return providerPriceResList[0], nil
}

type Sku struct {
	Sku           string            `json:"sku"`
	ProductFamily string            `json:"productFamily"`
	Attributes    map[string]string `json:"attributes"`
}
type RateCode struct {
	RateCode     string            `json:"rateCode"`
	Description  string            `json:"description"`
	Unit         string            `json:"unit"`
	BeginRange   string            `json:"beginRange"`
	EndRange     string            `json:"endRange"`
	PricePerUnit map[string]string `json:"pricePerUnit"`
	Version      string            `json:"version"`
}
type TermsSku struct {
	OfferTermCode      string              `json:"offerTermCode"`
	Sku                string              `json:"sku"`
	EffectiveDate      string              `json:"effectiveDate"`
	TermAttributesType string              `json:"termAttributesType"`
	TermAttributes     map[string]string   `json:"termAttributes"`
	PriceDimensions    map[string]RateCode `json:"priceDimensions"`
}
type AwsPriceJson struct {
	FormatVersion   string                                    `json:"formatVersion"`
	Disclaimer      string                                    `json:"disclaimer"`
	OfferCode       string                                    `json:"offerCode"`
	Version         string                                    `json:"version"`
	PublicationDate string                                    `json:"publicationDate"`
	Products        map[string]Sku                            `json:"products"`
	Terms           map[string]map[string]map[string]TermsSku `json:"terms"`
}

type PriceFromProducts struct {
	Product Sku                            `json:"product"`
	Terms   map[string]map[string]TermsSku `json:"terms"`
}

func getAwsDefaultSession() (*session.Session, error) {
	region := config.Get().GetString(config.AWSPriceRegion)
	sess, err := session.NewSession()
	if err != nil {
		return nil, err
	}
	creds := stscreds.NewCredentials(sess, config.Get().GetString(config.AWSRole))
	sess = sess.Copy(&aws.Config{Credentials: creds, Region: &region})
	//region := "us-east-1"
	//sess, err := session.NewSessionWithOptions(session.Options{
	//	SharedConfigState: session.SharedConfigEnable,
	//	Profile:           "sso",
	//})
	//if err != nil {
	//	return nil, err
	//}
	//
	//sess = sess.Copy(&aws.Config{
	//	Region: aws.String(region),
	//})
	return sess, nil
}

func (aws *AwsPriceHandlerImpl) GetAwsPricingClient() (*pricing.Pricing, error) {
	sess, err := getAwsDefaultSession()
	if err != nil {
		return nil, err
	}

	return pricing.New(sess), nil
}

func getFilters(params map[string]string, usageType string) ([]*pricing.Filter, string) {
	switch params["service_code"] {
	case "EC2_Compute":
		return getAWSComputeFilters(params), fmt.Sprintf("%s:%s", usageType, params["instanceType"])
	case "EC2_Storage_Size", "EC2_Storage_IOPS", "EC2_Storage_Throughput":
		return getAWSStorageFilters(params), fmt.Sprintf("%s.%s", usageType, params["volumeType"])
	case "S3":
		return getAWSS3Filters(params), usageType
	case "S3_RegionOut_MAX":
		return getAWSS3RegionOutMAXFilters(params), usageType
	case "Data_Transfer":
		return getAWSDataTransferFilters(params), fmt.Sprintf("%s.%s", usageType, params["service_code"])
	case "PrivateLink":
		return getAWSPrivateLinkFilters(params), usageType
	case "Data_Transfer_Within_SameRegion":
		return getAWSDataTransferWithinSameRegionFilters(params), usageType
	case "NatGateway":
		return getAWSNatGatewayFilters(params), usageType
	case "Data_Transfer_RegionOut_MAX":
		return getAWSDataTransferRegionOutFilters(params), usageType
	}

	return nil, ""

}
func newFilter(field, value string) *pricing.Filter {
	return &pricing.Filter{
		Type:  aws.String(FilterTypeTermMatch),
		Field: aws.String(field),
		Value: aws.String(value),
	}
}
func withRegionFilter(region string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldRegionCode, region),
	}
}
func withTransferTypeFilters(transferType string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldFromLocationType, FilterValueAWSRegion),
		newFilter(FilterFieldToLocationType, FilterValueAWSRegion),
		newFilter(FilterFieldTransferType, transferType),
	}
}
func getAWSComputeFilters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldRegionCode, params["region"]),
		newFilter(FilterFieldInstanceType, params["instanceType"]),
		newFilter(FilterFieldOS, FilterValueLinux),
		newFilter(FilterFieldMarketOption, FilterValueOnDemand),
		newFilter(FilterFieldTenancy, FilterValueShared),
		newFilter(FilterFieldOperation, FilterValueRunInstances),
	}
}

func getAWSStorageFilters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldRegionCode, params["region"]),
		newFilter(FilterFieldVolumeAPIName, params["volumeType"]),
	}
}

func getAWSS3Filters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldRegionCode, params["region"]),
		newFilter(FilterFieldVolumeType, FilterValueStandard),
	}
}

func getAWSS3RegionOutMAXFilters(params map[string]string) []*pricing.Filter {
	filters := []*pricing.Filter{
		newFilter(FilterFieldFromRegionCode, params["region"]),
	}
	filters = append(filters, withTransferTypeFilters(FilterValueInterRegionOut)...)
	return filters
}

func getAWSDataTransferFilters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldFromRegionCode, params["region"]),
		newFilter(FilterFieldFromLocationType, FilterValueAWSRegion),
	}
}

func getAWSPrivateLinkFilters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldRegionCode, params["region"]),
		newFilter(FilterFieldEndpointType, FilterValuePrivateLink),
	}
}

func getAWSDataTransferWithinSameRegionFilters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldFromRegionCode, params["region"]),
		newFilter(FilterFieldFromLocationType, FilterValueAWSRegion),
		newFilter(FilterFieldToRegionCode, params["region"]),
	}
}

func getAWSNatGatewayFilters(params map[string]string) []*pricing.Filter {
	return []*pricing.Filter{
		newFilter(FilterFieldRegionCode, params["region"]),
		newFilter(FilterFieldOperation, FilterValueNatGateway),
	}
}

func getAWSDataTransferRegionOutFilters(params map[string]string) []*pricing.Filter {
	filters := []*pricing.Filter{
		newFilter(FilterFieldFromRegionCode, params["region"]),
	}
	filters = append(filters, withTransferTypeFilters(FilterValueInterRegionOut)...)
	return filters
}
