// copied from github.com/tidbcloud/infra-provider/pkg/gitops-sdk

package gitopsmigrate

import (
	"context"
	"fmt"
	"os"
	"sort"
	"strings"

	"github.com/pingcap/errors"
	v1 "github.com/tidbcloud/infra-api/api/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"
	clog "sigs.k8s.io/controller-runtime/pkg/log"
)

type Gitops struct {
	Cli client.Client
}

// not directly copy, modified from DescribeClusterConfiguration
func (g *Gitops) DescribeClusterTenantOverlay(cluster *v1.Cluster) (*v1.ClusterConfig, error) {
	ls := cluster.GetLabels()
	ccl, err := g.allClusterConfig()
	if err != nil {
		return nil, err
	}

	tenantAndClusterLayer, err := g.matchClusterConfig(ccl, labels.Set(ls))
	if err != nil {
		return nil, err
	}

	return mergeClusterConfig(tenantAndClusterLayer)
}

func (g *Gitops) allClusterConfig() (*v1.ClusterConfigList, error) {
	ccs := new(v1.ClusterConfigList)
	err := g.Cli.List(context.TODO(), ccs)
	if err != nil {
		return nil, err
	}
	return ccs, nil
}

func (g *Gitops) matchClusterConfig(ccl *v1.ClusterConfigList, labels labels.Labels) ([]v1.ClusterConfig, error) {
	var res []v1.ClusterConfig
	for _, cc := range ccl.Items {
		selector, err := metav1.LabelSelectorAsSelector(cc.Spec.Selector)
		if err != nil {
			return nil, err
		}

		if selector.Matches(labels) {
			res = append(res, cc)
		}
	}

	return res, nil
}

func mergeClusterConfig(configs []v1.ClusterConfig) (*v1.ClusterConfig, error) {
	if len(configs) == 0 {
		return nil, nil
	}
	sort.Slice(configs, func(i, j int) bool {
		return configs[i].Spec.Priority <= configs[j].Spec.Priority
	})
	if !isMetaConsistent(configs) {
		return nil, fmt.Errorf("ClusterConfigList is not fully updated")
	}

	base := new(v1.ClusterConfig)
	for _, overlay := range configs {
		if overlay.Spec.Version != "" {
			base.Spec.Version = overlay.Spec.Version
		}
		if overlay.Spec.UpdatePolicy != "" {
			base.Spec.UpdatePolicy = overlay.Spec.UpdatePolicy
		}

		base.Spec.ConfigMeta = mergeConfigMeta(base.Spec.ConfigMeta, overlay.Spec.ConfigMeta)
		base.Spec.TiDB = mergeConfiguration(base.Spec.TiDB, overlay.Spec.TiDB)
		base.Spec.TiKV = mergeConfiguration(base.Spec.TiKV, overlay.Spec.TiKV)
		base.Spec.PD = mergeConfiguration(base.Spec.PD, overlay.Spec.PD)
		base.Spec.TiFlash = mergeConfiguration(base.Spec.TiFlash, overlay.Spec.TiFlash)
		base.Spec.TiProxy = mergeConfiguration(base.Spec.TiProxy, overlay.Spec.TiProxy)
		base.Spec.TiFlashLearner = mergeConfiguration(base.Spec.TiFlashLearner, overlay.Spec.TiFlashLearner)
		base.Spec.Restore = mergeConfiguration(base.Spec.Restore, overlay.Spec.Restore)
		base.Spec.Backup = mergeConfiguration(base.Spec.Backup, overlay.Spec.Backup)
		base.Spec.Lightning = mergeConfiguration(base.Spec.Lightning, overlay.Spec.Lightning)
		base.Spec.TiCDC = mergeConfiguration(base.Spec.TiCDC, overlay.Spec.TiCDC)
		base.Spec.Priority = overlay.Spec.Priority
	}
	return base, nil
}

