package hibernation

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

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"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"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	infraconst "github.com/tidbcloud/infra-provider/pkg/consts"
)

const (
	// AwakeTimeAnnotation is annotation that record last awake time
	AwakeTimeAnnotation = "tidbcloud.com/awake-time"
)

type HibernationController struct {
	log               logr.Logger
	infraCli          client.Client
	idleNetwork       sync.Map
	idleTimeThreshold time.Duration
	checkIdleInterval time.Duration
	dryRun            bool
	recorder          record.EventRecorder
	labelselector     metav1.LabelSelector
	onlyMasterWorker  func(infracli client.Client, network cloudv1.Network) bool
	featureEnabled    func(tenantID, projectID string) bool
}

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,
		onlyMasterWorker:  onlyMasterWorker,
		featureEnabled:    featureEnabled,
	}
}

func (r *HibernationController) RunCloudProviderHibernationController() {
	go func() {
		for {
			time.Sleep(r.checkIdleInterval)
			r.Estimator()
		}
	}()
}

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.TenantLabelKey]
}

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

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.featureEnabled(getTenantID(&network), getProjectID(&network)) {
				r.hibernationHandlle(network)
			}
		}
	}
}

func (r *HibernationController) hibernationHandlle(network cloudv1.Network) {
	key := fmt.Sprintf("%s-%s", network.Namespace, network.Name)
	if r.isIdle(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
				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)

				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(network cloudv1.Network) bool {
	if !r.onlyMasterWorker(r.infraCli, network) {
		return false
	}

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

	var meta metav1.ObjectMeta
	meta = network.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 (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 lastAwakedTimeLessThan(meta metav1.ObjectMeta, idleTimeThreshold time.Duration) bool {
	lastAwakeTime := meta.CreationTimestamp.Time
	if lastAwakeTimeStr, ok := meta.Annotations[AwakeTimeAnnotation]; ok {
		t, err := time.Parse(time.RFC3339, lastAwakeTimeStr)
		if err == nil {
			lastAwakeTime = t
		}
	}

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