package server

import (
	"context"
	"fmt"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/cluster-controller/server/server/cvt"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	apicommon "github.com/tidbcloud/infra-provider/api/tidbcloud/common"
	"github.com/tidbcloud/infra-provider/pkg/gitops-sdk/revision"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

func (s *ClusterServer) SetVersionAndConfigurations(ctx context.Context, req *apiv1.SetVersionAndConfigurationsRequest) (*apiv1.SetVersionAndConfigurationsResponse, error) {
	cluster, err := s.clusterReader.Get(ctx, s.KubeClient, req.ClusterId)
	if err != nil {
		tilog.Error(ctx, "Failed to get cluster", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "cluster %s not found", req.ClusterId)
		}
		return nil, status.Errorf(codes.Internal, "failed to get cluster %s: %s", req.ClusterId, err)
	}
	cr, err := getConfigRevisionByCluster(ctx, s.KubeClient, cluster)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to get config revision %s: %s", cluster.Spec.ConfigRevisionRef.Name, err)
	}

	// generate and create new ConfigRevision
	newCR := cr.DeepCopy()
	if err := overlayConfigAndVersion(ctx, newCR, req.ComponentConfigurations, req.ClusterVersion); err != nil {
		return nil, status.Errorf(codes.Internal, "failed to overlay config and version: %s", err)
	}
	revision.RectifyConfigRevisionForCreate(cluster, newCR, req.IdempotencyKey)
	out := &apiv1.SetVersionAndConfigurationsResponse{
		ConfigChangeId: cloudv1.GenConfigChangeName(newCR.Name),
	}

	if equality.Semantic.DeepEqual(cr, newCR) {
		tilog.Info(ctx, "no config changes")
		return out, nil
	}
	if err := k8s.CreateIfNotExist(ctx, s.KubeClient, newCR); err != nil {
		return nil, fmt.Errorf("failed to create config revision: %s", err)
	}

	if err := s.applyCR2Cluster(ctx, cr, newCR, cluster, cvt.Strategy2CR(req.Strategy)); err != nil {
		return nil, err
	}

	return out, nil
}

func (s *ClusterServer) GetVersionAndConfigurations(ctx context.Context, req *apiv1.GetVersionAndConfigurationsRequest) (*apiv1.GetVersionAndConfigurationsResponse, error) {
	cluster, err := s.clusterReader.Get(ctx, s.KubeClient, req.ClusterId)
	if err != nil {
		tilog.Error(ctx, "Failed to get cluster", zap.Error(err))
		if apierrors.IsNotFound(err) {
			return nil, status.Errorf(codes.NotFound, "cluster %s not found", req.ClusterId)
		}
		return nil, status.Errorf(codes.Internal, "failed to get cluster %s: %s", req.ClusterId, err)
	}
	cr, err := getConfigRevisionByCluster(ctx, s.KubeClient, cluster)
	if err != nil {
		return nil, fmt.Errorf("failed to get config revision %s: %s", cluster.Spec.ConfigRevisionRef.Name, err)
	}
	version, confs, err := cvt.ConfigRevision2PB(cr)
	if err != nil {
		return nil, err
	}

	out := &apiv1.GetVersionAndConfigurationsResponse{
		ClusterVersion:          version,
		ComponentConfigurations: confs,
	}
	return out, nil
}

// todo(chenfei): this method is copied from config-manager/pkg/gitops-v2/api.go:applyCon2Cluster
func (s *ClusterServer) applyCR2Cluster(ctx context.Context, currRevision, nextRevision *cloudv1.ConfigRevision, cluster *cloudv1.Cluster, strategy cloudv1.ConfigChangeStrategy) error {
	logger := tilog.WithContextL(ctx).With(
		zap.String("cluster", cluster.Name),
		zap.String("nextRevision", nextRevision.Name),
		zap.String("currRevision", currRevision.Name))

	curCCName := cluster.CurrentConfigChangeName()
	nextCCName := cloudv1.GenConfigChangeName(nextRevision.Name)
	if curCCName == "" {
		// set ConfigChange annotation into Cluster to mark this cluster is under config changing.
		cluster.SetConfigChange(nextCCName)
		if err := s.KubeClient.Update(ctx, cluster); err != nil {
			logger.Error("Failed to pause the cluster", zap.Error(err))
			return fmt.Errorf("update cluster %s: %s", cluster.Name, err)
		}
	} else if curCCName != nextCCName {
		return fmt.Errorf("the cluster is already under config changing, ccName=%s", curCCName)
	}

	cc := &cloudv1.ConfigChange{
		ObjectMeta: metav1.ObjectMeta{
			Name:      cloudv1.GenConfigChangeName(nextRevision.Name),
			Namespace: cluster.Namespace,
		},
		Spec: cloudv1.ConfigChangeSpec{
			ClusterName:        cluster.Name,
			ConfigRevisionName: nextRevision.Name,
			ChangeStrategy:     strategy,
			Paused:             nil,
		},
	}
	if err := k8s.CreateIfNotExist(ctx, s.KubeClient, cc); err != nil {
		return fmt.Errorf("create config change failed: %w", err)
	}
	return nil
}

