package hibernation

import (
	"context"
	"fmt"
	"sync"
	"time"

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

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/gardener"
	gardencorev1beta1 "github.com/tidbcloud/infra-api/pkg/gardener/core/apis/v1beta1"
	metricsctrl "github.com/tidbcloud/infra-api/pkg/metrics/controller"
)

type HibernationController struct {
	gardenCli         client.Client
	log               logr.Logger
	gardener          gardener.GardenerAPI
	infraCli          client.Client
	idleShoot         sync.Map
	idleTimeThreshold time.Duration
	checkIdleInterval time.Duration
	dryRun            bool
	recorder          record.EventRecorder
	labelselector     metav1.LabelSelector
}

func NewHibernationController(gardenCli, infracli client.Client, g gardener.GardenerAPI, recorder record.EventRecorder, l metav1.LabelSelector, hibernation cloudv1.Hibernation) *HibernationController {
	return &HibernationController{
		gardenCli:         gardenCli,
		log:               ctrl.Log.WithName("controllers").WithName("Shoot").WithName("hibernation"),
		gardener:          g,
		infraCli:          infracli,
		idleTimeThreshold: hibernation.IdleTimeThreshold.Duration,
		checkIdleInterval: hibernation.CheckIdleInterval.Duration,
		recorder:          recorder,
		dryRun:            hibernation.DryRun,
		labelselector:     l,
	}
}

func (r *HibernationController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	start := time.Now()
	defer func() {
		metricsctrl.ReconcileDuration.WithLabelValues("hibernation").Observe(time.Since(start).Seconds())
	}()
	_ = r.log.WithValues("shoot", req.NamespacedName)

	c := &gardencorev1beta1.Shoot{}
	err := r.gardenCli.Get(ctx, req.NamespacedName, c)
	if err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	r.gardener.CancelShootContext(c.Name)
	return ctrl.Result{}, nil
}

func (r *HibernationController) SetupWithManager(mgr ctrl.Manager) error {
	r.log.Info("start hibernation controller", "checkIdleInterval", r.checkIdleInterval, "idleTimeThreshold", r.idleTimeThreshold, "mode", r.dryRun)
	go func() {
		for {
			time.Sleep(r.checkIdleInterval)
			if r.gardener.Started() {
				r.Estimator()
			}
		}
	}()

	hibernationPredicate := predicate.NewPredicateFuncs(func(o client.Object) bool {
		shootObj := o.(*gardencorev1beta1.Shoot)
		return EnableHibernation(shootObj)
	})

	return ctrl.NewControllerManagedBy(mgr).
		For(&gardencorev1beta1.Shoot{}).
		WithEventFilter(hibernationPredicate).
		WithEventFilter(predicate.GenerationChangedPredicate{}).
		Complete(r)
}

func EnableHibernation(s *gardencorev1beta1.Shoot) bool {
	return s.Spec.Hibernation != nil && s.Spec.Hibernation.Enabled != nil && *s.Spec.Hibernation.Enabled == true
}

func (r *HibernationController) Estimator() {
	networks := new(cloudv1.NetworkList)
	selector, err := metav1.LabelSelectorAsSelector(&r.labelselector)
	if err != nil {
		r.log.Error(err, "invalid label selector", "labelselector", r.labelselector)
	}
	err = r.infraCli.List(context.TODO(), networks, client.MatchingLabelsSelector{
		Selector: selector,
	})
	if err != nil {
		r.log.Error(err, "failed list network")
	}

	for _, network := range networks.Items {
		// only hibernation when network status is normal
		if network.Status.Phase == cloudv1.NetworkPhaseNormal {
			// trying to get shoot name from network status
			networkStatus, err := cloudv1.GardenerNetworkStatusFromNetwork(&network)
			if err != nil {
				r.log.Error(err, "failed retrieve status from network")
				continue
			}
			networkShootName := networkStatus.ShootName
			if networkShootName == "" {
				continue
			}
			// shoot not exits, skip it
			s, err := r.gardener.GetShoot(context.TODO(), networkShootName)
			if err != nil {
				r.log.Error(err, "failed get shoot")
				continue
			}
			// if shoot already hibernation, just skip it
			if EnableHibernation(s) {
				continue
			}
			r.hibernationHandlle(s, network)
		}
	}
}

