package gitopsv2

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

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/controller-runtime/pkg/client"

	v1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/gitops-v2/configeval"
	"github.com/tidbcloud/infra-api/pkg/pingcap/util/config"
	generic_utils "github.com/tidbcloud/infra-api/pkg/util/generic"
)

type Interface interface {
	DescribeClusterConfiguration(cluster *v1.Cluster) (*v1.ClusterConfig, error)
}

type DescribeClusterConfigurationFunc func(cluster *v1.Cluster) (*v1.ClusterConfig, error)

func (f DescribeClusterConfigurationFunc) DescribeClusterConfiguration(cluster *v1.Cluster) (*v1.ClusterConfig, error) {
	return f(cluster)
}

type Gitops struct {
	Cli client.Client
}

func (g *Gitops) DescribeClusterConfiguration(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
	}

	clusterAndTenantOverlay := mergeClusterConfig(tenantAndClusterLayer)
	var version string
	labels := make(map[string]string)
	if clusterAndTenantOverlay != nil {
		for _, l := range tenantAndClusterLayer {
			labels = generic_utils.MergeMap(labels, l.Spec.Selector.MatchLabels)
		}
		version = clusterAndTenantOverlay.Spec.Version
	}

	baseObj, err := g.getBaseClusterConfig(ccl, version)
	if err != nil {
		return nil, err
	}
	if version == "" {
		version = baseObj.Spec.Version
	}
	labels[v1.VersionLabel] = version

	instanceTypeOverlay, err := g.InstanceTypeLevelOverlay(ccl, version, getAllInstanceType(cluster))
	if err != nil {
		return nil, err
	}

	var selectedClusterconfig []v1.ClusterConfig
	selectedClusterconfig = append(selectedClusterconfig, *baseObj)
	selectedClusterconfig = append(selectedClusterconfig, *g.ProviderLevelOverlay(cluster.GetProviderType()))
	selectedClusterconfig = append(selectedClusterconfig, *instanceTypeOverlay)
	if clusterAndTenantOverlay != nil {
		selectedClusterconfig = append(selectedClusterconfig, *clusterAndTenantOverlay)
	}

	cc := mergeClusterConfig(selectedClusterconfig)
	cc.ObjectMeta.Labels = labels
	err = configeval.EvalClusterConfig(g.Cli, cc, cluster)
	if err != nil {
		return nil, err
	}
	return cc, nil
}

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 getAllInstanceType(cluster *v1.Cluster) map[v1.ComponentType]string {
	providerType := cluster.Labels[v1.ProviderTypeLabel]

	if providerType == v1.ProviderTypeEKS {
		providerType = v1.ProviderTypeAWS
	}
	res := make(map[v1.ComponentType]string, 4)

	if cluster.Spec.Cluster.TiFlash.InstanceType != "" {
		res[v1.ComponentTiFlash] = fmt.Sprintf("%s.%s", providerType, cluster.Spec.Cluster.TiFlash.InstanceType)
	}
	if cluster.Spec.Cluster.TiDB.InstanceType != "" {
		res[v1.ComponentTiDB] = fmt.Sprintf("%s.%s", providerType, cluster.Spec.Cluster.TiDB.InstanceType)
	}
	if cluster.Spec.Cluster.TiKV.InstanceType != "" {
		res[v1.ComponentTiKV] = fmt.Sprintf("%s.%s", providerType, cluster.Spec.Cluster.TiKV.InstanceType)
	}
	if cluster.Spec.Cluster.PD.InstanceType != "" {
		res[v1.ComponentPD] = fmt.Sprintf("%s.%s", providerType, cluster.Spec.Cluster.PD.InstanceType)
	}
	return res
}

