package sdk

import (
	"fmt"
	"strings"

	"github.com/google/go-containerregistry/pkg/name"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/config-service/sdk/api"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/gitops-sdk/consts"
	"github.com/tidbcloud/infra-provider/pkg/gitops-sdk/revision"
)

var (
	ReCalculateConfigRevision func(infraCli client.Client, c *cloudv1.Cluster, instanceChanged []cloudv1.ComponentType) (*cloudv1.ConfigRevision, error)
)

func init() {
	ReCalculateConfigRevision = reCalculateConfigRevision
}

func GenerateConfigOutput(cluster *cloudv1.Cluster, conf *cloudv1.ClusterConfig) (*api.DescribeClusterConfigOutput, error) {
	components, err := generateComponentOutput(cluster, conf)
	if err != nil {
		return nil, err
	}
	return &api.DescribeClusterConfigOutput{
		Version: conf.Spec.Version,
		ConfigMeta: &api.ConfigMeta{
			ID:            conf.ObjectMeta.Name,
			GitCommitID:   conf.Spec.ConfigMeta.GitCommitID,
			ConfigVersion: conf.Spec.ConfigMeta.ConfigVersion,
		},
		Components: components,
	}, nil
}

func generateComponentOutput(cluster *cloudv1.Cluster, clusterConf *cloudv1.ClusterConfig) ([]api.ComponentOutput, error) {
	var componentOutputs []api.ComponentOutput
	for _, c := range cluster.GetComponents() {
		var compConf *cloudv1.Configuration
		switch c.GetComponentType() {
		case cloudv1.ComponentTiKV:
			compConf = clusterConf.Spec.TiKV
		case cloudv1.ComponentPD:
			compConf = clusterConf.Spec.PD
		case cloudv1.ComponentTiDB:
			compConf = clusterConf.Spec.TiDB
		case cloudv1.ComponentTiFlash:
			compConf = clusterConf.Spec.TiFlash
		case cloudv1.ComponentTiCDC:
			compConf = clusterConf.Spec.TiCDC
		case cloudv1.ComponentTiProxy:
			compConf = clusterConf.Spec.TiProxy
		default:
			continue
		}
		if compConf == nil || compConf.BaseImage == nil || compConf.Config == nil {
			// ignore missing tiproxy overlay config, it is optional
			if c.GetComponentType() == cloudv1.ComponentTiProxy {
				continue
			}
			return nil, fmt.Errorf("missing overlay config for %s", c.GetComponentType())
		}
		componentOutputs = append(componentOutputs, api.ComponentOutput{
			Image: api.Image{
				BaseImage: OverrideBaseImage(cluster, *compConf.BaseImage),
				Tag:       compConf.Tag,
			},
			Component: api.ComponentName(c.GetComponentType()),
			Config:    api.NewTomlConfig(compConf.Config.MP),
		})

		// also append tiflash leaner conf if the component is tiflash
		if c.GetComponentType() == cloudv1.ComponentTiFlash {
			componentType := cloudv1.ComponentTiFlashLearner
			compConf := clusterConf.Spec.TiFlashLearner
			if compConf == nil || compConf.BaseImage == nil || compConf.Config == nil {
				return nil, fmt.Errorf("missing overlay config for %s", componentType)
			}
			componentOutputs = append(componentOutputs, api.ComponentOutput{
				Image: api.Image{
					BaseImage: OverrideBaseImage(cluster, *compConf.BaseImage),
					Tag:       compConf.Tag,
				},
				Component: api.ComponentName(componentType),
				Config:    api.NewTomlConfig(compConf.Config.MP),
			})
		}
	}
	return componentOutputs, nil
}

func OverrideBaseImage(obj client.Object, imagename string) string {
	// Use the annotation but not the cluster spec for now to have more flexibility
	// so we don't need to change the API for the short-term solution.
	// The annotation can either be tagged by the controller or by central.
	overridedRegistry := GetAnnotation(obj, consts.OverrideRegistryKey)
	overridedRepoPrefix := GetAnnotation(obj, consts.OverrideRepoPrefixKey)
	return OverrideImageWithRepoPrefix(overridedRegistry, overridedRepoPrefix, imagename)
}

func OverrideImageWithRepoPrefix(overridedRegistry, overridedRepoPrefix, imagename string) string {
	ref, err := name.ParseReference(imagename, name.WithDefaultRegistry(overridedRegistry))
	if err != nil {
		// There's an underlying error in the cluster config itself. Pass the base image as-is
		// so the underlying error can be surfaced.
		return imagename
	}
	registry := ref.Context().RegistryStr()
	if overridedRegistry != "" {
		registry = overridedRegistry
	}
	repo := ref.Context().RepositoryStr()
	repoPaths, imagePaths := strings.Split(repo, "/"), strings.Split(imagename, "/")
	if len(repoPaths) == 0 || len(imagePaths) == 0 {
		return imagename
	}
	image := imagePaths[len(imagePaths)-1]
	repoPrefix := strings.Join(repoPaths[:len(repoPaths)-1], "/")
	if overridedRepoPrefix != "" {
		repoPrefix = overridedRepoPrefix
	}
	var result []string
	for _, s := range []string{registry, repoPrefix, image} {
		if s != "" {
			result = append(result, s)
		}
	}
	return strings.Join(result, "/")
}

