/*
Copyright 2021 The KubeVela Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package applicationrollout

import (
	"context"

	apiequality "k8s.io/apimachinery/pkg/api/equality"
	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
	ktypes "k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/klog/v2"

	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
	"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
	"github.com/oam-dev/kubevela/pkg/webhook/common/rollout"
)

// ValidateCreate validates the AppRollout on creation
func (h *ValidatingHandler) ValidateCreate(appRollout *v1beta1.AppRollout) field.ErrorList {
	klog.InfoS("validate create", "name", appRollout.Name)
	allErrs := apimachineryvalidation.ValidateObjectMeta(&appRollout.ObjectMeta, true,
		apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))

	fldPath := field.NewPath("spec")
	target := appRollout.Spec.TargetAppRevisionName
	if len(target) == 0 {
		allErrs = append(allErrs, field.Required(fldPath.Child("targetApplicationName"),
			"target application name cannot be empty"))
		// can't continue without target
		return allErrs
	}

	if appRollout.DeletionTimestamp.IsZero() {
		var targetAppRevision v1beta1.ApplicationRevision
		sourceAppRevision := &v1beta1.ApplicationRevision{}
		targetAppName := appRollout.Spec.TargetAppRevisionName
		if err := h.Get(context.Background(), ktypes.NamespacedName{Namespace: appRollout.Namespace, Name: targetAppName},
			&targetAppRevision); err != nil {
			klog.ErrorS(err, "cannot locate target application revision", "target application revision",
				klog.KRef(appRollout.Namespace, targetAppName))
			allErrs = append(allErrs, field.NotFound(fldPath.Child("targetAppRevisionName"), targetAppName))
			// can't continue without target
			return allErrs
		}
		sourceAppName := appRollout.Spec.SourceAppRevisionName
		if sourceAppName != "" {
			if err := h.Get(context.Background(), ktypes.NamespacedName{Namespace: appRollout.Namespace, Name: sourceAppName},
				sourceAppRevision); err != nil {
				klog.ErrorS(err, "cannot locate source application revision", "source application revision",
					klog.KRef(appRollout.Namespace, sourceAppName))
				allErrs = append(allErrs, field.NotFound(fldPath.Child("sourceAppRevisionName"), sourceAppName))
			}
		} else {
			// nolint
			sourceAppRevision = nil
		}
		/*

			    // TODO(wonderflow): refactor it and add component validate using appRevision
				// 1. there can only be one component or less
				// 2. if there are no components, make sure the applications has only one common component so that's the default
				// 3. it is contained in both source and target application
				// 4. the common component has the same type

		*/
	}

	// validate the rollout plan spec
	allErrs = append(allErrs, rollout.ValidateCreate(h, &appRollout.Spec.RolloutPlan, fldPath.Child("rolloutPlan"))...)
	return allErrs
}

// ValidateUpdate validates the AppRollout on update
func (h *ValidatingHandler) ValidateUpdate(new, old *v1beta1.AppRollout) field.ErrorList {
	klog.InfoS("validate update", "name", new.Name)
	errList := h.ValidateCreate(new)
	fldPath := field.NewPath("spec").Child("rolloutPlan")

	if len(errList) > 0 {
		return errList
	}
	// we can only reuse the rollout after reaching terminating state if the target and source has changed
	if old.Status.RollingState == v1alpha1.RolloutSucceedState ||
		old.Status.RollingState == v1alpha1.RolloutFailedState {
		if old.Spec.SourceAppRevisionName == new.Spec.SourceAppRevisionName &&
			old.Spec.TargetAppRevisionName == new.Spec.TargetAppRevisionName {
			if !apiequality.Semantic.DeepEqual(&old.Spec.RolloutPlan, &new.Spec.RolloutPlan) {
				// here we allow user restart a scale rollout by modifying rollout targetSize
				if old.Spec.RolloutPlan.TargetSize != nil && new.Spec.RolloutPlan.TargetSize != nil &&
					*old.Spec.RolloutPlan.TargetSize != *new.Spec.RolloutPlan.TargetSize {
					return errList
				}
				errList = append(errList, field.Invalid(fldPath, new.Spec,
					"a successful or failed rollout cannot be modified without changing the target or the source"))
				return errList
			}
		}
	}

	return rollout.ValidateUpdate(h, &new.Spec.RolloutPlan, &old.Spec.RolloutPlan, fldPath)
}
