package models

import (
	"encoding/json"
	"fmt"
	"strconv"

	log "github.com/sirupsen/logrus"
	"github.com/tidbcloud/metering/pkg/common/constants"
	"github.com/tidbcloud/metering/pkg/common/utils"
	dedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"gorm.io/gorm"
)

type ClusterMeta struct {
	gorm.Model
	TenantID     string           `json:"tenantID"`
	ProjectID    string           `json:"projectID"`
	ClusterID    string           `json:"clusterID" gorm:"uniqueIndex"`
	Cluster      string           `json:"cluster"`
	DevTier      bool             `json:"devTier"`
	Poc          bool             `json:"poc"`
	CrossAccount bool             `json:"crossAccount"`
	Provider     string           `json:"provider"`
	Region       string           `json:"region"`
	Components   []*ComponentMeta `json:"components" gorm:"foreignKey:meta_id"`
	IsScaling    bool             `json:"isScaling" gorm:"-"`
	Timestamp    int64            `json:"timestamp"`
	State        int32            `json:"state" `
}

type ComponentMeta struct {
	gorm.Model
	Component     string `json:"component"`
	ComponentType string `json:"componentType"`
	Count         uint32 `json:"count"`
	TiDBGroupName string `json:"tidbGroupName"`

	InstanceType string `json:"instanceType"`
	CpuNum       uint32 `json:"cpuNum"`
	MemoryGiB    uint32 `json:"memoryGiB"`

	StorageID       string `json:"storageID"`
	VolumeType      string `json:"volumeType"`
	StorageSizeGi   uint32 `json:"storageSizeGi"`
	Iops            uint32 `json:"iops"`
	ThroughputMibps uint32 `json:"throughputMibps"`

	State       int32  `json:"state"`
	MetaId      int    `json:"meta_id"`
	NodeSKU     string `json:"nodeSKU"`
	StoragesSKU string `json:"storagesSKU"`
}

type SKUJson struct {
	SKUName  string `json:"skuName"`
	SKUValue string `json:"skuValue"`
	Count    uint32 `json:"count"`
}

func SKUJsonToString(sku []*SKUJson) (string, error) {
	skuJson, err := json.Marshal(sku)
	if err != nil {
		log.Errorf("Marshal sku to json failed, %s", err.Error())
		return "", err
	}
	return string(skuJson), nil
}

func StringToSKUJson(s string) ([]*SKUJson, error) {
	sku := make([]*SKUJson, 0)
	if len(s) == 0 {
		return sku, nil
	}
	err := json.Unmarshal([]byte(s), &sku)
	if err != nil {
		log.Errorf("Unmarshal sku failed, %s", err.Error())
		return nil, err
	}
	return sku, nil
}

func (c ClusterMeta) GenerateBillingReport() ([]*BillingReport, error) {
	reports := make([]*BillingReport, 0)

	if !IfClusterStateNeedMetering(dedicatedv1.Cluster_State(c.State)) {
		log.Infof("skip GenerateBillingReport because of state, cluster id = %s, state = %d", c.ClusterID, c.State)
		return reports, nil
	}

	for _, component := range c.Components {
		labels := c.CommonLabels()
		labels["provider"] = c.Provider
		labels["cpuNum"] = strconv.FormatUint(uint64(component.CpuNum), 10)
		labels["memoryGiB"] = strconv.FormatUint(uint64(component.MemoryGiB), 10)
		labels["iops"] = strconv.FormatUint(uint64(component.Iops), 10)
		labels["throughput"] = strconv.FormatUint(uint64(component.ThroughputMibps), 10)
		labels["component"] = component.ComponentType

		labelsJson, err := json.Marshal(labels)
		if err != nil {
			log.Errorf("Marshal labels to json failed, %s", err.Error())
			return reports, err
		}

		if IfPauseOrResume(dedicatedv1.Cluster_State(c.State)) {
			log.Infof("skip node billing report because of state %d, cluster id = %s", c.State, c.ClusterID)
		} else {
			if component.Component == "tiproxy" {
				log.Debugf("build tiproxy billing report, component = %s", component.Component)
				tiproxyBillingReport := &BillingReport{
					CostID:        utils.GenerateID(),
					CostTimestamp: c.Timestamp,
					TenantID:      ParseUint64(c.TenantID),
					Cost:          int64(component.Count),
					Labels:        string(labelsJson),
					Unit:          fmt.Sprintf("tiproxy:%s:%s:DEPLOY_TYPE_DEDICATED:%s", c.Provider, c.Region, component.InstanceType),
				}
				reports = append(reports, tiproxyBillingReport)
			} else {
				nodeBillingReport := &BillingReport{
					CostID:        utils.GenerateID(),
					CostTimestamp: c.Timestamp,
					TenantID:      ParseUint64(c.TenantID),
					Cost:          int64(component.Count),
					Labels:        string(labelsJson),
					Unit:          fmt.Sprintf("node:%s:%s:DEPLOY_TYPE_DEDICATED:%s", c.Provider, c.Region, component.InstanceType),
				}
				reports = append(reports, nodeBillingReport)
			}

		}

		if len(component.VolumeType) > 0 {
			var storageUnit string
			var iopsUnit string
			var throughputUnit string
			if c.Provider == constants.VendorAzure {
				storageUnit = fmt.Sprintf("disk-storage:%s:%s:%s:%s", c.Provider, c.Region, mapVolumeType(component.VolumeType), component.Component)
				iopsUnit = fmt.Sprintf("disk-iops:%s:%s:%s:%s", c.Provider, c.Region, mapVolumeType(component.VolumeType), component.Component)
				throughputUnit = fmt.Sprintf("disk-throughput:%s:%s:%s:%s", c.Provider, c.Region, mapVolumeType(component.VolumeType), component.Component)
			} else {
				storageUnit = fmt.Sprintf("disk-storage:%s:%s:%s", c.Provider, c.Region, mapVolumeType(component.VolumeType))
				iopsUnit = fmt.Sprintf("disk-iops:%s:%s:%s", c.Provider, c.Region, mapVolumeType(component.VolumeType))
				throughputUnit = fmt.Sprintf("disk-throughput:%s:%s:%s", c.Provider, c.Region, mapVolumeType(component.VolumeType))
			}
			reports = append(reports, &BillingReport{
				CostID:        utils.GenerateID(),
				CostTimestamp: c.Timestamp,
				TenantID:      ParseUint64(c.TenantID),
				Cost:          int64(component.StorageSizeGi * component.Count),
				Labels:        string(labelsJson),
				Unit:          storageUnit,
			})

			reports = append(reports, &BillingReport{
				CostID:        utils.GenerateID(),
				CostTimestamp: c.Timestamp,
				TenantID:      ParseUint64(c.TenantID),
				Cost:          int64(component.Count),
				Labels:        string(labelsJson),
				Unit:          iopsUnit,
			})

			reports = append(reports, &BillingReport{
				CostID:        utils.GenerateID(),
				CostTimestamp: c.Timestamp,
				TenantID:      ParseUint64(c.TenantID),
				Cost:          int64(component.Count),
				Labels:        string(labelsJson),
				Unit:          throughputUnit,
			})
		}
	}

	return reports, nil
}

