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"

	"sigs.k8s.io/controller-runtime/pkg/cache"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	infraconst "github.com/tidbcloud/infra-api/pkg/consts"
	"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
	idleNetwork       sync.Map
	idleTimeThreshold time.Duration
	checkIdleInterval time.Duration
	dryRun            bool
	recorder          record.EventRecorder
	labelselector     metav1.LabelSelector
	// Cloud vendor kubernetes cluster, such as EKS and GKE
	isCloudProvider  bool
	onlyMasterWorker func(infracli client.Client, network cloudv1.Network) bool
	featureEnabled   func(tenantID, projectID string) bool
}

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,
		isCloudProvider:   false,
	}
}

func NewCloudProviderHibernationController(infracli client.Client, recorder record.EventRecorder, l metav1.LabelSelector, hibernation cloudv1.Hibernation, onlyMasterWorker func(infracli client.Client, network cloudv1.Network) bool, featureEnabled func(tenantID, projectID string) bool) *HibernationController {
	return &HibernationController{
		log:               ctrl.Log.WithName("controllers").WithName("eks").WithName("hibernation"),
		infraCli:          infracli,
		idleTimeThreshold: hibernation.IdleTimeThreshold.Duration,
		checkIdleInterval: hibernation.CheckIdleInterval.Duration,
		recorder:          recorder,
		dryRun:            hibernation.DryRun,
		labelselector:     l,
		isCloudProvider:   true,
		onlyMasterWorker:  onlyMasterWorker,
		featureEnabled:    featureEnabled,
	}
}

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) RunCloudProviderHibernationController() {
	go func() {
		for {
			time.Sleep(r.checkIdleInterval)
			r.Estimator()
		}
	}()
}

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 isErrCacheNotStarted(err error) bool {
	ec := new(cache.ErrCacheNotStarted)
	if ec.Error() == err.Error() {
		return true
	}
	return false
}

func getTenantID(network *cloudv1.Network) string {
	return network.Labels[infraconst.TenantTagKey]
}

func getProjectID(network *cloudv1.Network) string {
	return network.Labels[infraconst.ProjectTagKey]
}

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 {
		if isErrCacheNotStarted(err) {
			return
		}
		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 {
			if r.isCloudProvider {
				if r.featureEnabled(getTenantID(&network), getProjectID(&network)) {
					r.hibernationHandlle(nil, network)
				}
			} else {
				// 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", network.Namespace, network.Name)
	if r.isIdle(shoot, network) {
		if value, ok := r.idleNetwork.Load(key); ok {
			idleDurationTotal := value.(time.Duration)
			idleDurationTotal += r.checkIdleInterval
			r.idleNetwork.Store(key, idleDurationTotal)
			// if the shoot is idle for a long time, then hibernate it
			if idleDurationTotal > r.idleTimeThreshold {
				var err error
				if r.isCloudProvider {
					r.recorder.Event(&network, corev1.EventTypeNormal, "HibernatingNetwork", fmt.Sprintf("network %s is idle over(%s), will trying hibernate it", network.Name, r.idleTimeThreshold.String()))
					// If network is idle over threshold, update network status to hibernation.
					// eks network controller will clean up unused resources and update status hibernated
					err = r.hibernateNetwork(network)
				} else {
					r.recorder.Event(&network, corev1.EventTypeNormal, "HibernatingIdleShoot", fmt.Sprintf("Shoot %s is idle over(%s), will trying hibernate it", shoot.Name, r.idleTimeThreshold.String()))
					err = r.hibernateShoot(shoot, network)
				}
				if err != nil {
					// hibernate failed will retry
					r.log.Error(err, "failed hibernate", "network", network.Name)
				} else {
					// shoot already hibernation, remove it from idle shoots collection
					r.idleNetwork.Delete(key)
				}
			}
		} else {
			r.idleNetwork.Store(key, r.checkIdleInterval)
		}
	} else {
		r.idleNetwork.Delete(key)
	}
}

func (r *HibernationController) isIdle(shoot *gardencorev1beta1.Shoot, network cloudv1.Network) bool {
	if r.isCloudProvider {
		if !r.onlyMasterWorker(r.infraCli, network) {
			return false
		}
	} else {
		// 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
	}

	var meta metav1.ObjectMeta
	if r.isCloudProvider {
		meta = network.ObjectMeta
	} else {
		meta = shoot.ObjectMeta
	}
	// network not awaked recently, we can think shoot is idle
	if lastAwakedTimeLessThan(meta, 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) hibernateNetwork(network cloudv1.Network) error {
	if r.dryRun {
		return nil
	}
	// update network status to hibernating
	new := network.DeepCopy()
	new.Status.Phase = cloudv1.NetworkPhaseHibernating
	if new.Annotations == nil {
		new.Annotations = map[string]string{}
	}
	// record hibernating time
	new.Annotations[cloudv1.NetworkHibernatingTimeAnnotation] = time.Now().Format(time.RFC3339)
	return r.infraCli.Status().Update(context.TODO(), new)
}

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(meta metav1.ObjectMeta, idleTimeThreshold time.Duration) bool {
	lastAwakeTime := meta.CreationTimestamp.Time
	if lastAwakeTimeStr, ok := meta.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
}
