package v1

import (
	"context"
	"fmt"

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

var configChangeLogger = logf.Log.WithName("configchange-resource")
var _ admission.Validator = &ConfigChange{}
var _ admission.Defaulter = &ConfigChange{}

// +kubebuilder:webhook:path=/mutate-cloud-pingcap-com-v1-configchange,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=configchanges,verbs=create;update,versions=v1,name=mconfigchange.kb.io,admissionReviewVersions={v1,v1beta1}
// +kubebuilder:webhook:path=/validate-cloud-pingcap-com-v1-configchange,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=configchanges,verbs=create;update,versions=v1,name=vconfigchange.kb.io,admissionReviewVersions={v1,v1beta1}

func SelectAllConfigChanges4Cluster(clusterName string) labels.Selector {
	return labels.SelectorFromSet(map[string]string{
		ClusterTagKey: clusterName,
	})
}

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

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (cc *ConfigChange) Default() {
	configChangeLogger.Info("default", "name", cc.Name)
	if cc.Labels == nil {
		cc.Labels = map[string]string{}
	}

	var cluster Cluster
	err := webhookClient.Get(context.TODO(), client.ObjectKey{Namespace: cc.Namespace, Name: cc.Spec.ClusterName}, &cluster)
	if err != nil {
		configChangeLogger.Error(err, "get cluster failed, forward to validation")
		return
	}
	cc.Labels[TenantTagKey] = cluster.Labels[TenantTagKey]
	cc.Labels[ClusterTagKey] = cluster.Name
	cc.Labels[RegionLabel] = cluster.GetRegion()
	cc.Labels[ProviderTypeLabel] = cluster.GetProviderType()
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (cc *ConfigChange) ValidateCreate() (warnings admission.Warnings, err error) {
	logger := configChangeLogger.WithValues("name", cc.Name)
	logger.Info("validate create")
	ctx := context.Background()
	var allErrs field.ErrorList
	if errs := cc.validateSpec(ctx); errs != nil {
		allErrs = append(allErrs, errs...)
	}

	logger.Info("validate create result", "succ", len(allErrs) == 0)
	return nil, invalidOrNil(allErrs, cc)
}

func (cc *ConfigChange) validateSpec(ctx context.Context) field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")
	var cluster Cluster
	err := webhookClient.Get(context.TODO(), client.ObjectKey{Namespace: cc.Namespace, Name: cc.Spec.ClusterName}, &cluster)
	if err != nil {
		allErrs = append(allErrs, field.Invalid(fld.Child("clusterName"), cc.Spec.ClusterName,
			fmt.Sprintf("Cluster not found, err: %s", err.Error())))
		return allErrs
	}

	var targetConfigRevision ConfigRevision
	err = webhookClient.Get(ctx, client.ObjectKey{Name: cc.Spec.ConfigRevisionName}, &targetConfigRevision)
	if err != nil {
		allErrs = append(allErrs, field.Invalid(fld.Child("configRevisionName"), cc.Spec.ConfigRevisionName,
			fmt.Sprintf("ConfigRevision not found, err: %s", err.Error())))
		return allErrs
	}
	if targetConfigRevision.Labels[ClusterTagKey] != cluster.Name {
		allErrs = append(allErrs, field.Invalid(fld.Child("configRevisionName"), cc.Spec.ConfigRevisionName,
			fmt.Sprintf("CR(%s) is not for Cluster(%s)", targetConfigRevision.Name, cluster.Name)))
	}
	var curConfigRevision ConfigRevision
	err = webhookClient.Get(ctx, client.ObjectKey{Name: cluster.Spec.ConfigRevisionRef.Name}, &curConfigRevision)
	if err != nil {
		allErrs = append(allErrs, field.InternalError(fld.Child("clusterName"),
			fmt.Errorf("ConfigRevision(%s) not found, err: %s", cluster.Spec.ConfigRevisionRef.Name, err.Error())))
		return allErrs
	}

	if errs := cc.validateStrategy(&curConfigRevision, &targetConfigRevision); errs != nil {
		allErrs = append(allErrs, errs...)
	}
	if errs := cc.ensureTargetIsLatest(ctx, &targetConfigRevision); errs != nil {
		allErrs = append(allErrs, errs...)
	}
	if errs := cc.ensureClusterIsNotUnderConfigChanging(ctx, &cluster); errs != nil {
		allErrs = append(allErrs, errs...)
	}

	return allErrs
}

func (cc *ConfigChange) ensureTargetIsLatest(ctx context.Context, target *ConfigRevision) field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")

	crs := &ConfigRevisionList{}
	err := webhookClient.List(ctx, crs, &client.ListOptions{
		LabelSelector: SelectAllConfigRevisions4Cluster(cc.Spec.ClusterName),
	})
	if err != nil {
		allErrs = append(allErrs, field.InternalError(fld.Child("configRevisionName"),
			fmt.Errorf("list ConfigRevision: %s", err)))
		return allErrs
	}

	latest := LatestCreatedConfigRevision(crs.Items)
	if target.Name != latest.Name {
		allErrs = append(allErrs, field.Invalid(fld.Child("configRevisionName"), cc.Spec.ConfigRevisionName,
			fmt.Sprintf("target ConfigRevision(%s) is NOT latest(%s)", cc.Spec.ConfigRevisionName, latest.Name)))
		return allErrs
	}
	return nil
}

