package platformsetting

import (
	"bytes"
	"fmt"
	"github.com/go-openapi/swag"
	"github.com/pingcap/log"
	"github.com/pingcap/tep/db/entity"
	"github.com/pingcap/tep/pkg/apis/apiserver"
	store "github.com/pingcap/tep/pkg/registry/apiserver"
	"github.com/pingcap/tep/pkg/utils"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/validation"
	"regexp"
	"strconv"
	"strings"
)

const (
	PDResourcesConfigMapKey        = "pdResources"
	TiKVResourcesConfigMapKey      = "tikvResources"
	TiDBResourcesConfigMapKey      = "tidbResources"
	TiDBVersionsConfigMapKey       = "tidbVersions"
	ReplicationFactorsConfigMapKey = "replicationFactors"

	maxAZSize                   = 20
	maxPDResourceTemplateSize   = 20
	maxTiKVResourceTemplateSize = 20
	maxTiDBResourceTemplateSize = 20
)

var (
	resourceRegexp  = regexp.MustCompile(`^([1-9]+[0-9]*)C([1-9]+[0-9]*)GiB$`)
	resourcesRegexp = regexp.MustCompile(`^([1-9]+\d*C[1-9]+\d*GiB){1}(,\s*[1-9]+\d*C[1-9]+\d*GiB)*$`)
)

func (s *Storage) GetPlatform(name string) (*apiserver.PlatformSetting, error) {
	cm, err := s.store.Controller().GetConfigMap(s.store.Namespace, store.Path2ConfigMapName[name])
	if err != nil {
		return nil, err
	}
	configMapData := cm.Data

	pdResourceTemplates, err := getResourcesFromConfigMap(cm, PDResourcesConfigMapKey)
	if err != nil {
		return nil, err
	}
	tidbResourceTemplates, err := getResourcesFromConfigMap(cm, TiDBResourcesConfigMapKey)
	if err != nil {
		return nil, err
	}
	tikvResourceTemplates, err := getResourcesFromConfigMap(cm, TiKVResourcesConfigMapKey)
	if err != nil {
		return nil, err
	}
	replicas, err := getReplicasFromConfigMap(cm, ReplicationFactorsConfigMapKey)
	if err != nil {
		return nil, err
	}
	utcOffset, err := s.store.GetUTCOffsetFromConfigMap(cm, store.UTCOffsetConfigMapKey)
	if err != nil {
		return nil, err
	}

	platform := &apiserver.PlatformSetting{
		AZs:                   utils.MakeNilSlice2EmptySlice(s.getAllAzName()).([]string),
		TiDBVersions:          utils.MakeNilSlice2EmptySlice(swag.SplitByFormat(configMapData[TiDBVersionsConfigMapKey], "csv")).([]string),
		ReplicationFactors:    utils.MakeNilSlice2EmptySlice(replicas).([]uint32),
		PDResourceTemplates:   utils.MakeNilSlice2EmptySlice(pdResourceTemplates).([]apiserver.ResourceTemplate),
		TiDBResourceTemplates: utils.MakeNilSlice2EmptySlice(tidbResourceTemplates).([]apiserver.ResourceTemplate),
		TiKVResourceTemplates: utils.MakeNilSlice2EmptySlice(tikvResourceTemplates).([]apiserver.ResourceTemplate),
		UTCOffset:             utcOffset,
	}
	platform.Name = name
	platform.UID = types.UID(name)
	return platform, nil
}

