package configuration

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/apiserver/pkg/util/webhook"
	"k8s.io/klog/v2"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)

// ValidatingAdmission validates ResourceInterpreterWebhookConfiguration object when creating/updating.
type ValidatingAdmission struct {
	decoder *admission.Decoder
}

// Check if our ValidatingAdmission implements necessary interface
var _ admission.Handler = &ValidatingAdmission{}
var _ admission.DecoderInjector = &ValidatingAdmission{}

// Handle implements admission.Handler interface.
// It yields a response to an AdmissionRequest.
func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response {
	configuration := &configv1alpha1.ResourceInterpreterWebhookConfiguration{}

	err := v.decoder.Decode(req, configuration)
	if err != nil {
		return admission.Errored(http.StatusBadRequest, err)
	}
	klog.V(2).Infof("Validating ResourceInterpreterWebhookConfiguration(%s) for request: %s", configuration.Name, req.Operation)

	var allErrors field.ErrorList
	hookNames := sets.NewString()
	for i, hook := range configuration.Webhooks {
		allErrors = append(allErrors, validateWebhook(&configuration.Webhooks[i], field.NewPath("webhooks").Index(i))...)
		if hookNames.Has(hook.Name) {
			allErrors = append(allErrors, field.Duplicate(field.NewPath("webhooks").Index(i).Child("name"), hook.Name))
			continue
		}
		hookNames.Insert(hook.Name)
	}

	if len(allErrors) != 0 {
		klog.Error(allErrors.ToAggregate())
		return admission.Denied(allErrors.ToAggregate().Error())
	}

	return admission.Allowed("")
}

// InjectDecoder implements admission.DecoderInjector interface.
// A decoder will be automatically injected.
func (v *ValidatingAdmission) InjectDecoder(d *admission.Decoder) error {
	v.decoder = d
	return nil
}

var supportedInterpreterOperation = sets.NewString(
	string(configv1alpha1.InterpreterOperationAll),
	string(configv1alpha1.InterpreterOperationInterpretReplica),
	string(configv1alpha1.InterpreterOperationInterpretDependency),
	string(configv1alpha1.InterpreterOperationReviseReplica),
	string(configv1alpha1.InterpreterOperationRetain),
	string(configv1alpha1.InterpreterOperationAggregateStatus),
	string(configv1alpha1.InterpreterOperationInterpretStatus),
	string(configv1alpha1.InterpreterOperationInterpretHealth),
)

var acceptedInterpreterContextVersions = []string{configv1alpha1.GroupVersion.Version}

func validateWebhook(hook *configv1alpha1.ResourceInterpreterWebhook, fldPath *field.Path) field.ErrorList {
	var allErrors field.ErrorList
	// hook.Name must be fully qualified
	allErrors = append(allErrors, validation.IsFullyQualifiedDomainName(fldPath.Child("name"), hook.Name)...)

	for i := range hook.Rules {
		allErrors = append(allErrors, validateRuleWithOperations(&hook.Rules[i], fldPath.Child("rules").Index(i))...)
	}

	if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) {
		allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds"))
	}

	cc := hook.ClientConfig
	switch {
	case (cc.URL == nil) == (cc.Service == nil):
		allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required"))
	case cc.URL != nil:
		allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...)
	case cc.Service != nil:
		allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, *cc.Service.Port)...)
	}

	allErrors = append(allErrors, validateInterpreterContextVersions(hook.InterpreterContextVersions, fldPath.Child("interpreterContextVersions"))...)
	return allErrors
}

func hasWildcardOperation(operations []configv1alpha1.InterpreterOperation) bool {
	for _, o := range operations {
		if o == configv1alpha1.InterpreterOperationAll {
			return true
		}
	}
	return false
}

func validateRuleWithOperations(ruleWithOperations *configv1alpha1.RuleWithOperations, fldPath *field.Path) field.ErrorList {
	var allErrors field.ErrorList
	if len(ruleWithOperations.Operations) == 0 {
		allErrors = append(allErrors, field.Required(fldPath.Child("operations"), ""))
	}
	if len(ruleWithOperations.Operations) > 1 && hasWildcardOperation(ruleWithOperations.Operations) {
		allErrors = append(allErrors, field.Invalid(fldPath.Child("operations"), ruleWithOperations.Operations, "if '*' is present, must not specify other operations"))
	}
	for i, operation := range ruleWithOperations.Operations {
		if !supportedInterpreterOperation.Has(string(operation)) {
			allErrors = append(allErrors, field.NotSupported(fldPath.Child("operations").Index(i), operation, supportedInterpreterOperation.List()))
		}
	}
	allErrors = append(allErrors, validateRule(&ruleWithOperations.Rule, fldPath)...)
	return allErrors
}

func validateInterpreterContextVersions(versions []string, fldPath *field.Path) field.ErrorList {
	allErrors := field.ErrorList{}

	// Currently, only v1alpha1 accepted in InterpreterContextVersions
	if len(versions) < 1 {
		allErrors = append(allErrors, field.Required(fldPath, fmt.Sprintf("must specify one of %v", strings.Join(acceptedInterpreterContextVersions, ", "))))
	} else {
		visited := map[string]bool{}
		hasAcceptedVersion := false
		for i, v := range versions {
			if visited[v] {
				allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, "duplicate version"))
				continue
			}
			visited[v] = true
			for _, errString := range validation.IsDNS1035Label(v) {
				allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, errString))
			}
			if isAcceptedExploreReviewVersions(v) {
				hasAcceptedVersion = true
			}
		}
		if !hasAcceptedVersion {
			allErrors = append(allErrors, field.Invalid(
				fldPath, versions,
				fmt.Sprintf("must include at least one of %v", strings.Join(acceptedInterpreterContextVersions, ", "))))
		}
	}
	return allErrors
}

func isAcceptedExploreReviewVersions(v string) bool {
	for _, version := range acceptedInterpreterContextVersions {
		if v == version {
			return true
		}
	}
	return false
}