func (r *HibernationController) hibernationHandlle(shoot *gardencorev1beta1.Shoot, network cloudv1.Network) {
	key := fmt.Sprintf("%s-%s", shoot.Namespace, shoot.Name)
	if r.isIdle(shoot, network) {
		if value, ok := r.idleShoot.Load(key); ok {
			idleDurationTotal := value.(time.Duration)
			idleDurationTotal += r.checkIdleInterval
			r.idleShoot.Store(key, idleDurationTotal)
			// if the shoot is idle for a long time, then hibernate it
			if idleDurationTotal > r.idleTimeThreshold {
				r.recorder.Event(&network, corev1.EventTypeNormal, "HibernatingIdleShoot", fmt.Sprintf("Shoot %s is idle over(%s), will trying hibernate it", shoot.Name, r.idleTimeThreshold.String()))
				if err := r.hibernateShoot(shoot, network); err != nil {
					// hibernate failed will retry
					r.log.Error(err, "failed hibernate", "shoot", shoot.Name)
				} else {
					// shoot already hibernation, remove it from idle shoots collection
					r.idleShoot.Delete(key)
				}
			}
		} else {
			r.idleShoot.Store(key, r.checkIdleInterval)
		}
	} else {
		r.idleShoot.Delete(key)
	}
}

func (r *HibernationController) isIdle(shoot *gardencorev1beta1.Shoot, network cloudv1.Network) bool {
	// if shoot only have master work group, continue check it
	if !onlyMasterWorker(shoot) {
		return false
	}

	// counts clusters in special network, if count is 0, continue check it
	if r.clusterExistInNetwork(network) {
		return false
	}

	// shoot not awaked recently, we can think shoot is idle
	if lastAwakedTimeLessThan(shoot, r.idleTimeThreshold) {
		return false
	}
	return true
}

func (r *HibernationController) clusterExistInNetwork(network cloudv1.Network) bool {
	// if request report an error, we also think cluster exist in network
	clusters := new(cloudv1.ClusterList)
	err := r.infraCli.List(context.TODO(), clusters, client.MatchingFields(map[string]string{
		"spec.networkName": network.Name}), client.InNamespace(network.Namespace))
	if err != nil {
		r.log.Error(err, "failed list cluster by network name")
		return true
	}

	if len(clusters.Items) == 0 {
		return false
	}

	return true

}

func onlyMasterWorker(shoot *gardencorev1beta1.Shoot) bool {
	if len(shoot.Spec.Provider.Workers) != 1 {
		return false
	}
	worker := shoot.Spec.Provider.Workers[0]
	return worker.Name == gardener.MasterWorkerGroup
}

func (r *HibernationController) hibernateShoot(shoot *gardencorev1beta1.Shoot, network cloudv1.Network) error {
	if r.dryRun {
		return nil
	}
	ctx := context.TODO()
	st, err := r.gardener.GetShootContext(ctx, shoot.Name)
	if err != nil {
		return err
	}

	err = ensureProxyServiceDeleted(st.Manager.GetClient())
	if err != nil {
		return err
	}
	r.recorder.Event(&network, corev1.EventTypeNormal, "CleanProxyLoadbalancer", "Proxy load balancer deleted")
	err = r.gardener.HibernateShoot(ctx, shoot.Name)
	if err != nil {
		return err
	}
	r.recorder.Event(&network, corev1.EventTypeNormal, "HibernationSuccessful", "Shoot hibernation successsful")
	// update network status to hibernated
	new := network.DeepCopy()
	new.Status.Phase = cloudv1.NetworkPhaseHibernated
	return r.infraCli.Status().Update(context.TODO(), new)
}

// ensureProxyServiceDeleted delete proxy service, proxy service is loadbalancer type.
// wait loadbalancer deleted by cloudprovider and object removed in etcd store
func ensureProxyServiceDeleted(shootCli client.Client) error {
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "dbaas-proxy",
			Namespace: "tidb-admin",
		},
	}
	ctx := context.TODO()
	err := shootCli.Delete(ctx, svc)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}
	err = shootCli.Get(ctx, types.NamespacedName{
		Name:      svc.Name,
		Namespace: svc.Namespace,
	}, svc)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}
	return fmt.Errorf("Wait dbaas-proxy load balancer deleted")
}

func lastAwakedTimeLessThan(shoot *gardencorev1beta1.Shoot, idleTimeThreshold time.Duration) bool {
	lastAwakeTime := shoot.CreationTimestamp.Time
	if lastAwakeTimeStr, ok := shoot.Annotations[gardener.AwakeTimeAnnotation]; ok {
		t, err := time.Parse(time.RFC3339, lastAwakeTimeStr)
		if err == nil {
			lastAwakeTime = t
		}
	}

	if time.Since(lastAwakeTime) < idleTimeThreshold {
		return true
	}
	return false
}