func (s *Storage) UpdatePlatform(platform *apiserver.PlatformSetting) (runtime.Object, bool, error) {
	err := validate(platform)
	if err != nil {
		return nil, false, err
	}
	log.Info("update platform request", zap.Any("payload", platform))
	cm, err := s.store.Controller().GetConfigMap(s.store.Namespace, store.Path2ConfigMapName[platform.Name])
	if err != nil {
		return nil, false, err
	}
	resourcesStr := strings.Join(resourceTemplateToSlice(platform.PDResourceTemplates), ",")
	if err = IsResourcesValid(resourcesStr); err != nil {
		return nil, false, err
	}
	cm.Data[PDResourcesConfigMapKey] = resourcesStr

	resourcesStr = strings.Join(resourceTemplateToSlice(platform.TiDBResourceTemplates), ",")
	if err = IsResourcesValid(resourcesStr); err != nil {
		return nil, false, err
	}
	cm.Data[TiDBResourcesConfigMapKey] = resourcesStr

	resourcesStr = strings.Join(resourceTemplateToSlice(platform.TiKVResourceTemplates), ",")
	if err = IsResourcesValid(resourcesStr); err != nil {
		return nil, false, err
	}
	cm.Data[TiKVResourcesConfigMapKey] = resourcesStr

	itemsStr := strings.Join(platform.TiDBVersions, ",")
	if hasDuplicateItem(itemsStr) {
		return nil, false, errors.NewBadRequest("duplicate tidb version is not allowed")
	}
	cm.Data[TiDBVersionsConfigMapKey] = itemsStr

	itemsStr = joinUInt32(platform.ReplicationFactors, ",")
	if hasDuplicateItem(itemsStr) {
		return nil, false, errors.NewBadRequest("duplicate replica factor is not allowed")
	}
	cm.Data[ReplicationFactorsConfigMapKey] = itemsStr

	valid, err := s.store.IsUTCOffsetValid(platform.UTCOffset)
	if !valid {
		return nil, false, errors.NewBadRequest(err.Error())
	}
	cm.Data[store.UTCOffsetConfigMapKey] = strconv.Itoa(platform.UTCOffset)

	_, err = s.store.Controller().UpdateConfigMap(s.store.Namespace, cm)
	if err != nil {
		return nil, false, err
	}
	return s.updateAzs(platform)
}

func (s *Storage) updateAzs(platform *apiserver.PlatformSetting) (runtime.Object, bool, error) {
	azMap := make(map[string]bool)
	for _, item := range platform.AZs {
		if err := validation.IsValidLabelValue(item); err != nil {
			return nil, false, errors.NewBadRequest(fmt.Sprintf("\"%s\" is not a valid az name, regex used for validation is `^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$`, and max length is 63", item))
		}
		azMap[item] = true
	}
	log.Info("az map", zap.Any("azMap", azMap))
	if len(azMap) != len(platform.AZs) {
		return nil, false, errors.NewBadRequest("duplicate az is not allowed")
	}
	var azs []entity.Az
	s.store.DB.Find(&azs)
	dbAzMap := make(map[string]entity.Az)
	for _, item := range azs {
		dbAzMap[item.Name] = item
	}
	log.Info("db az map", zap.Any("dbAzMap", dbAzMap))
	var deleteAzs []string
	var createAzs []string
	for _, azName := range platform.AZs {
		_, found := dbAzMap[azName]
		if !found { //new az
			createAzs = append(createAzs, azName)
		}
	}
	for azName, az := range dbAzMap {
		if !azMap[azName] { //deleted
			var machines []entity.Machine
			s.store.DB.Model(&az).Related(&machines)
			log.Info("az's machines", zap.Any("az", az), zap.Any("machine", machines))
			if len(machines) > 0 {
				return nil, false, errors.NewBadRequest(fmt.Sprintf("az[%s] is not empty", azName))
			}
			deleteAzs = append(deleteAzs, azName)
		}
	}
	log.Info("create Azs", zap.Any("createAzs", createAzs))
	log.Info("delete Azs", zap.Any("deleteAzs", deleteAzs))
	db := s.store.DB.Begin()
	for _, item := range createAzs {
		db = db.Create(&entity.Az{Name: item})
	}
	for _, item := range deleteAzs {
		db = db.Delete(&entity.Az{}, "name=?", item)
	}
	db = db.Commit()
	if db.Error != nil {
		return nil, false, db.Error
	}
	return platform, true, nil
}

func IsResourcesValid(s string) error {
	if !resourcesRegexp.MatchString(s) {
		return fmt.Errorf("%s should be `1C1GiB, 2C2GiB` format, separated by comma", s)
	}
	if hasDuplicateItem(s) {
		return fmt.Errorf("dulicate item is found in string[%s]", s)
	}
	return nil
}

func IsReplicasValid(s string) (bool, error) {
	if hasDuplicateItem(s) {
		return false, fmt.Errorf("dulicate item is found in string[%s]", s)
	}
	return true, nil
}

