package gc

import (
	"context"
	"time"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/approute"
	"k8s.io/apimachinery/pkg/fields"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type appBoxGCWorker struct {
	conf      config.GCConfig
	appboxrw  appbox.APPBoxReaderWriter
	approuter approute.APPRouteReader
	log       logr.Logger
}

func NewappBoxGCWorker(cli client.Client, log logr.Logger, conf config.GCConfig) *appBoxGCWorker {
	return &appBoxGCWorker{
		appboxrw:  appbox.NewAPPBoxReaderWriter(cli),
		approuter: approute.NewAPPRouteReader(cli),
		log:       log,
		conf:      conf,
	}
}

func (g *appBoxGCWorker) scanIdle(ctx context.Context) error {
	appboxs := &v1alpha1.APPBoxList{}
	if err := g.appboxrw.List(ctx, appboxs); err != nil {
		return err
	}

	for _, appbox := range appboxs.Items {
		appbox := appbox
		if appbox.Status.Phase != v1alpha1.APPBoxPhaseActive {
			continue
		}
		if appbox.GetAnnotations()[v1alpha1.SkipGCAnnotationKey] == v1alpha1.SkipGCAnnotationVal {
			g.log.Info("skip gc", "appbox", appbox.Name)
			continue
		}
		if err := g.checkIdleAPPBoxOne(ctx, &appbox); err != nil {
			g.log.Error(err, "check idle appbox failed", "appbox", appbox.Name)
			continue
		}
	}
	return nil
}

// checkIdleAPPBoxOne checks if the appbox is idle at this time.
// if the appbox is idle, update the idleStartTime annotation.
// if the appbox is idle for a long time, set phase to ReadyForDelete and add it to readyForDeleteAPPBox.
// if the appbox is not idle, remove the idleStartTime annotation.
func (g *appBoxGCWorker) checkIdleAPPBoxOne(ctx context.Context, appbox *v1alpha1.APPBox) error {
	if !g.isIdleAPPBox(ctx, appbox) {
		if appbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime] != "" {
			delete(appbox.Annotations, v1alpha1.AnnotationKeyIdelStartTime)
			return g.appboxrw.Update(ctx, appbox)
		}
		return nil
	}

	idleStartTimeStr := appbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime]
	if idleStartTimeStr == "" {
		if appbox.Annotations == nil {
			appbox.Annotations = make(map[string]string)
		}
		appbox.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = time.Now().Format(time.RFC3339)
		return g.appboxrw.Update(ctx, appbox)
	}
	idleStartTime, err := time.Parse(time.RFC3339, idleStartTimeStr)
	if err != nil {
		g.log.Error(err, "parse idleStartTime failed", "appbox", appbox.Name, "idleStartTime", idleStartTime)
		return err
	}
	idleTime := time.Since(idleStartTime).Seconds()
	if idleTime > g.conf.IdleTimeThreshold.Duration.Seconds() {
		g.log.Info("appbox is idle for a long time, set phase to ReadyForDelete",
			"appbox", appbox.Name, "idleTime", idleTime, "threshold", g.conf.IdleTimeThreshold.Duration.Seconds())
		appbox.Status.Phase = v1alpha1.APPBoxPhaseReadyForDelete
		if err := g.appboxrw.UpdateStatus(ctx, appbox); err != nil {
			g.log.Error(err, "update appbox status failed", "appbox", appbox.Name)
			return err
		}
		appbox.Annotations[v1alpha1.AnnotationKeyReadyForDeleteStartTime] = time.Now().Format(time.RFC3339)
		return g.appboxrw.Update(ctx, appbox)
	}
	return nil
}

func (g *appBoxGCWorker) isIdleAPPBox(ctx context.Context, appbox *v1alpha1.APPBox) bool {
	approutes := &v1alpha1.APPRouteList{}
	opts := &client.ListOptions{
		FieldSelector: fields.OneTermEqualSelector(controllers.APPBoxNameField, appbox.Name),
		Namespace:     appbox.Namespace,
	}
	if err := g.approuter.List(ctx, approutes, opts); err != nil {
		g.log.Error(err, "list approutes failed", "appbox", appbox.Name)
		return false
	}

	return len(approutes.Items) == 0
}

func (g *appBoxGCWorker) cleanIdle(ctx context.Context) error {
	appboxs := &v1alpha1.APPBoxList{}
	if err := g.appboxrw.List(ctx, appboxs); err != nil {
		return err
	}

	for _, appbox := range appboxs.Items {
		appbox := appbox
		if appbox.Status.Phase != v1alpha1.APPBoxPhaseReadyForDelete {
			continue
		}
		if appbox.GetAnnotations()[v1alpha1.SkipGCAnnotationKey] == v1alpha1.SkipGCAnnotationVal {
			continue
		}
		if err := g.cleanIdleAPPBoxOne(ctx, &appbox); err != nil {
			g.log.Error(err, "clean idle appbox failed", "appbox", appbox.Name)
			continue
		}
	}

	return nil
}

func (g *appBoxGCWorker) cleanIdleAPPBoxOne(ctx context.Context, appbox *v1alpha1.APPBox) error {
	if !g.isIdleAPPBox(ctx, appbox) {
		g.log.Info("appbox is not idle, set phase to Active", "appbox", appbox.Name)
		appbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		if err := g.appboxrw.UpdateStatus(ctx, appbox); err != nil {
			g.log.Error(err, "update appbox status failed", "appbox", appbox.Name)
			return err
		}
		if appbox.GetAnnotations()[v1alpha1.AnnotationKeyReadyForDeleteStartTime] != "" {
			delete(appbox.Annotations, v1alpha1.AnnotationKeyReadyForDeleteStartTime)
			if err := g.appboxrw.Update(ctx, appbox); err != nil {
				g.log.Error(err, "update appbox failed", "appbox", appbox.Name)
				return err
			}
		}
		return nil
	}
	readyDeleteStartTimeStr := appbox.GetAnnotations()[v1alpha1.AnnotationKeyReadyForDeleteStartTime]
	if readyDeleteStartTimeStr == "" {
		if appbox.Annotations == nil {
			appbox.Annotations = make(map[string]string)
		}
		appbox.Annotations[v1alpha1.AnnotationKeyReadyForDeleteStartTime] = time.Now().Format(time.RFC3339)
		if err := g.appboxrw.Update(ctx, appbox); err != nil {
			g.log.Error(err, "update appbox failed", "appbox", appbox.Name)
			return err
		}
	}
	readyDeleteStartTime, err := time.Parse(time.RFC3339, readyDeleteStartTimeStr)
	if err != nil {
		g.log.Error(err, "parse readyDeleteStartTime failed", "appbox", appbox.Name, "readyDeleteStartTime", readyDeleteStartTimeStr)
		return err
	}
	readyDeleteTime := time.Since(readyDeleteStartTime)
	if readyDeleteTime > g.conf.IdleTimeRetention.Duration {
		g.log.Info("appbox is ready for delete for a long time, delete it", "appbox", appbox.Name, "readyDeleteTime", readyDeleteTime)
		if err := g.appboxrw.Delete(ctx, appbox); err != nil {
			g.log.Error(err, "delete appbox failed", "appbox", appbox.Name)
			return err
		}
	}
	return nil
}