func getConfigRevisionByCluster(ctx context.Context, cli client.Client, cluster *cloudv1.Cluster) (*cloudv1.ConfigRevision, error) {
	if cluster.Spec.ConfigRevisionRef == nil || cluster.Spec.ConfigRevisionRef.Name == "" {
		return nil, status.Errorf(codes.Internal, "cluster %s has no config revision", cluster.Name)
	}

	cr := &cloudv1.ConfigRevision{}
	key := client.ObjectKey{Name: cluster.Spec.ConfigRevisionRef.Name}
	if err := cli.Get(ctx, key, cr); err != nil {
		return nil, fmt.Errorf("failed to get config revision %s: %s", key, err)
	}

	return cr, nil
}

func overlayConfigAndVersion(ctx context.Context, cr *cloudv1.ConfigRevision, configurations []*apiv1.ComponentConfiguration, version *apiv1.ClusterVersion) error {
	for _, each := range configurations {
		compType := cvt.ComponentType2CR(each.ComponentType)
		newTomlConf := cloudv1.NewTomlConfig(map[string]interface{}{})
		if err := newTomlConf.UnmarshalTOML([]byte(each.ConfigurationFileContents)); err != nil {
			return fmt.Errorf("unmarshal config: %w", err)
		}
		cr.GetOrCreateCompConf(compType).SetConfig(newTomlConf)
	}

	if version != nil {
		if version.ClusterVersion != "" {
			cr.Spec.Version = version.ClusterVersion
		}

		for _, each := range version.ComponentVersions {
			compType := cvt.ComponentType2CR(each.ComponentType)
			cr.GetOrCreateCompConf(compType).SetImage(each.ImageRepo, each.ImageTag)

			// TiflashLearner is a module of tiflash instead of an independent container, so it doesn't need image and tag,
			// and the upstream(Global, Config Center) doesn't provide these too.
			// But for data structure consistency and backward compatibility, we still need to add it.
			if each.ComponentType == apicommon.ComponentType_TIFLASH {
				cr.GetOrCreateCompConf(cloudv1.ComponentTiFlashLearner).SetImage(each.ImageRepo, each.ImageTag)
			}
		}
	}

	return nil
}

// updateConfigWithoutConfigChangeCR updates the config revision without creating a new config change CR.
func updateConfigWithoutConfigChangeCR(ctx context.Context, cli client.Client, cluster *cloudv1.Cluster, configurations []*apiv1.ComponentConfiguration, version *apiv1.ClusterVersion) (newConfigRevisionName string, err error) {
	cr, err := getConfigRevisionByCluster(ctx, cli, cluster)
	if err != nil {
		return "", status.Errorf(codes.Internal, "failed to get config revision %s: %s", cluster.Spec.ConfigRevisionRef.Name, err)
	}

	// generate and create new ConfigRevision
	newCR := cr.DeepCopy()
	if err = overlayConfigAndVersion(ctx, newCR, configurations, version); err != nil {
		return "", status.Errorf(codes.Internal, "failed to overlay config: %s", err)
	}

	if !equality.Semantic.DeepEqual(cr, newCR) {
		revision.RectifyConfigRevisionForCreate(cluster, newCR, "")
		if err = k8s.CreateIfNotExist(ctx, cli, newCR); err != nil {
			return "", status.Errorf(codes.Internal, "failed to create config revision: %s", err)
		}
		return newCR.Name, nil
	} else {
		return cr.Name, nil
	}
}
