package dispatcher

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	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/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"

	"github.com/tidbcloud/shared-provider/pkg/manager"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
)

type Metrics interface {
	UpdateMetrics(obj client.Object)
	DeleteMetrics(obj client.ObjectKey)
}

type Dispatcher interface {
	New() client.Object
	Watch(c controller.Controller) error
	Schedule(ctx context.Context, obj client.Object) (string, error)
	Metrics
}

// Reconciler reconciles a Network object.
type Reconciler struct {
	Logger logr.Logger

	InfraClient   client.Client
	InfraCache    cache.Cache
	InfraRecorder record.EventRecorder

	d Dispatcher
}

//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=networks,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=clusters,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=backups,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=restores,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=imports,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=core,namespace=default,resources=configmaps,verbs=get;list;watch

// 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 (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := r.Logger.WithValues("namespace", req.Namespace, "name", req.Name)
	logger.Info("Start reconcile")

	startTime := time.Now()
	// TODO(liubo02): combine two defer log
	defer func() {
		logger.Info("Finish reconcile",
			"time", time.Since(startTime),
		)
	}()

	obj := r.d.New()

	if err := r.InfraCache.Get(ctx, req.NamespacedName, obj); err != nil {
		if apierrors.IsNotFound(err) {
			r.d.DeleteMetrics(req.NamespacedName)
			return ctrl.Result{}, nil
		}

		return ctrl.Result{}, fmt.Errorf("can't get resource %s from cache: %w", req.NamespacedName, err)
	}

	curResourceVersion := obj.GetResourceVersion()

	defer func() {
		logger = logger.WithValues("resourceVersion.before", curResourceVersion).
			WithValues("resourceVersion.after", obj.GetResourceVersion())
	}()

	r.d.UpdateMetrics(obj)

	scheduleTo, err := r.d.Schedule(ctx, obj)
	if err != nil {
		return ctrl.Result{}, err
	}

	if scheduleTo == "" {
		return ctrl.Result{}, nil
	}

	lbs := obj.GetLabels()
	lbs = labels.NewKubeClusterLabels(lbs, scheduleTo)
	obj.SetLabels(lbs)

	if err := r.InfraClient.Update(ctx, obj); err != nil {
		return ctrl.Result{}, fmt.Errorf("can't update labels for %s: %w", req.NamespacedName, err)
	}

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr manager.Manager) error {
	r.InfraClient = mgr.InfraCluster().GetClient()
	r.InfraCache = mgr.InfraCluster().GetCache()
	r.InfraRecorder = mgr.InfraCluster().GetEventRecorderFor("Dispatcher")

	c, err := controller.New("Dispatcher", mgr, controller.Options{
		Reconciler: r,
	})
	if err != nil {
		return fmt.Errorf("can't new a controller: %w", err)
	}

	r.Logger = c.GetLogger()

	return r.d.Watch(c)
}

func New(d Dispatcher) *Reconciler {
	return &Reconciler{
		d: d,
	}
}
