// Package imports add by blacknc <blacknc@163.com> at 2024/08/27 23:43:21
package imports

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	"go.uber.org/multierr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	"github.com/tidbcloud/eks-provider/controllers"
	"github.com/tidbcloud/eks-provider/controllers/imports/task"
	"github.com/tidbcloud/eks-provider/controllers/utils"
	putils "github.com/tidbcloud/eks-provider/controllers/utils"
	utilplane "github.com/tidbcloud/eks-provider/controllers/utils/dataplane"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/metrics"
)

// ImportReconciler reconciles a Import object
type ImportReconciler struct {
	client.Client
	Logger        logr.Logger
	EventRecorder record.EventRecorder
	Manager       dataplane.K8sClientManager
	Cache         client.Reader
	Config        config.RootConfig
}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the import task closer to the desired state.
func (r *ImportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	ctx, cancel := context.WithTimeout(ctx, time.Minute)
	defer cancel()
	logger := log.FromContext(ctx)

	logger.Info("Reconciling Import")
	start := time.Now()
	defer func() {
		metrics.ReconcileDuration.
			WithLabelValues("import-3layer", r.Config.Provider, r.Config.Network.Region).Observe(float64(time.Since(start).Milliseconds()))
		logger.Info("Finish reconcile", "take", time.Since(start))
	}()

	var importCR cloudv1.Import
	if err := r.Cache.Get(ctx, req.NamespacedName, &importCR); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("Import CR not found, skipping", "import", req.NamespacedName)
			return ctrl.Result{}, nil
		}
		metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "GetImportCR").Inc()
		return ctrl.Result{}, fmt.Errorf("unable to get import CR %s: %v", req.NamespacedName, err)
	}

	providerType := importCR.GetProviderType()
	if !isProviderSupported(providerType) {
		logger.Info("unsupported providerr type", "import", req.NamespacedName, "provider_type", providerType)
		return ctrl.Result{}, nil
	} else if r.Config.Predicate.MatchLabels[cloudv1.ProviderTypeLabel] != providerType {
		logger.Info("provider type not match", "import", req.NamespacedName, "provider_type", providerType)
		return ctrl.Result{}, nil
	}

	clusterID, err1 := putils.GetClusterIDFromLabel(&importCR)
	projectID, err2 := putils.GetProjectIDFromLabel(&importCR)
	tenantID, err3 := putils.GetTenantIDFromLabel(&importCR)
	err := multierr.Combine(err1, err2, err3)
	if err != nil {
		metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "ParseTenantClusterID").Inc()
		return ctrl.Result{}, fmt.Errorf("failed to get import tenant && project && cluster label, import:%s, cluster:%s, err:%v", req.NamespacedName, importCR.Spec.ClusterName, err)
	}
	dataplaneProvider, err := utilplane.NewDataplaneProvider(ctx, logger, tenantID, projectID, clusterID, &r.Config)
	if err != nil {
		metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "NewDataplaneProvider").Inc()
		return ctrl.Result{}, fmt.Errorf("failed to get dataplane provider impl, import:%s, cluster:%s, err:%v", req.NamespacedName, importCR.Spec.ClusterName, err)
	}
	kubernetesInfo := dataplaneProvider.GetKubernetes()
	cluster := dataplaneProvider.GetCluster()

	var ep dataplane.EnvProvider
	if dataplaneProvider.IsDataplaneExist() {
		var dataplaneKubeData []byte
		// if kubeconfigSecretRef is set, get the kubeconfig from the secret
		if kubernetesInfo.KubeConfigContent != "" {
			var err error
			dataplaneKubeData, err = dataplane.OverrideAksKubeconfigWithBase64([]byte(kubernetesInfo.KubeConfigContent))
			if err != nil {
				r.EventRecorder.Eventf(&importCR, corev1.EventTypeWarning, "CannotDecodeKubeConfig", "cannot base64 decode kubeconfig, import:%s, err:%v", req.NamespacedName, err)
				metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "OverrideAksKubeconfig").Inc()
				return ctrl.Result{}, fmt.Errorf("cannot base64 decode kubeconfig: %w", err)
			}
			logger.Info("get dataplane kubeconfig from appbox KubeConfigContent", "kubeconfig_len", len(dataplaneKubeData))
		} else if ref := kubernetesInfo.KubeConfigSecretRef; ref != nil {
			var err error
			if dataplaneKubeData, err = dataplane.GetDataplaneKubeConfigFromMetaSecret(ctx, r.Client, ref); err != nil {
				r.EventRecorder.Eventf(&importCR, corev1.EventTypeWarning, "CannotGetKubeConfig", "cannot get kubeconfig, import:%s, err:%v", req.NamespacedName, err)
				metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "GetKubeconfigFromSecret").Inc()
				return ctrl.Result{}, fmt.Errorf("cannot get kubeconfig: %w", err)
			}
			logger.Info("get dataplane kubeconfig from appbox KubeConfigSecretRef", "kubeconfig_len", len(dataplaneKubeData))
		}

		ep, err = dataplane.NewWithKubenets(r.Manager,
			clusterID,
			kubernetesInfo.Kubernetes,
			r.Config.Network.Region,
			r.Config.FullManage.AssumeRoleName,
			r.Config.FullManage.AssumeRoleExternalID,
			dataplaneKubeData)
		if err != nil {
			r.EventRecorder.Eventf(&importCR, corev1.EventTypeWarning, "CannotNewEnvProvider", "cannot new env provier: %v", err)
			metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "NewEnvProvider").Inc()
			return ctrl.Result{}, fmt.Errorf("cannot new env provider: %w", err)
		}
	} else {
		ep = &dataplane.NopEnvProvider{}
	}

	c, err := (&task.Context{
		EnvProvider:   ep,
		Local:         r.Client,
		EventRecorder: r.EventRecorder,
		Config:        r.Config,
		Cluster:       cluster,
		ClusterRole:   dataplaneProvider.GetClusterRole(),
		Kubernetes:    kubernetesInfo.Kubernetes,
		Import:        &importCR,
		Logger:        logger,
		ClusterID:     clusterID,
		ProjectID:     projectID,
		TenantID:      tenantID,
	}).Default()
	if err != nil {
		metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "InitContext").Inc()
		return ctrl.Result{}, fmt.Errorf("failed to new context: %v", err)
	}

	tasks := GetImportTasks(r, c)

	var res ctrl.Result

	d := deploy.NewDeployer(tasks, nil, logger)
	if importCR.DeletionTimestamp.IsZero() && c.IsDataplaneExist() && c.IsClusterExist() {
		res, err = updateCreate(ctx, c, d.Deploy(ctx))
	} else {
		res, err = updateDelete(ctx, c, d.Destroy(ctx))
	}

	if err == nil {
		r.EventRecorder.Event(&importCR, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		metrics.ImportFailCounter.WithLabelValues(r.Config.Provider, r.Config.Network.Region, "ReconcileFail").Inc()
		r.EventRecorder.Event(&importCR, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}

	return res, err
}

// SetupWithManager sets up the controller with the Manager.
func (r *ImportReconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {

	if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &cloudv1.Import{}, "spec.clusterName", func(obj client.Object) []string {
		importObj := obj.(*cloudv1.Import)
		return []string{importObj.Spec.ClusterName}
	}); err != nil {
		return err
	}

	return ctrl.NewControllerManagedBy(mgr).
		For(&cloudv1.Import{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{RateLimiter: k8s.RetryLimiter, MaxConcurrentReconciles: controllers.GetMaxConcurrentReconciles(r.Config, "import")}).
		WithEventFilter(utils.GetMigrationAnnotationSkipPredicator()).
		Complete(r)
}

func isProviderSupported(provider string) bool {
	switch provider {
	case
		consts.ProviderTypeEKS,
		consts.ProviderTypeGKE,
		consts.ProviderTypeAKS:
		return true
	default:
		return false
	}
}