func (cc *ConfigChange) ensureClusterIsNotUnderConfigChanging(ctx context.Context, cluster *Cluster) field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")

	ccList := &ConfigChangeList{}
	err := webhookClient.List(ctx, ccList, &client.ListOptions{
		LabelSelector: SelectAllConfigChanges4Cluster(cc.Spec.ClusterName),
	})
	if err != nil {
		allErrs = append(allErrs, field.InternalError(fld.Child("configRevisionName"),
			fmt.Errorf("list ConfigChange: %s", err)))
		return allErrs
	}
	for _, each := range ccList.Items {
		if !each.Done() {
			configChangeLogger.Info("cluster is under config changing", "ns", each.Namespace, "configChangeName", each.Name, "phase", each.Status.Phase)
			allErrs = append(allErrs, field.Invalid(fld.Child("clusterName"), cc.Spec.ClusterName,
				fmt.Sprintf("cluster(%s) is under config changing(%s)", cluster.Name, each.Name)))
			return allErrs
		}
	}
	return nil
}

func (cc *ConfigChange) validateStrategy(lastCR, targetCR *ConfigRevision) field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")
	if cc.Spec.ChangeStrategy != ConfigChangeStrategyHotReload {
		return nil
	}

	if _, ok := lastCR.IsImageChange(targetCR); ok ||
		lastCR.IsVersionChange(targetCR) {
		allErrs = append(allErrs, field.Invalid(fld.Child("changeStrategy"), cc.Spec.ChangeStrategy,
			"HotReload is not supported for version or image change"))
	}

	return allErrs
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (cc *ConfigChange) ValidateUpdate(old runtime.Object) (warnings admission.Warnings, err error) {
	configChangeLogger.Info("validate update", "name", cc.Name)
	oldCC, _ := old.(*ConfigChange)
	var allErrs field.ErrorList
	if cc.Spec.ClusterName != oldCC.Spec.ClusterName {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("clusterName"), cc.Spec.ClusterName, "field is immutable"))
	}
	if cc.Spec.ChangeStrategy != oldCC.Spec.ChangeStrategy {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("changeStrategy"), cc.Spec.ChangeStrategy, "field is immutable"))
	}
	if cc.Spec.ConfigRevisionName != oldCC.Spec.ConfigRevisionName {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("configRevisionName"), cc.Spec.ConfigRevisionName, "field is immutable"))
	}

	return nil, invalidOrNil(allErrs, cc)
}

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