func OverrideImageWithThirdPartyRepoPrefix(overridedRegistry, overridedRepoPrefix, imagename string) string {
	ref, err := name.ParseReference(imagename, name.WithDefaultRegistry(overridedRegistry))
	if err != nil {
		// There's an underlying error in the cluster config itself. Pass the base image as-is
		// so the underlying error can be surfaced.
		return imagename
	}

	registry := ref.Context().RegistryStr()
	if overridedRegistry != "" {
		registry = overridedRegistry
	}

	var result []string
	for _, s := range []string{registry, overridedRepoPrefix, ref.Context().RepositoryStr()} {
		if s != "" {
			result = append(result, s)
		}
	}
	return strings.Join(result, "/") + ":" + ref.Identifier()
}

func GetAnnotation(obj client.Object, key string) string {
	annotations := obj.GetAnnotations()
	if annotations == nil {
		return ""
	}
	return annotations[key]
}

func reCalculateConfigRevision(infraCli client.Client, c *cloudv1.Cluster, instanceChanged []cloudv1.ComponentType) (*cloudv1.ConfigRevision, error) {
	curRevRef := c.Spec.ConfigRevisionRef
	// Get current cluster config
	curConfig, err := GetCurrentClusterConfiguration(infraCli, curRevRef)
	if err != nil {
		return nil, err
	}
	g := &Gitops{Cli: infraCli}
	r := revision.NewRevisioner(infraCli)
	// Get the whole cluster config, include dryrun config
	cc, err := g.DescribeClusterConfiguration(c)
	if err != nil {
		return nil, err
	}
	// copy labels and configMeta from cc to curConfig, these info is IMPORTANT
	curConfig.Labels = cc.Labels
	curConfig.Spec.ConfigMeta = cc.Spec.ConfigMeta

	// Replace current clusterconfig component config with gitops new config
	// Note: the component spec baseImage and tag changes are ignored
	for _, cmp := range instanceChanged {
		switch cmp {
		case cloudv1.ComponentPD:
			curConfig.Spec.PD.Config = cc.Spec.PD.Config
		case cloudv1.ComponentTiKV:
			curConfig.Spec.TiKV.Config = cc.Spec.TiKV.Config
		case cloudv1.ComponentTiDB:
			curConfig.Spec.TiDB.Config = cc.Spec.TiDB.Config
		case cloudv1.ComponentTiFlash:
			curConfig.Spec.TiFlash.Config = cc.Spec.TiFlash.Config
			curConfig.Spec.TiFlashLearner.Config = cc.Spec.TiFlashLearner.Config
			// Shouldn't got this value. In here, we expect cluster component only, that's pd,tikv,tidb,tiflash, etc.
			// tiflashLearner is the type of component configuration rather than a type of component.
		case cloudv1.ComponentTiFlashLearner:
			curConfig.Spec.TiFlashLearner.Config = cc.Spec.TiFlashLearner.Config
		case cloudv1.ComponentTiProxy:
			curConfig.Spec.TiProxy.Config = cc.Spec.TiProxy.Config
		case cloudv1.ComponentTiCDC:
			curConfig.Spec.TiCDC.Config = cc.Spec.TiCDC.Config
		case cloudv1.ComponentBackup:
			curConfig.Spec.Backup.Config = cc.Spec.Backup.Config
		case cloudv1.ComponentRestore:
			curConfig.Spec.Restore.Config = cc.Spec.Restore.Config
		case cloudv1.ComponentLightning:
			curConfig.Spec.Lightning.Config = cc.Spec.Lightning.Config
		default:
			// Unsupported components
		}
	}
	rev, err := GetClusterRevision(r, curConfig, true)
	if err != nil {
		return nil, fmt.Errorf("create ClusterRevision: %w", err)
	}
	return rev, nil
}

// GetClusterRevision returns the desired ConfigRevision for the cluster.
// This method may create a new revision.
func GetClusterRevision(rv revision.Interface, cc *cloudv1.ClusterConfig, create bool) (*cloudv1.ConfigRevision, error) {
	cr := revision.NewConfigRevision(cc)
	// find if ConfigRevision exists for this ClusterConfig
	existCR, err := rv.GetConfigRevisions(client.ObjectKey{Name: cr.Name})
	if err != nil && !apierrors.IsNotFound(errors.Cause(err)) {
		return nil, err
	}
	if existCR != nil {
		if !revision.EqualRevision(cr, existCR) {
			return nil, fmt.Errorf("two configRevisions with the same name but have different conf, l=%s,r=%s", cr.Name, existCR.Name)
		} else {
			return existCR, nil
		}
	}

	if create {
		var err error
		cr, err = rv.CreateConfigRevision(cr)
		if err != nil {
			return nil, err
		}
	}
	return cr, nil
}
