package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"path"
	"regexp"
	"sort"
	"strings"

	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
)

var (
	yamlDir            string
	tierDir            = "v2"
	nodeFileRe         = regexp.MustCompile(`^node(.*)\.yaml$`)
	tierFileRe         = regexp.MustCompile(`^tier(.*)\.yaml$`)
	mergedTierFileName = "merged-tier.yaml"
)

func newConvertCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use: "convert",
		Run: func(cmd *cobra.Command, args []string) {
			cobra.CheckErr(convert())
		},
	}
	return cmd
}

func newMergeCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use: "merge",
		Run: func(cmd *cobra.Command, args []string) {
			cobra.CheckErr(merge())
		},
	}
	return cmd
}

func newRootCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:               "migrator",
		CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
	}
	cmd.AddCommand(newConvertCmd(), newMergeCmd())
	cmd.PersistentFlags().StringVar(&yamlDir, "dir", ".", "directory of nodes")
	return cmd
}

func main() {

	flag.StringVar(&yamlDir, "dir", ".", "directory of nodes")
	flag.Parse()

	rootCmd := newRootCmd()
	if err := rootCmd.Execute(); err != nil {
		logrus.Fatal("serve return err", zap.Error(err))
	}
}

func convert() error {
	fs, err := ioutil.ReadDir(yamlDir)
	if err != nil {
		panic(err)
	}
	for _, f := range fs {
		matches := nodeFileRe.FindStringSubmatch(f.Name())
		if matches == nil {
			logrus.Infof("filename not match, name=%s \n", f.Name())
			continue
		}
		tierUnitFileName := fmt.Sprintf("tier_unit%s.yaml", matches[1])
		tierFileName := fmt.Sprintf("tier%s.yaml", matches[1])
		logrus.Infof("start convert src=%s,dst=%s|%s", f.Name(), tierUnitFileName, tierFileName)

		tierUnitFileName = path.Join(yamlDir, tierDir, tierUnitFileName)
		tierFileName = path.Join(yamlDir, tierDir, tierFileName)
		b, err := ioutil.ReadFile(path.Join(yamlDir, f.Name()))
		if err != nil {
			panic(err)
		}

		var nodes []*nodeYaml
		if err := yaml.Unmarshal(b, &nodes); err != nil {
			return fmt.Errorf("unmarshal %s: %w", f.Name(), err)
		}

		tierUnits, tiers, err := convertNode2TierAndTierUnit(nodes)
		if err != nil {
			return fmt.Errorf("convert %s: %w", f.Name(), err)
		}

		tierUnitBytes, err := yaml.Marshal(tierUnits)
		if err != nil {
			return fmt.Errorf("marshal tierUnits %s: %w", f.Name(), err)
		}
		if err := ioutil.WriteFile(tierUnitFileName, tierUnitBytes, 0666); err != nil {
			return fmt.Errorf("write tierUnits %s: %w", f.Name(), err)
		}
		tierBytes, err := yaml.Marshal(tiers)
		if err != nil {
			return fmt.Errorf("marshal tiers %s: %w", f.Name(), err)
		}
		if err := ioutil.WriteFile(tierFileName, tierBytes, 0666); err != nil {
			return fmt.Errorf("write tiers %s: %w", f.Name(), err)
		}
	}

	return nil
}

func merge() error {
	tierDirPath := path.Join(yamlDir, tierDir)
	fs, err := ioutil.ReadDir(tierDirPath)
	if err != nil {
		panic(err)
	}
	key2MergedTier := map[string]*tierYaml{}
	for _, f := range fs {
		matches := tierFileRe.FindStringSubmatch(f.Name())
		if matches == nil {
			logrus.Infof("filename not match, name=%s \n", f.Name())
			continue
		}
		if strings.HasPrefix(f.Name(), "tier_unit") {
			logrus.Infof("filename not match, name=%s \n", f.Name())
			continue
		}
		logrus.Infof("start merge src=%s,dst=%s", f.Name(), mergedTierFileName)

		b, err := ioutil.ReadFile(path.Join(tierDirPath, f.Name()))
		if err != nil {
			panic(err)
		}

		var tiers []*tierYaml
		if err := yaml.Unmarshal(b, &tiers); err != nil {
			return fmt.Errorf("unmarshal %s: %w", f.Name(), err)
		}

		// merge tiers into one by tierKey
		for _, tier := range tiers {
			tierKey := buildTierKey(tier.Name, tier.DeployType, tier.ComponentType)
			mergedTier, ok := key2MergedTier[tierKey]
			if !ok {
				key2MergedTier[tierKey] = tier
			} else {
				for providerName, mappings := range tier.TierUnitMap {
					mergedTier.TierUnitMap[providerName] = append(mergedTier.TierUnitMap[providerName], mappings...)

					sort.SliceStable(mergedTier.TierUnitMap[providerName], func(i, j int) bool {
						return sortTierUnitMap(mergedTier.TierUnitMap[providerName][i], mergedTier.TierUnitMap[providerName][j])
					})
				}
			}
		}
	}

	mergedTiers := make([]*tierYaml, 0, len(key2MergedTier))
	for _, each := range key2MergedTier {
		mergedTiers = append(mergedTiers, each)
	}
	// sort merged tiers
	sort.SliceStable(mergedTiers, func(i, j int) bool {
		left, right := mergedTiers[i], mergedTiers[j]
		return sortTier(left, right)
	})

	tierBytes, err := yaml.Marshal(mergedTiers)
	if err != nil {
		return fmt.Errorf("marshal merged-tiers: %w", err)
	}
	if err := ioutil.WriteFile(path.Join(yamlDir, tierDir, mergedTierFileName), tierBytes, 0666); err != nil {
		return fmt.Errorf("write merged-tiers: %w", err)
	}
	return nil
}

