package v1

import (
	"fmt"
	"regexp"

	apiequality "k8s.io/apimachinery/pkg/api/equality"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/validation/field"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var workflowlog = logf.Log.WithName("workflow")

func (wf *Workflow) setupWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr).
		For(wf).
		Complete()
}

//+kubebuilder:webhook:path=/mutate-cloud-pingcap-com-v1-workflow,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=workflows,verbs=create,versions=v1,name=mworkflow.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &Workflow{}

const (
	WorkflowFinalizer = "workflow.cloud.pingcap.com/finalizer"
)

var (
	stepNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-_ ]*[a-zA-Z0-9]+$`)
)

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (wf *Workflow) Default() {
	workflowlog.Info("default", "name", wf.Name)

	if wf.Labels == nil {
		wf.Labels = make(map[string]string)
	}
	wf.Labels[WorkflowTypeTagKey] = string(wf.Spec.Type)
	controllerutil.AddFinalizer(wf, WorkflowFinalizer)

	if wf.Spec.RetryPolicy == nil {
		wf.Spec.RetryPolicy = &WorkflowRetryPolicy{
			Type: RetryPolicyTypeAlways,
		}
	}
	if wf.Spec.RetryPolicy.Type == "" {
		wf.Spec.RetryPolicy.Type = RetryPolicyTypeAlways
	}
	if wf.Spec.RetryPolicy.Backoff == nil && wf.Spec.RetryPolicy.Type != RetryPolicyTypeNever {
		wf.Spec.RetryPolicy.Backoff = &metav1.Duration{Duration: WorkflowDefaultBackoff}
	}

	if wf.Spec.TTL == nil {
		wf.Spec.TTL = &metav1.Duration{Duration: WorkflowDefaultTTL}
	}
}

//+kubebuilder:webhook:path=/validate-cloud-pingcap-com-v1-workflow,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=workflows,verbs=create;update,versions=v1,name=vworkflow.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &Workflow{}

func (wf *Workflow) validateSpec() (errs field.ErrorList) {
	if err := wf.validateSteps(); err != nil {
		errs = append(errs, err...)
	}
	return
}

// validateSteps checks if the steps are valid.
func (wf *Workflow) validateSteps() (errs field.ErrorList) {
	fld := field.NewPath("spec").Child("steps")

	if len(wf.Spec.Steps) == 0 {
		errs = append(errs, field.Invalid(fld, wf.Spec.Steps, "empty steps"))
		return
	}

	stepNames := map[string]bool{}
	for idx, step := range wf.Spec.Steps {
		if _, ok := stepNames[step.Name]; ok {
			errs = append(errs, field.Invalid(fld, step, fmt.Sprintf("duplicated step name, idx=%d,name=%s", idx, step.Name)))
			continue
		}
		if !stepNameRegex.MatchString(step.Name) {
			errs = append(errs, field.Invalid(fld, step, fmt.Sprintf("invalid step name, idx=%d,name=%s", idx, step.Name)))
			continue
		}
		stepNames[step.Name] = true

		// NOTE:  Webhook-server is an independent deployed server and all Actions are registered in cluster-controller,
		// so that we can't check whether Action used by step exist here.
	}

	return
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (wf *Workflow) ValidateCreate() (admission.Warnings, error) {
	workflowlog.Info("validate create", "name", wf.Name, "namespace", wf.Namespace)
	return nil, invalidOrNil(wf.validateSpec(), wf)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (wf *Workflow) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
	workflowlog.Info("validate update", "name", wf.Name, "namespace", wf.Namespace)
	oldWF, _ := old.(*Workflow)
	var allErrs field.ErrorList

	switch wf.Status.Phase {
	// Allow to update spec when it does not begin to run.
	case WorkflowPhaseNone, WorkflowPhasePending:
		if err := wf.validateSpec(); err != nil {
			allErrs = append(allErrs, err...)
		}

	// Once it begins to run, or it's finished, some fields are immutable.
	case WorkflowPhaseRunning, WorkflowPhaseCompleted, WorkflowPhaseFailed, WorkflowPhaseCancelling, WorkflowPhaseCancelled:
		// TTL, Paused, RetryPolicy are mutable

		if wf.Spec.Type != oldWF.Spec.Type {
			err := fmt.Errorf("type is immutable: %s", wf.Spec.Type)
			workflowlog.Error(err, "try to update type", "old", oldWF.Spec.Type, "new", wf.Spec.Type)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("type"), wf.Spec.Type, err.Error()))
		}

		// Compare schedule using time values instead of pointer comparison
		scheduleChanged := false
		if wf.Spec.Schedule != nil && oldWF.Spec.Schedule != nil {
			scheduleChanged = !wf.Spec.Schedule.Time.Equal(oldWF.Spec.Schedule.Time)
		} else if wf.Spec.Schedule != oldWF.Spec.Schedule {
			scheduleChanged = true
		}
		if scheduleChanged {
			err := fmt.Errorf("schedule is immutable: %s", wf.Spec.Schedule)
			workflowlog.Info("try to update schedule", "old", oldWF.Spec.Schedule, "new", wf.Spec.Schedule)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("schedule"), wf.Spec.Schedule, err.Error()))
		}

		if !wf.Spec.Steps.Equals(oldWF.Spec.Steps) {
			err := fmt.Errorf("steps are immutable: %v", wf.Spec.Steps)
			workflowlog.Info("try to update steps", "old", oldWF.Spec.Steps, "new", wf.Spec.Steps)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("steps"), wf.Spec.Steps, err.Error()))
		}

		if !apiequality.Semantic.DeepEqual(wf.Spec.Inputs, oldWF.Spec.Inputs) {
			err := fmt.Errorf("inputs are immutable: %v", wf.Spec.Inputs)
			workflowlog.Info("try to update inputs", "old", oldWF.Spec.Inputs, "new", wf.Spec.Inputs)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("inputs"), wf.Spec.Inputs, err.Error()))
		}
	}

	return nil, invalidOrNil(allErrs, wf)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (wf *Workflow) ValidateDelete() (admission.Warnings, error) {
	workflowlog.Info("validate delete", "name", wf.Name)
	return nil, nil
}