func (c ClusterMeta) GenerateBillingReportV2() ([]*BillingReportV2, error) {
	reports := make([]*BillingReportV2, 0)

	if !IfClusterStateNeedMetering(dedicatedv1.Cluster_State(c.State)) {
		log.Infof("skip GenerateBillingReport because of state, cluster id = %s, state = %d", c.ClusterID, c.State)
		return reports, nil
	}
	for _, component := range c.Components {
		labels := c.CommonLabelsV2()
		labels["provider"] = c.Provider
		labels["component"] = component.ComponentType

		labelsJson, err := json.Marshal(labels)
		if err != nil {
			log.Errorf("Marshal labels to json failed, %s", err.Error())
			return reports, err
		}

		if IfPauseOrResume(dedicatedv1.Cluster_State(c.State)) {
			log.Infof("skip node billing report because of state %d, cluster id = %s", c.State, c.ClusterID)
		} else {
			nodeskus, err := StringToSKUJson(component.NodeSKU)
			if err != nil {
				log.Errorf("StringToSKUJson failed in GenerateBillingReportV2, %s", err.Error())
				return reports, err
			}
			for _, nodeSKU := range nodeskus {
				nodeBillingReport := &BillingReportV2{
					SchemeVersion: constants.RepoterSchemeVersion,
					CostID:        utils.GenerateID(),
					CostTimestamp: c.Timestamp,
					TenantID:      ParseUint64(c.TenantID),
					Cost:          int64(nodeSKU.Count),
					UnitValue:     nodeSKU.SKUValue,
					Labels:        string(labelsJson),
					Unit:          nodeSKU.SKUName,
				}
				reports = append(reports, nodeBillingReport)
			}
		}
		storageskus, err := StringToSKUJson(component.StoragesSKU)
		if err != nil {
			log.Errorf("StringToSKUJson failed in GenerateBillingReportV2, %s", err.Error())
			return reports, err
		}
		if len(storageskus) > 0 {
			for _, storagesku := range storageskus {
				reports = append(reports, &BillingReportV2{
					SchemeVersion: constants.RepoterSchemeVersion,
					CostID:        utils.GenerateID(),
					CostTimestamp: c.Timestamp,
					TenantID:      ParseUint64(c.TenantID),
					Cost:          int64(storagesku.Count),
					UnitValue:     storagesku.SKUValue,
					Labels:        string(labelsJson),
					Unit:          storagesku.SKUName,
				})
			}
		}
	}

	return reports, nil
}

func (c ClusterMeta) CommonLabels() map[string]string {
	labels := map[string]string{}
	labels["poc"] = strconv.FormatBool(c.Poc)
	labels["devTier"] = strconv.FormatBool(c.DevTier)
	labels["crossAccount"] = strconv.FormatBool(c.CrossAccount)
	labels["cluster"] = c.Cluster
	labels["clusterID"] = c.ClusterID
	labels["projectID"] = c.ProjectID

	return labels
}

func (c ClusterMeta) CommonLabelsV2() map[string]string {
	labels := map[string]string{}
	labels["clusterID"] = c.ClusterID
	labels["projectID"] = c.ProjectID

	return labels
}

func ParseUint64(s string) uint64 {
	v, err := strconv.ParseUint(s, 10, 64)
	if err != nil {
		panic(err)
	}
	return v
}
