package validator

import (
	"context"
	"errors"
	"fmt"
	"regexp"

	"k8s.io/apimachinery/pkg/runtime"
	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"

	customdomainv1 "github.com/tidbcloud/api-gateway/operator/api/v1"
)

const hostKey = ".spec.host"

var validDomainRegexp = regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)

func isValidDomain(host string) bool {
	return validDomainRegexp.MatchString(host)
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
//+kubebuilder:webhook:path=/validate-customdomain-apigateway-tidbcloud-com-v1-customdomain,mutating=false,failurePolicy=fail,sideEffects=None,groups=customdomain.apigateway.tidbcloud.com,resources=customdomains,verbs=create;update,versions=v1,name=vcustomdomain.kb.io,admissionReviewVersions=v1

type CustomDomainValidator struct {
	client.Client
	Decoder          *admission.Decoder
	ControllerDomain string
}

func (v *CustomDomainValidator) validate(ctx context.Context, obj runtime.Object) error {
	var err error
	log := logf.FromContext(ctx)
	log.Info("validate webhook", "resource", "CustomDomain")
	domain, ok := obj.(*customdomainv1.CustomDomain)
	if !ok {
		err = fmt.Errorf("expected a CustomDomain but got a %T", obj)
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain")
		return err
	}
	if domain.Spec.Cname != v.ControllerDomain {
		err = errors.New("invalid cname, please check if it's matched with your target region")
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain", "name", domain.Name, "host", domain.Spec.Host)
		return err
	}
	if !isValidDomain(domain.Spec.Host) {
		err = errors.New("invalid host, it must be a valid domain")
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain", "name", domain.Name, "host", domain.Spec.Host)
		return err
	}

	var sameHostDomains customdomainv1.CustomDomainList
	if err = v.List(ctx, &sameHostDomains, client.MatchingFields{hostKey: domain.Spec.Host}); err != nil {
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain", "name", domain.Name, "host", domain.Spec.Host)
		return err
	}
	if len(sameHostDomains.Items) >= 1 {
		if len(sameHostDomains.Items) == 1 && sameHostDomains.Items[0].Name == domain.Name && sameHostDomains.Items[0].Namespace == domain.Namespace {
			return nil
		}
		err = fmt.Errorf("spec.host must be unique, but got %d duplicate resources", len(sameHostDomains.Items))
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain", "name", domain.Name, "host", domain.Spec.Host)
		return err
	}

	return nil
}

func (v *CustomDomainValidator) validateImmutableFields(ctx context.Context, oldObj, newObj runtime.Object) error {
	var err error
	log := logf.FromContext(ctx)
	log.Info("validate immutable fields webhook", "resource", "CustomDomain")
	oldDomain, ok := oldObj.(*customdomainv1.CustomDomain)
	if !ok {
		err = fmt.Errorf("expected oldObj CustomDomain but got a %T", oldObj)
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain")
		return err
	}
	newDomain, ok := newObj.(*customdomainv1.CustomDomain)
	if !ok {
		err = fmt.Errorf("expected newObj CustomDomain but got a %T", oldObj)
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain")
		return err
	}
	if oldDomain.Spec.ServiceName != newDomain.Spec.ServiceName {
		err = errors.New("serviceName is immutable")
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain", "name", newDomain.Name, "host", newDomain.Spec.Host)
		return err
	}
	if oldDomain.Spec.ServicePort != newDomain.Spec.ServicePort {
		err = errors.New("servicePort is immutable")
		log.Error(err, "validate customdomain failed", "resource", "CustomDomain", "name", newDomain.Name, "host", newDomain.Spec.Host)
		return err
	}

	return nil
}

func (v *CustomDomainValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
	return v.validate(ctx, obj)
}

func (v *CustomDomainValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
	if err := v.validateImmutableFields(ctx, oldObj, newObj); err != nil {
		return err
	}
	return v.validate(ctx, newObj)
}

func (v *CustomDomainValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
	return v.validate(ctx, obj)
}

func (v *CustomDomainValidator) SetupWithManager(mgr ctrl.Manager) error {
	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &customdomainv1.CustomDomain{}, hostKey, func(rawObj client.Object) []string {
		domain := rawObj.(*customdomainv1.CustomDomain)
		return []string{domain.Spec.Host}
	}); err != nil {
		return err
	}
	return ctrl.NewWebhookManagedBy(mgr).
		For(&customdomainv1.CustomDomain{}).
		WithValidator(v).
		Complete()
}