func (g *Gitops) getBaseClusterConfig(ccl *v1.ClusterConfigList, version string) (*v1.ClusterConfig, error) {
	// query label that contains the specified version
	sets := labels.Set{}
	sets[v1.VersionLabel] = version
	ccs, err := g.matchClusterConfig(ccl, sets)
	if err != nil {
		return nil, err
	}

	// if the specified version is not found, query label that contains the default version
	if len(ccs) == 0 {
		sets := labels.Set{}
		sets[v1.DefaultVersionLabel] = "true"
		ccs, err := g.matchClusterConfig(ccl, sets)
		if err != nil {
			return nil, err
		}
		if len(ccs) != 1 {
			return nil, fmt.Errorf("should be only one base clusterconfig, but now: %d", len(ccs))
		}
		return &ccs[0], nil
	}

	if len(ccs) != 1 {
		return nil, fmt.Errorf("should be only one base clusterconfig, but now: %d", len(ccs))
	}

	return &ccs[0], nil
}

func mergeClusterConfig(configs []v1.ClusterConfig) *v1.ClusterConfig {
	if len(configs) == 0 {
		return nil
	}
	sort.Slice(configs, func(i, j int) bool {
		return configs[i].Spec.Priority <= configs[j].Spec.Priority
	})

	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.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
}

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 c2.Config != nil {
		if c1.Config == nil {
			c1.Config = v1.NewTomlConfig(map[string]interface{}{})
		}
		err := Merge(c1.Config.MP, c2.Config.MP)
		if err != nil {
			fmt.Fprintf(os.Stderr, "failed merge config: %v", err)
		}
	}

	return c1
}

// ProviderLevelOverlay provider level configs
// todo: add another layer in tidb-configuration later? right now we only need to change config of import
// in free-tier
func (g *Gitops) ProviderLevelOverlay(providerType string) *v1.ClusterConfig {
	base := new(v1.ClusterConfig)
	base.Spec.Priority = v1.PriorityProvider
	if providerType == consts.ProviderTypeAWSFreeTier {
		lightningConfigTpl := `[lightning]
region-concurrency = 2
table-concurrency = 1
index-concurrency = 1
[tikv-importer]
range-concurrency = 1
backend = "tidb"
[tidb]
build-stats-concurrency = 1
distsql-scan-concurrency = 1
index-serial-scan-concurrency = 1
checksum-table-concurrency = 1
`
		cfg := config.New(map[string]interface{}{})
		_ = cfg.UnmarshalTOML([]byte(lightningConfigTpl))

		tikvConfigTpl := `[import]
num-threads = 1
`
		tikvCfg := config.New(map[string]interface{}{})
		_ = tikvCfg.UnmarshalTOML([]byte(tikvConfigTpl))

		base.Spec.Lightning = &v1.Configuration{
			Config: v1.NewTomlConfig(cfg.MP),
		}
		base.Spec.TiKV = &v1.Configuration{
			Config: v1.NewTomlConfig(tikvCfg.MP),
		}
	}
	return base
}

func (g *Gitops) InstanceTypeLevelOverlay(ccl *v1.ClusterConfigList, version string, instancesTypes map[v1.ComponentType]string) (*v1.ClusterConfig, error) {
	base := new(v1.ClusterConfig)
	base.Spec.Priority = v1.PriorityInstanceType
	for component, instancesType := range instancesTypes {
		sets := labels.Set{}
		sets[v1.VersionLabel] = version
		sets[v1.InstanceTypeLabel] = instancesType
		ccs, err := g.matchClusterConfig(ccl, sets)
		if err != nil {
			return nil, err
		}
		// only get the configuration of the specified component, since the ClusterConfig of instance types
		// may contain the configuration of multiple components.
		for _, c := range ccs {
			switch component {
			case v1.ComponentTiDB:
				base.Spec.TiDB = mergeConfiguration(base.Spec.TiDB, c.Spec.TiDB)
			case v1.ComponentTiKV:
				base.Spec.TiKV = mergeConfiguration(base.Spec.TiKV, c.Spec.TiKV)
			case v1.ComponentPD:
				base.Spec.PD = mergeConfiguration(base.Spec.PD, c.Spec.PD)
			case v1.ComponentTiFlash:
				base.Spec.TiFlash = mergeConfiguration(base.Spec.TiFlash, c.Spec.TiFlash)
			}
		}
	}

	return base, nil
}