func sortTierUnitMap(left, right tierUnitMap) bool {
	// in DESC order, put `tenantIds: [0]` into the tail of mappings
	return left.TenantIds[0] > right.TenantIds[0]
}

func sortTierUnit(left, right *tierUnitYaml) bool {
	if left.CloudProvider != right.CloudProvider {
		return left.CloudProvider < right.CloudProvider
	}
	if left.DeployType != right.DeployType {
		return left.DeployType < right.DeployType
	}
	if left.ComponentType != right.ComponentType {
		return left.ComponentType < right.ComponentType
	}

	return left.Key < right.Key
}

func sortTier(left, right *tierYaml) bool {
	if left.DeployType != right.DeployType {
		return left.DeployType < right.DeployType
	}
	if left.Name != right.Name {
		return left.Name < right.Name
	}
	if left.ComponentType != right.ComponentType {
		return left.ComponentType < right.ComponentType
	}
	return left.CpuNum < right.CpuNum
}

func buildTierKey(name, deployType, componentType string) string {
	return fmt.Sprintf("%s:%s:%s", name, deployType, componentType)
}

func buildTierUnitKey(providerName string, componentType string, computeType, storageType string, storageIops, storageThroughput int64) string {
	return fmt.Sprintf("%s:%s:%s:%s:%d:%d", providerName, componentType, computeType, storageType, storageIops, storageThroughput)
}

func buildPdNodeKey(providerName, deployType, nodeName string) string {
	return fmt.Sprintf("%s:%s:%s", providerName, deployType, nodeName)
}

func convertNode2TierAndTierUnit(nodes []*nodeYaml) ([]*tierUnitYaml, []*tierYaml, error) {
	pdNodeKey2PdNode := map[string]*nodeYaml{}
	for _, each := range nodes {
		if each.ComponentType == "pd" {
			pdNodeKey := buildPdNodeKey(each.CloudProvider, each.Node.DeployType, each.Node.Name)

			if _, ok := pdNodeKey2PdNode[pdNodeKey]; ok {
				return nil, nil, fmt.Errorf("duplicate pdNodeKey, key=%s", pdNodeKey)
			}
			pdNodeKey2PdNode[pdNodeKey] = each
		}
	}

	tierUnits := make([]*tierUnitYaml, 0, len(nodes))
	tiers := make([]*tierYaml, 0, len(nodes))
	for _, node := range nodes {
		tierUnitKey := buildTierUnitKey(node.CloudProvider, node.ComponentType, node.Node.InstanceType, node.Node.Disk.InstanceType, node.Node.Disk.Iops, node.Node.Disk.ThroughputMi)
		var nonStandardTenantIds []uint64
		for _, each := range node.NonStandardCustomers {
			nonStandardTenantIds = append(nonStandardTenantIds, each.TenantId)
		}
		tierUnit := &tierUnitYaml{
			CloudProvider:  node.CloudProvider,
			ComponentType:  node.ComponentType,
			DeployType:     node.Node.DeployType,
			Key:            tierUnitKey,
			Memo:           node.Annotation,
			DefaultNodeNum: node.Node.DefaultNodeNum,
			Regions:        node.Regions,
			Compute: tierCompute{
				InstanceType: node.Node.InstanceType,
			},
			Storage: &tierStorage{
				InstanceType:  node.Node.Disk.InstanceType,
				Iops:          int(node.Node.Disk.Iops),
				ThroughputMi:  int(node.Node.Disk.ThroughputMi),
				MinSizeGi:     int(node.Node.Disk.MinSizeGi),
				MaxSizeGi:     int(node.Node.Disk.MaxSizeGi),
				DefaultSizeGi: int(node.Node.Disk.DefaultSizeGi),
			},
			PD: nil, // fill later
			ForMigration: &forMigration{
				TierName:             node.Node.Name,
				TierDisplayName:      node.Node.DisplayName,
				NonStandardTenantIds: nonStandardTenantIds,
			},
		}
		if node.Node.PD != nil {
			pdNodeKey := buildPdNodeKey(node.CloudProvider, node.Node.DeployType, node.Node.PD.NodeName)
			pdNode, ok := pdNodeKey2PdNode[pdNodeKey]
			if !ok {
				return nil, nil, fmt.Errorf("can't find pdNode, pdNodeKey=%s,node=%+v", pdNodeKey, node)
			}

			tierUnit.PD = &pdTier{
				TierName:    node.Node.PD.NodeName,
				TierUnitKey: buildTierUnitKey(pdNode.CloudProvider, pdNode.ComponentType, pdNode.Node.InstanceType, pdNode.Node.Disk.InstanceType, pdNode.Node.Disk.Iops, pdNode.Node.Disk.ThroughputMi),
				NodeNum:     node.Node.PD.NodeNum,
			}
		}
		tier := &tierYaml{
			ComponentType: node.ComponentType,
			DeployType:    node.Node.DeployType,
			Name:          node.Node.Name,
			DisplayName:   node.Node.DisplayName,
			Memo:          node.Annotation,
			Default:       false,
			CpuNum:        int(node.Node.CpuNum),
			MemoryGi:      int(node.Node.MemoryGi),
			TierUnitMap: map[string][]tierUnitMap{
				node.CloudProvider: {
					{
						Regions:     node.Regions,
						TenantIds:   []uint64{0},
						TierUnitKey: tierUnitKey,
					},
				},
			},
		}
		// non standard node
		if len(node.NonStandardCustomers) != 0 {
			tier.TierUnitMap[node.CloudProvider][0].TenantIds = nonStandardTenantIds
		}

		tierUnits = append(tierUnits, tierUnit)
		tiers = append(tiers, tier)
	}

	return tierUnits, tiers, nil
}
