package reconciler

import (
	"context"
	"time"

	"github.com/tidbcloud/observability/pkg/common/constants"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

const (
	reconcileGracePeriod = 30 * time.Second
	reconcileTimeout     = 1 * time.Minute

	defaultpollInterval = 5 * time.Second
)

type ExternalObjConnecter interface {
	Connect(ctx context.Context, obj client.Object) (ExternalObjClient, error)
}

type ExternalObjClient interface {
	Observe(ctx context.Context, obj client.Object) (ExternalObservation, error)

	Create(ctx context.Context, obj client.Object) error

	Update(ctx context.Context, obj client.Object) error

	Delete(ctx context.Context, obj client.Object) (ExternalDeletion, error)
}

type SubResourceObservation struct {
	ResourceExists     bool
	PartResourceExists bool
}

func Combine(subs ...SubResourceObservation) SubResourceObservation {
	finalResourceExists := true
	finalPartResourceExists := false

	for _, sub := range subs {
		finalResourceExists = finalResourceExists && sub.ResourceExists
		finalPartResourceExists = finalPartResourceExists || sub.PartResourceExists
	}
	return SubResourceObservation{
		finalResourceExists, finalPartResourceExists,
	}
}

type ExternalObservation struct {
	ResourceExists     bool
	PartResourceExists bool
	ResourceUpToDate   bool
	Droped             bool
	PollInterval       time.Duration
}

type ExternalDeletion struct {
	ResourceDeleted bool
	PollInterval    time.Duration
}

type Reconciler struct {
	NewResource func() client.Object
	Client      client.Client
	ExternalObj ExternalObjConnecter
}

// Reconcile defined a paradigm for how to handle resource states
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request, finalizer string) (reconcile.Result, error) {
	log := log.FromContext(ctx)

	log.Info("start reconcile")
	externalCtx, cancel := context.WithTimeout(ctx, reconcileTimeout)
	defer cancel()

	ctx, cancel = context.WithTimeout(ctx, reconcileTimeout+reconcileGracePeriod)
	defer cancel()

	obj := r.NewResource()
	err := r.Client.Get(ctx, req.NamespacedName, obj)
	if err != nil {
		if client.IgnoreNotFound(err) != nil {
			log.Error(err, "get resource failed")
			return reconcile.Result{}, err
		}
		return reconcile.Result{}, nil
	}

	if obj.GetAnnotations()[constants.AnnotationKeyPauseReconcile] == "true" && obj.GetDeletionTimestamp().IsZero() {
		return reconcile.Result{}, nil
	}

	externalCli, err := r.ExternalObj.Connect(ctx, obj)
	if err != nil {
		log.Error(err, "get external client failed")
		return reconcile.Result{Requeue: true}, err
	}

	observation, err := externalCli.Observe(externalCtx, obj)
	if err != nil {
		if observation.Droped {
			log.Error(err, "Observe resource failed and drop this event")
			return reconcile.Result{Requeue: false}, nil
		}
		log.Error(err, "Observe resource failed")
		return reconcile.Result{Requeue: true}, err
	}

	if !obj.GetDeletionTimestamp().IsZero() {
		if observation.ResourceExists || observation.PartResourceExists {
			externalDeletion, err := externalCli.Delete(externalCtx, obj)
			if err != nil {
				log.Error(err, "Delete resoure failed")
				return reconcile.Result{Requeue: true}, err
			}
			if !externalDeletion.ResourceDeleted {
				log.Info("waiting for external resource deleted")
				if externalDeletion.PollInterval > 0 {
					return reconcile.Result{RequeueAfter: externalDeletion.PollInterval}, nil
				}
				return reconcile.Result{Requeue: true}, nil
			}
		}
		controllerutil.RemoveFinalizer(obj, finalizer)
		if err := r.Client.Update(ctx, obj); err != nil {
			log.Error(err, "Remove Finalizer failed")
			return reconcile.Result{Requeue: true}, err
		}

		log.Info("Successfully deleted resource")
		return reconcile.Result{Requeue: false}, nil
	}

	if !controllerutil.ContainsFinalizer(obj, finalizer) {
		controllerutil.AddFinalizer(obj, finalizer)
		if err := r.Client.Update(ctx, obj); err != nil {
			log.Error(err, "Add Finalizer failed")
			return reconcile.Result{Requeue: true}, err
		}
	}
	//wait before do any resource except delete
	if observation.PollInterval > 0 {
		return reconcile.Result{RequeueAfter: observation.PollInterval}, nil
	}
	if !observation.ResourceExists {
		err = externalCli.Create(externalCtx, obj)
		if err != nil {
			log.Error(err, "create resource failed")
			return reconcile.Result{Requeue: true}, err
		}

		return reconcile.Result{Requeue: true}, nil
	}

	if observation.ResourceUpToDate {
		log.Info("resource update to date")
		return reconcile.Result{Requeue: false}, nil
	}

	err = externalCli.Update(externalCtx, obj)
	if err != nil {
		log.Error(err, "update resource failed")
		return reconcile.Result{Requeue: true}, err
	}

	return reconcile.Result{RequeueAfter: defaultpollInterval}, nil
}
