package s3clean

import (
	"context"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/providers/aws"
	"github.com/tidbcloud/shared-provider/pkg/manager"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/util/retry"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/source"
)

const (
	s3cleanFinalizer = "s3clean.tidbcloud.com/finalizer"
)

type Reconciler struct {
	InfraClient     client.Client
	InfraCache      cache.Cache
	AwsClient       aws.Client
	Logger          logr.Logger
	KubeClusterName string

	AWSCLIImage string
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("Start reconcile")

	startTime := time.Now()
	defer func() {
		logger.Info("Finish reconcile",
			"time", time.Since(startTime),
		)
	}()

	backup := cloudv1.Backup{}

	if err := r.InfraCache.Get(ctx, req.NamespacedName, &backup); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("backup has been deleted")
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}

	if backup.DeletionTimestamp == nil {
		if controllerutil.ContainsFinalizer(&backup, s3cleanFinalizer) {
			return ctrl.Result{}, nil
		}

		logger.Info("Add finalizer")

		// TODO(liubo02): change to use PATCH?
		if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
			if err := r.InfraClient.Get(ctx, client.ObjectKeyFromObject(&backup), &backup); err != nil {
				return err
			}
			controllerutil.AddFinalizer(&backup, s3cleanFinalizer)
			return r.InfraClient.Update(ctx, &backup)
		}); err != nil {
			return ctrl.Result{}, fmt.Errorf("add finalizer for backup %s failed: %w", client.ObjectKeyFromObject(&backup), err)
		}

		return ctrl.Result{}, nil
	}

	if err := r.Clean(ctx, &backup); err != nil {
		return ctrl.Result{}, err
	}

	if !controllerutil.ContainsFinalizer(&backup, s3cleanFinalizer) {
		return ctrl.Result{}, nil
	}

	logger.Info("Remove finalizer")
	// TODO(liubo02): change to use PATCH?
	if err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		if err := r.InfraClient.Get(ctx, client.ObjectKeyFromObject(&backup), &backup); err != nil {
			return err
		}
		controllerutil.RemoveFinalizer(&backup, s3cleanFinalizer)
		return r.InfraClient.Update(ctx, &backup)
	}); err != nil {
		return ctrl.Result{}, fmt.Errorf(
			"remove finalizer for backup %s failed: %w",
			client.ObjectKeyFromObject(&backup),
			err,
		)
	}

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr manager.Manager) error {
	r.Logger = mgr.GetLogger()
	r.InfraClient = mgr.InfraCluster().GetClient()
	r.InfraCache = mgr.InfraCluster().GetCache()

	ptp := utils.GetPredicateForProviderTypes(types.ProviderTypeGCPSharedTier, types.ProviderTypeAWSSharedTier)
	p, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
		MatchLabels: map[string]string{
			labels.LabelKeyKubernetesCluster: r.KubeClusterName,
		},
	})
	if err != nil {
		return fmt.Errorf("invalid label selector for backup: %w", err)
	}

	c, err := controller.New("S3 Clean", mgr, controller.Options{
		Reconciler: r,
	})
	if err != nil {
		return fmt.Errorf("can't new a controller: %w", err)
	}

	r.Logger = c.GetLogger()

	if err := c.Watch(
		source.NewKindWithCache(&cloudv1.Backup{}, mgr.InfraCluster().GetCache()),
		&handler.EnqueueRequestForObject{},
		ptp,
		p,
	); err != nil {
		return fmt.Errorf("can't watch backup: %w", err)
	}

	return nil
}
