package networkpeer

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	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/networkpeer/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/applier"
	"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"
)

// NetworkPeerReconciler reconciles a NetworkPeer object
type NetworkPeerReconciler struct {
	Logger logr.Logger
	Config config.RootConfig

	InfraCli      client.Client
	APIReader     client.Reader
	EventRecorder record.EventRecorder
}

// Reconcile drives the NetworkPeer object to the desired state.
func (r *NetworkPeerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
	logger := log.FromContext(ctx)

	logger.Info("Reconciling NetworkPeer")
	start := time.Now()

	defer func() {
		metricsctrl.ReconcileDuration.
			WithLabelValues("network_peer").Observe(time.Since(start).Seconds())
		logger.Info("finish reconcile")
	}()

	peer := &cloudv1.NetworkPeer{}
	if err := r.InfraCli.Get(ctx, req.NamespacedName, peer); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	providerType := peer.Labels[cloudv1.ProviderTypeLabel]
	if providerType != consts.ProviderTypeEKS && providerType != consts.ProviderTypeGKE {
		return ctrl.Result{}, nil
	}

	network := applier.NewObject[*cloudv1.Network](req.Namespace, peer.Spec.Source)
	if err := r.InfraCli.Get(ctx, client.ObjectKeyFromObject(network), network); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("Network not found", "network", peer.Spec.Source)
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get network %s: %v", peer.Spec.Source, err)
	}

	var target *cloudv1.Network
	if peer.Spec.Target.TargetNetwork != nil {
		targetName := peer.Spec.Target.TargetNetwork.NetworkName
		obj := applier.NewObject[*cloudv1.Network](req.Namespace, targetName)
		if err := r.APIReader.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
			if apierrors.IsNotFound(err) {
				logger.Info("target network not found", "network", targetName)
				return ctrl.Result{}, nil
			}
			return ctrl.Result{}, fmt.Errorf("unable to get network %s: %v", targetName, err)
		}

		target = obj
	}

	c, err := (&task.Context{
		Local:         r.InfraCli,
		ProviderType:  providerType,
		NetworkPeer:   peer,
		Network:       network,
		TargetNetwork: target,
	}).Default()
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("failed to new context: %v", err)
	}

	tasks := GetNetworkPeerTasks(r, c)

	d := deploy.NewDeployer(tasks, nil, logger)
	if peer.DeletionTimestamp.IsZero() {
		res, err = updateCreate(ctx, c, d.Deploy(ctx))
	} else {
		res, err = updateDelete(ctx, c, d.Destroy(ctx))
	}
	if err == nil {
		r.EventRecorder.Event(peer, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		r.EventRecorder.Event(peer, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}

	return res, err
}

// SetupWithManager sets up the controller with the Manager.
func (r *NetworkPeerReconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&cloudv1.NetworkPeer{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{RateLimiter: k8s.RetryLimiter, MaxConcurrentReconciles: controllers.GetMaxConcurrentReconciles(r.Config, "networkPeer")}).
		Complete(r)
}
