package step

import (
	"context"

	"github.com/RafayLabs/rcloud-base/pkg/controller/apply"
	cr "github.com/RafayLabs/rcloud-base/pkg/controller/runtime"
	"github.com/RafayLabs/rcloud-base/pkg/controller/scheme"
	"github.com/RafayLabs/rcloud-base/pkg/controller/util"
	hash "github.com/RafayLabs/rcloud-base/pkg/hasher"
	clusterv2 "github.com/RafayLabs/rcloud-base/proto/types/controller"
	corev1 "k8s.io/api/core/v1"
	apierrs "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	ref "k8s.io/client-go/tools/reference"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

var (
	stepLog = logf.Log.WithName("cluster-v2-step")
	ma      = meta.NewAccessor()
)

// Handler is the interface for working with steps
type Handler interface {
	// Handle executes the step
	Handle(ctx context.Context, owner metav1.Object, step clusterv2.StepTemplate) (status clusterv2.StepStatus)
	// Status returns new status for current status
	HandleStatus(ctx context.Context, owner metav1.Object, original clusterv2.StepStatus) (current clusterv2.StepStatus)
	// Delete deletes a given step
	HandleDelete(ctx context.Context, owner metav1.Object, stage clusterv2.StageSpec) error
}

type handler struct {
	apply.Applier
}

// NewHandler returns object handler that handles step object
func NewHandler(a apply.Applier) Handler {
	return &handler{a}
}

var _ Handler = (*handler)(nil)

func (h *handler) getObject(ctx context.Context, ref *corev1.ObjectReference) (o client.Object, err error) {
	gvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)
	nn := types.NamespacedName{
		Namespace: ref.Namespace,
		Name:      ref.Name,
	}
	if o, err = util.NewObject(gvk); err != nil {
		return
	}

	err = h.Get(ctx, nn, o)
	return
}

func (h *handler) getObjectState(ctx context.Context, original clusterv2.StepStatus) (state clusterv2.StepObjectState, reason string, err error) {
	if original.ObjectRef == nil {
		state = clusterv2.StepObjectState(original.ObjectState)
		reason = original.ObjectReason
		return
	}

	var o runtime.Object
	o, err = h.getObject(ctx, original.ObjectRef)
	if err != nil {
		return
	}

	state, reason = ObjectState(o)

	return
}

func (h *handler) getJobState(ctx context.Context, ref *corev1.ObjectReference) (state clusterv2.StepJobState, reason string, err error) {
	if ref == nil {
		state = clusterv2.StepJobComplete
		reason = "not configured"
		return
	}

	var o runtime.Object
	o, err = h.getObject(ctx, ref)
	if err != nil {
		return
	}

	state, reason = JobState(o)

	return
}

func (h *handler) HandleStatus(ctx context.Context, owner metav1.Object, original clusterv2.StepStatus) (current clusterv2.StepStatus) {
	log := stepLog.WithValues("owner", types.NamespacedName{
		Namespace: owner.GetNamespace(),
		Name:      owner.GetName(),
	}, "step", original.Name)

	jobRef := original.JobRef

	var objectState clusterv2.StepObjectState
	var objectReason string
	var jobState clusterv2.StepJobState
	var jobReason string
	var err error

	current = *original.DeepCopy()

	if objectState, objectReason, err = h.getObjectState(ctx, original); err != nil {
		log.Info("unable to get object state", "error", err)
		return original
	}

	if jobState, jobReason, err = h.getJobState(ctx, jobRef); err != nil {
		log.Info("unable to get job state", "error", err)
		return original
	}

	log.Info("step", "objectState", objectState, "objectReason", objectReason, "jobState", jobState, "jobReason", jobReason)

	current.ObjectState = string(objectState)
	current.ObjectReason = objectReason
	current.JobState = string(jobState)
	current.JobReason = jobReason

	if objectState == clusterv2.StepObjectComplete && jobState == clusterv2.StepJobComplete {
		current.State = string(clusterv2.StepComplete)
		current.Reason = "complete"
		return
	}

	if objectState == clusterv2.StepObjectFailed {
		current.State = string(clusterv2.StepFailed)
		current.Reason = objectReason
		return
	}

	if jobState == clusterv2.StepJobFailed {
		current.State = string(clusterv2.StepFailed)
		current.Reason = jobReason
		return
	}

	return
}