func mergeConfigMeta(c1, c2 *v1.ConfigMeta) *v1.ConfigMeta {
	if c2 == nil {
		return c1
	}
	if c1 == nil {
		c1 = new(v1.ConfigMeta)
	}

	if c2.GitCommitID != "" {
		c1.GitCommitID = c2.GitCommitID
	}
	c1.ConfigVersion = c2.ConfigVersion

	return c1
}

func mergeConfiguration(c1, c2 *v1.Configuration) *v1.Configuration {
	if c2 == nil {
		return c1
	}
	if c1 == nil {
		c1 = new(v1.Configuration)
	}

	if c2.BaseImage != nil {
		c1.BaseImage = c2.BaseImage
	}
	if c2.Tag != nil {
		c1.Tag = c2.Tag
	}
	if c1.Config == nil {
		c1.Config = v1.NewTomlConfig(map[string]interface{}{})
	}
	if c2.Config != nil {
		err := Merge(c1.Config.MP, c2.Config.MP)
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed merge config: %v", err)
		}
	}

	return c1
}

func isMetaConsistent(configs []v1.ClusterConfig) bool {
	list := make([]v1.ClusterConfig, 0, len(configs))
	for _, each := range configs {
		// Since BR may create some ClusterConfigs with the name prefix "restore--", and these ClusterConfigs
		// don't have the ConfigMeta, we need to filter them out
		if !strings.HasPrefix(each.Name, "base--") {
			continue
		}

		// The test-infra will create some ClusterConfigs having GitCommitID 'hacked-config-by-test-infra'
		if each.Spec.ConfigMeta != nil && each.Spec.ConfigMeta.GitCommitID == "hacked-config-by-test-infra" {
			continue
		}

		list = append(list, each)
	}
	if len(list) == 0 {
		return true
	}

	base := list[0]
	for _, each := range list {
		if each.Spec.ConfigMeta.GitCommitID != base.Spec.ConfigMeta.GitCommitID {
			clog.Log.Info("config meta is inconsistent",
				"baseName", base.Name, "baseCommitID", base.Spec.ConfigMeta.GitCommitID,
				"name", each.Name, "commitID", each.Spec.ConfigMeta.GitCommitID,
			)
			return false
		}
		if each.Spec.ConfigMeta.ConfigVersion != base.Spec.ConfigMeta.ConfigVersion {
			clog.Log.Info("config meta is inconsistent",
				"baseName", base.Name, "baseConfigVersion", base.Spec.ConfigMeta.ConfigVersion,
				"name", each.Name, "configVersion", each.Spec.ConfigMeta.ConfigVersion,
			)
			return false
		}
	}

	return true
}

// Merge apply the overlay struct to the base struct and override the same key.
// Merge will deep merge two maps, overlay will override values in base.
func Merge(base map[string]interface{}, overlay map[string]interface{}, keyPath ...string) error {
	child, err := GetChild(base, keyPath...)
	if err != nil {
		return errors.Wrap(err, "get child")
	}
	for k, v := range overlay {
		switch v := v.(type) {
		case map[string]interface{}:
			if _, ok := child[k]; !ok {
				child[k] = map[string]interface{}{}
			}
			if err := Merge(child, v, k); err != nil {
				return errors.Wrap(err, fmt.Sprintf("error overlay processing key %s", k))
			}
		default:
			child[k] = v
		}
	}
	return nil
}

func GetChild(base map[string]interface{}, keyPath ...string) (map[string]interface{}, error) {
	child := base
	for _, key := range keyPath {
		next, ok := child[key]
		if !ok {
			return nil, errors.New(fmt.Sprintf("key %s not found", strings.Join(keyPath, ".")))
		}
		child, ok = next.(map[string]interface{})
		if !ok {
			return nil, errors.New(fmt.Sprintf("key %s is not an Object", strings.Join(keyPath, ".")))
		}
	}
	return child, nil
}