func getReplicasFromConfigMap(cm *corev1.ConfigMap, key string) ([]uint32, error) {
	replicasStr, exist := cm.Data[key]
	if !exist {
		return nil, fmt.Errorf("key[%s] not found in ConfigMap[%s]", key, cm.GetName())
	}

	valid, err := IsReplicasValid(replicasStr)
	if !valid {
		return nil, fmt.Errorf("%s %v", key, err)
	}

	replicas := swag.SplitByFormat(replicasStr, "csv")
	result := make([]uint32, 0)
	for _, s := range replicas {
		i, err := swag.ConvertUint32(s)
		if err != nil {
			return nil, err
		}
		result = append(result, i)
	}

	return result, nil
}

func resourceTemplateToSlice(templates []apiserver.ResourceTemplate) []string {
	var resources []string
	for _, item := range templates {
		resources = append(resources, fmt.Sprintf("%dC%dGiB", item.CPU, item.Memory))
	}
	return resources
}
func getResourcesFromConfigMap(cm *corev1.ConfigMap, key string) ([]apiserver.ResourceTemplate, error) {
	resourcesStr, exist := cm.Data[key]
	if !exist {
		return nil, fmt.Errorf("key[%s] not found in ConfigMap[%s]", key, cm.GetName())
	}

	err := IsResourcesValid(resourcesStr)
	if err != nil {
		return nil, fmt.Errorf("%s %v", key, err)
	}

	var resources []apiserver.ResourceTemplate
	for _, resource := range swag.SplitByFormat(resourcesStr, "csv") {
		matched := resourceRegexp.FindStringSubmatch(resource)
		if matched == nil {
			return nil, fmt.Errorf("unable to parse resource from string[%s]", resource)
		}
		if hasDuplicateItem(resource) {
			return nil, fmt.Errorf("dulicate item is found in string[%s]", resource)
		}

		var cpu, memory uint32
		var err error

		if cpu, err = swag.ConvertUint32(matched[1]); err != nil {
			return nil, err
		}
		if memory, err = swag.ConvertUint32(matched[2]); err != nil {
			return nil, err
		}
		resourceTemplate := apiserver.ResourceTemplate{
			CPU:    cpu,
			Memory: memory,
		}
		resources = append(resources, resourceTemplate)
	}
	return resources, nil
}

func hasDuplicateItem(itemStr string) bool {
	items := strings.Split(itemStr, ",")
	itemMap := make(map[string]interface{})
	for _, item := range items {
		item := strings.TrimSpace(item)
		if _, found := itemMap[item]; found {
			return true
		}
		itemMap[item] = struct{}{}
	}
	return false
}

func joinUInt32(items []uint32, sep string) string {
	switch len(items) {
	case 0:
		return ""
	case 1:
		return fmt.Sprintf("%d", items[0])
	}

	var b bytes.Buffer
	b.WriteString(fmt.Sprintf("%d", items[0]))
	for _, s := range items[1:] {
		b.WriteString(sep)
		b.WriteString(fmt.Sprintf("%d", s))
	}
	return b.String()
}

func (s *Storage) getAllAzName() []string {
	azNames := []string{}
	var azs []entity.Az
	s.store.DB.Find(&azs)
	for _, item := range azs {
		azNames = append(azNames, item.Name)
	}
	return azNames
}

func validate(platform *apiserver.PlatformSetting) error {
	if len(platform.AZs) == 0 {
		return errors.NewBadRequest("az is empty")
	}
	if len(platform.AZs) > maxAZSize {
		return errors.NewBadRequest(fmt.Sprintf("az size is greater than %d", maxAZSize))
	}
	if len(platform.PDResourceTemplates) > maxPDResourceTemplateSize {
		return errors.NewBadRequest(fmt.Sprintf("PDResourceTemplates size is greater than %d", maxPDResourceTemplateSize))
	}
	if len(platform.TiKVResourceTemplates) > maxTiKVResourceTemplateSize {
		return errors.NewBadRequest(fmt.Sprintf("PDResourceTemplates size is greater than %d", maxTiKVResourceTemplateSize))
	}
	if len(platform.TiDBResourceTemplates) > maxTiDBResourceTemplateSize {
		return errors.NewBadRequest(fmt.Sprintf("TiDBResourceTemplates size is greater than %d", maxTiDBResourceTemplateSize))
	}
	for _, item := range platform.AZs {
		if len(strings.TrimSpace(item)) == 0 {
			return errors.NewBadRequest("empty az is not allowed")
		}
	}
	return nil
}