func (h *handler) deleteStep(ctx context.Context, owner metav1.Object, step clusterv2.StepTemplate) error {
	log := stepLog.WithValues("owner", types.NamespacedName{
		Namespace: owner.GetNamespace(),
		Name:      owner.GetName(),
	}, "step", step.Name)

	log.Info("deleting step", "name", step.Name)

	if step.Object != nil {
		log.Info("step object configured for step", "gvk", step.Object.TypeMeta.GroupVersionKind())
		o, _, err := cr.ToUnstructuredObject(step.Object)
		if err != nil {
			return err
		}
		var objectKey client.ObjectKey
		var existing client.Object

		objectKey = client.ObjectKey{
			Name:      o.GetName(),
			Namespace: o.GetNamespace(),
		}

		existing, err = util.NewObject(o.GetObjectKind().GroupVersionKind())

		if err != nil {
			return err
		}

		err = h.Get(ctx, objectKey, existing)
		if err != nil {
			if apierrs.IsNotFound(err) {
				return nil
			}
			return err
		}

		if eo, ok := existing.(metav1.Object); ok {
			if !util.OwnsObject(owner, eo) {
				return nil
			}
			log.Info("deleting object", "name", step.Name)
			err = h.Delete(ctx, existing, client.PropagationPolicy(metav1.DeletePropagationBackground))
			if err != nil {
				return err
			}

		}

	}

	if step.JobTemplate != nil {
		log.Info("step job configured for step")
	}

	return nil
}

func (h *handler) HandleDelete(ctx context.Context, owner metav1.Object, stage clusterv2.StageSpec) error {
	log := stepLog.WithValues("owner", types.NamespacedName{
		Namespace: owner.GetNamespace(),
		Name:      owner.GetName(),
	})

	for _, step := range stage {
		log.Info("deleting step", "name", step.Name)
		err := h.deleteStep(ctx, owner, step)
		if err != nil {
			log.Info("unable to delete step", "name", step.Name, "error", err)
			return err
		}

	}

	return nil
}

func (h *handler) Handle(ctx context.Context, owner metav1.Object, step clusterv2.StepTemplate) (status clusterv2.StepStatus) {
	log := stepLog.WithValues("owner", types.NamespacedName{
		Namespace: owner.GetNamespace(),
		Name:      owner.GetName(),
	}, "step", step.Name)

	status = clusterv2.StepStatus{
		Name: step.Name,
	}
	// handle step object
	if step.Object != nil {
		accessor, err := step.Object.Accessor()
		if err != nil {
			log.Error(err, "unable to get step object accessor")
			status.ObjectState = string(clusterv2.StepObjectFailed)
			status.ObjectReason = err.Error()
			status.State = string(clusterv2.StepFailed)
			status.Reason = err.Error()
			return
		}
		gvk, err := accessor.GroupVersionKind()
		if err != nil {
			log.Error(err, "unable to get step object gvk")
			status.ObjectState = string(clusterv2.StepObjectFailed)
			status.ObjectReason = err.Error()
			status.State = string(clusterv2.StepFailed)
			status.Reason = err.Error()
			return
		}
		log.Info("step object configured for step", "gvk", gvk)
		o, _, err := cr.ToUnstructuredObject(step.Object)
		if err != nil {
			log.Error(err, "unable to convert step object")
			status.ObjectState = string(clusterv2.StepObjectFailed)
			status.ObjectReason = err.Error()
			status.State = string(clusterv2.StepFailed)
			status.Reason = err.Error()
			return
		}

		err = util.SetOwnerReference(owner, o)
		if err != nil {
			log.Error(err, "unable to set controller reference")
			status.ObjectState = string(clusterv2.StepObjectFailed)
			status.ObjectReason = err.Error()
			status.State = string(clusterv2.StepFailed)
			status.Reason = err.Error()
			return
		}

		err = h.Apply(ctx, o)
		if err != nil {
			log.Error(err, "unable to patch step object", "name", step.Name)
			status.ObjectState = string(clusterv2.StepObjectFailed)
			status.ObjectReason = err.Error()
			status.State = string(clusterv2.StepFailed)
			status.Reason = err.Error()
			return
		}

		objRef, _ := ref.GetReference(scheme.Scheme, o)
		status.ObjectState = string(clusterv2.StepObjectCreated)
		status.ObjectReason = "created"
		status.ObjectRef = objRef
		hash, _ := hash.GetHash(o)
		status.ObjectSpecHash = hash
	}

	// handle step job
	if step.JobTemplate != nil {
		log.Info("step job configured for step")
	}

	status.State = string(clusterv2.StepExecuted)

	return

}
