package reconciler

import (
	"context"
	"time"

	"github.com/google/uuid"
	"github.com/pingcap/errors"
	kerrors "k8s.io/apimachinery/pkg/api/errors"
	"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"

	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
)

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

	defaultpollInterval = 15 * time.Second
)

const (
	errGetResource = "cannot get managed resource"
)

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 ExternalObservation struct {
	ResourceExists   bool
	ResourceUpToDate bool
	PollInterval     time.Duration
}

type ExternalDeletion struct {
	ResourceDeleted bool
}

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

// TODO: the resource need to set Conditions to indicate the status and its reconciliation process
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request, finalizer string) (reconcile.Result, error) {
	logger := log.FromContext(ctx).WithValues("resource", req.NamespacedName, "reconcile-id", uuid.New().String())
	ctx = log.IntoContext(ctx, logger)
	log := logger

	timeout := reconcileTimeout + reconcileGracePeriod
	externalTimeout := reconcileTimeout
	if r.ReconcileTimeout > 0 {
		timeout = r.ReconcileTimeout + reconcileGracePeriod
		externalTimeout = r.ReconcileTimeout
	}

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

	ctx, cancel = context.WithTimeout(ctx, timeout)
	defer cancel()

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

	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 {
		log.Error(err, "Observe resource failed")
		return reconcile.Result{Requeue: true}, err
	}

	if !obj.GetDeletionTimestamp().IsZero() {
		externalDeletion, err := externalCli.Delete(externalCtx, obj)
		if err != nil {
			log.Error(err, "Delete resoure failed")
			return common.ReconcileErr(log, err)
		}

		if !externalDeletion.ResourceDeleted {
			log.Info("waiting for external resource deleted")
			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
		}
	}

	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 {
		if observation.PollInterval > 0 {
			return reconcile.Result{RequeueAfter: observation.PollInterval}, nil
		}
		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

}

// An ErrorIs function returns true if an error satisfies a particular condition.
type ErrorIs func(err error) bool

// Ignore any errors that satisfy the supplied ErrorIs function by returning
// nil. Errors that do not satisfy the suppled function are returned unmodified.
func Ignore(is ErrorIs, err error) error {
	if is(err) {
		return nil
	}
	return err
}

func IgnoreNotFound(err error) error {
	return Ignore(kerrors.IsNotFound, err)
}
