/*
Copyright 2021.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package dumpling

import (
	"context"
	"fmt"
	"time"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	"k8s.io/utils/pointer"
	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/controller/controllerutil"
	"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/dumpling/task"
	"github.com/tidbcloud/eks-provider/controllers/dumpling/task/common"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	compatk8s "github.com/tidbcloud/infra-provider/pkg/compat/k8s/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	metricsctrl "github.com/tidbcloud/infra-provider/pkg/metrics/controller"
)

const (
	APIVersion            = "cloud.pingcap.com/v1"
	Kind                  = "Cluster"
	NetworkNameLabel      = "dumpling.tidbcloud.com/network-name"
	NetworkNamespaceLabel = "dumpling.tidbcloud.com/network-namespace"
)

// DumplingReconciler reconciles a Dumpling object
type DumplingReconciler struct {
	EventRecorder record.EventRecorder
	client.Client
	Cache  client.Reader
	Config config.RootConfig
}

//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=dumplings,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=dumplings/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=dumplings/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (reconcile *DumplingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	logger.Info("Reconciling Dumpling")
	start := time.Now()
	defer func() {
		metricsctrl.ReconcileDuration.
			WithLabelValues("dumpling").Observe(time.Since(start).Seconds())
		logger.Info("Finish reconcile", "take", time.Since(start))
	}()

	var dumpling cloudv1.Dumpling
	if err := reconcile.Cache.Get(ctx, req.NamespacedName, &dumpling); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("dumpling cr is not found", "dumpling namespaced name is", req.NamespacedName)
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get dumpling %s: %v", req.NamespacedName, err)
	}

	// Add Finializer to make sure related job and secret will be deleted when dumpling CR is deleted
	if !controllerutil.ContainsFinalizer(&dumpling, common.Finalizer) {
		controllerutil.AddFinalizer(&dumpling, common.Finalizer)
		if err := reconcile.Client.Update(ctx, &dumpling); err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to add finalizer: %v", err)
		}
	}

	// get cluster / network / provider and do check
	var cluster cloudv1.Cluster
	namespacedName := types.NamespacedName{
		Name:      dumpling.Spec.ClusterName,
		Namespace: dumpling.ObjectMeta.Namespace,
	}

	clusterNotFound := false
	if err := reconcile.Cache.Get(ctx, namespacedName, &cluster); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("cluster not found", "cluster namespaced name", namespacedName)
			clusterNotFound = true
		} else {
			return ctrl.Result{}, fmt.Errorf("unable to get cluster %s: %v", req.NamespacedName, err)
		}
	}

	// return ctrl.Result{}, nil
	var network cloudv1.Network
	var networkKey types.NamespacedName

	if !clusterNotFound {
		networkKey = cluster.GetNetworkRef()
	} else {
		if dumpling.ObjectMeta.Labels[NetworkNameLabel] == "" || dumpling.ObjectMeta.Labels[NetworkNamespaceLabel] == "" {
			return ctrl.Result{}, fmt.Errorf("unable to get cluster %s and can't get network info in labels %s", req.NamespacedName, &dumpling.ObjectMeta.Labels)
		}
		networkKey = types.NamespacedName{
			Name:      dumpling.ObjectMeta.Labels[NetworkNameLabel],
			Namespace: dumpling.ObjectMeta.Labels[NetworkNamespaceLabel],
		}
		logger.Info("get network info from labels")
	}

	if err := reconcile.Cache.Get(ctx, networkKey, &network); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("Network not found", "network", cluster.Spec.NetworkName)
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get network %s: %v", cluster.Spec.NetworkName, err)
	}

	providerType := network.Spec.Provider
	// we only support EKS and GKE now
	if providerType != consts.ProviderTypeEKS && providerType != consts.ProviderTypeGKE {
		logger.Info("ignore unknown provider type", "provider", providerType)
		return ctrl.Result{}, nil
	}
	if !config.FeatureEnabled(reconcile.Config.FeatureGates, config.EnableDPCompatibilityMode, "", "") {
		// set owner reference and labels at first time
		{
			if len(dumpling.ObjectMeta.OwnerReferences) == 0 {
				// update dumpling controller ownerrefernce
				dumpling.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
					{
						APIVersion:         APIVersion,
						Kind:               Kind,
						Name:               cluster.ObjectMeta.Name,
						UID:                cluster.ObjectMeta.UID,
						BlockOwnerDeletion: pointer.Bool(true),
					},
				}

				if err := reconcile.Client.Update(ctx, &dumpling); err != nil {
					return ctrl.Result{}, fmt.Errorf("update dumpling cr owner reference and labels failed %v", err)
				}
				logger.Info("updated dumpling cr with owner reference and labels")
			}
		}
	}

	taskcontext, err := (&task.Context{
		Local:    reconcile.Client,
		Dumpling: &dumpling,
		Provider: &reconcile.Config,
		Network:  &network,
		TaskName: req.Name,
		Image:    reconcile.Config.ControllerConfig.DumplingConfig.DumplingImage,
	}).Default()
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to new context: %v", err)
	}

	// set remote client
	remote, err := reconcile.getRemoteClient(ctx, &network)
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to get remote client: %v", err)
	}
	taskcontext.Remote = remote

	tasks := GetDumplingTasks(reconcile, taskcontext)

	var res ctrl.Result

	d := deploy.NewDeployer(tasks, nil, logger)
	if dumpling.DeletionTimestamp.IsZero() {
		res, err = updateCreate(ctx, taskcontext, d.Deploy(ctx))
	} else {
		res, err = updateDelete(ctx, taskcontext, d.Destroy(ctx))
	}

	if err == nil {
		reconcile.EventRecorder.Event(&dumpling, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		reconcile.EventRecorder.Event(&dumpling, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}

	return res, err
}

func (reconcile *DumplingReconciler) getRemoteClient(ctx context.Context, network *cloudv1.Network) (k8s.ClientGetter, error) {
	scheme, err := k8s.TiDBOperatorScheme()
	if err != nil {
		return nil, fmt.Errorf("failed to get scheme: %v", err)
	}
	getter, err := compatk8s.NetworkClientGetter(ctx, reconcile.Client, network, scheme)
	if err != nil {
		return nil, fmt.Errorf("failed to get getter: %v", err)
	}
	return k8s.NewCacheClientGetter(getter), nil
}

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

	return ctrl.NewControllerManagedBy(mgr).
		For(&cloudv1.Dumpling{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{RateLimiter: k8s.RetryLimiter, MaxConcurrentReconciles: controllers.GetMaxConcurrentReconciles(reconcile.Config, "dumpling")}).
		Complete(reconcile)
}
