package tidbpod

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/aws-shared-provider/pkg/manager"
	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/labels"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"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/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
)

const (
	delayCleanTime = 2 * time.Second

	MaxConcurrentCleanReconciles = 3
)

// CleanReconciler reconciles a Tidb Pod object.
type CleanReconciler struct {
	Client    client.Client
	TidbPools []string
}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *CleanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.V(2).Info("start reconcile tidb pod for clean")
	start := time.Now()
	defer func() {
		logger.V(2).Info("finish reconcile tidb pod for clean", "time", time.Since(start))
	}()

	pod := &corev1.Pod{}
	if err := r.Client.Get(ctx, req.NamespacedName, pod); err != nil {
		if apierrors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}

	if pod.DeletionTimestamp != nil {
		if time.Since(pod.DeletionTimestamp.Time) > delayCleanTime {
			return ctrl.Result{}, r.tryRemoveFinalizer(ctx, pod)
		} else {
			return ctrl.Result{RequeueAfter: delayCleanTime}, nil
		}
	}

	if pod.Status.Phase == corev1.PodFailed && len(pod.Status.ContainerStatuses) == 0 {
		logger.Info("tidb pod failed with no container, kill pod")
		return ctrl.Result{}, client.IgnoreNotFound(
			r.Client.Delete(ctx, pod, &client.DeleteOptions{GracePeriodSeconds: ptr.To[int64](0)}))
	}

	for i := range pod.Status.ContainerStatuses {
		if cstatus := pod.Status.ContainerStatuses[i]; cstatus.State.Terminated != nil && !isE2ETest(pod) {
			logger.Info("[pod lifecycle] container terminated, kill pod", "name", cstatus.Name, "terminated", cstatus.State.Terminated, "pod_name", pod.Name)
			return ctrl.Result{}, client.IgnoreNotFound(
				r.Client.Delete(ctx, pod, &client.DeleteOptions{GracePeriodSeconds: ptr.To[int64](0)}))
		}
	}

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *CleanReconciler) SetupWithManager(mgr manager.Manager) error {
	byNamespaceFilter := func(object client.Object) bool {
		for _, pool := range r.TidbPools {
			if object.GetNamespace() == pool {
				return true
			}
		}
		return false
	}
	byLabelSelector := metav1.LabelSelector{
		MatchLabels: map[string]string{
			labels.NameLabelKey:      labels.TidbPodNameLabelValue,
			labels.ComponentLabelKey: labels.TidbPodComponentLabelValue,
		},
	}
	byNamespaceFuncs := predicate.NewPredicateFuncs(byNamespaceFilter)
	byTidbPod, err := predicate.LabelSelectorPredicate(byLabelSelector)
	if err != nil {
		return fmt.Errorf("invalid label selector: %w", err)
	}

	return ctrl.NewControllerManagedBy(mgr).Named("Tidb-Pod-Clean").For(&corev1.Pod{}).
		WithEventFilter(byNamespaceFuncs).
		WithEventFilter(byTidbPod).
		WithOptions(controller.Options{MaxConcurrentReconciles: MaxConcurrentCleanReconciles}).
		Complete(r)
}

func (r *CleanReconciler) tryRemoveFinalizer(ctx context.Context, obj client.Object) error {
	if controllerutil.ContainsFinalizer(obj, types.TidbPoolFinalizer) {
		logger := log.FromContext(ctx)
		logger.Info("Remove tidb pod finalizer")

		copyObj := obj.DeepCopyObject().(client.Object)
		controllerutil.RemoveFinalizer(copyObj, types.TidbPoolFinalizer)

		if err := r.Client.Update(ctx, copyObj); err != nil {
			return fmt.Errorf("remove finalizer for tidb pod failed: %w", err)
		}
	}

	return nil
}

func isE2ETest(pod *corev1.Pod) bool {
	for i := range pod.Spec.Containers {
		if pod.Spec.Containers[i].Name == "tidb" && strings.Contains(pod.Spec.Containers[i].Image, "e2e-invalid") {
			return true
		}
	}

	return false
}
