package v1

import (
	"context"
	"fmt"
	"net"

	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"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"
)

// log is for logging in this package.
var networklog = logf.Log.WithName("network-resource")

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

//+kubebuilder:webhook:path=/mutate-cloud-pingcap-com-v1-network,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=networks,verbs=create;update,versions=v1,name=mnetwork.kb.io,admissionReviewVersions={v1,v1beta1}

var _ webhook.Defaulter = &Network{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *Network) Default() {
	if r.Labels == nil {
		r.Labels = map[string]string{}
	}

	var p Provider
	err := webhookClient.Get(context.TODO(), types.NamespacedName{Name: r.Spec.Provider}, &p)
	if err != nil {
		networklog.Error(err, "error get provider of network, forward to validation")
		return
	}
	// provider must have a type label
	r.Labels[ProviderTypeLabel] = p.Labels[ProviderTypeLabel]
	r.ObjectMeta.Labels[RegionLabel] = r.Spec.Region

	if r.Spec.ForProvider == nil {
		r.Spec.ForProvider = &runtime.RawExtension{}
	}
	populateRawExtension(r.Spec.ForProvider)
	populateRawExtension(&r.Status.FromProvider)
	setDefaultProjectId(webhookClient, &r.ObjectMeta)
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
//+kubebuilder:webhook:path=/validate-cloud-pingcap-com-v1-network,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=networks,verbs=create;update,versions=v1,name=vnetwork.kb.io,admissionReviewVersions={v1,v1beta1}

var _ webhook.Validator = &Network{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Network) ValidateCreate() (admission.Warnings, error) {
	networklog.V(4).Info("validate create", "name", r.Name)

	var allErrs field.ErrorList
	allErrs = r.validateCreate()

	pt := r.Labels[ProviderTypeLabel]

	if pt == ProviderTypeAWS || pt == ProviderTypeGCP {
		if err := r.validataShootName(); err != nil {
			allErrs = append(allErrs, err)
		}
	}

	return nil, invalidOrNil(allErrs, r)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Network) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
	networklog.V(4).Info("validate update", "name", r.Name)

	var allErrs field.ErrorList
	oldNet := old.(*Network)

	if errs := r.validateCreate(); errs != nil {
		allErrs = append(allErrs, errs...)
	}
	if errs := r.validateUpdate(oldNet); errs != nil {
		allErrs = append(allErrs, errs...)
	}

	return nil, invalidOrNil(allErrs, r)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Network) ValidateDelete() (admission.Warnings, error) {
	networklog.Info("validate delete", "name", r.Name)

	// TODO: reject deletions if there is any cluster belongs to this network running
	return nil, nil
}

func (r *Network) validateCreate() field.ErrorList {
	var allErrs field.ErrorList
	if errs := r.validateSpec(); errs != nil {
		allErrs = append(allErrs, errs...)
	}
	if err := ValidateProjectId(&r.ObjectMeta); err != nil {
		allErrs = append(allErrs, err)
	}

	if err := r.validateProviderLabel(); err != nil {
		allErrs = append(allErrs, err)
	}
	return allErrs
}

// shoot name generate by ProjectID、Region and Provider
// ProjectID、Region and Provider can not same with another network
func (r *Network) validataShootName() *field.Error {
	nl := new(NetworkList)
	// should be use client list network, but directly client unsupport field-selector
	err := webhookClient.cached.List(context.TODO(), nl,
		client.MatchingFields(map[string]string{
			"spec.region": r.Spec.Region,
		}),
		client.MatchingLabels(map[string]string{
			ProjectLabel: r.ObjectMeta.Labels[ProjectLabel],
		}),
	)
	if err != nil {
		return field.Invalid(field.NewPath("metadata").Child("labels"), r.ObjectMeta.Labels[ProjectLabel], err.Error())
	}
	// controller-runtime only support one field selector
	for _, network := range nl.Items {
		if network.Spec.Provider == r.Spec.Provider {
			return field.Invalid(field.NewPath("metadata").Child("labels"), r.ObjectMeta.Labels[ProjectLabel], fmt.Sprintf("ProjectID、Region and Provider same with %s, shoot name will conflict", network.Name))
		}
	}
	return nil
}

func (r *Network) validateUpdate(oldNet *Network) field.ErrorList {
	var allErrs field.ErrorList
	if r.Spec.AccountRef != nil && oldNet.Spec.AccountRef != nil {
		if r.Spec.AccountRef.Name != oldNet.Spec.AccountRef.Name {
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("accountRef").Child("name"), r.Spec.AccountRef.Name, "field is immutable"))
		}
	} else if r.Spec.AccountRef == nil && oldNet.Spec.AccountRef != nil {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("accountRef"), r.Spec.AccountRef, "field is immutable"))
	}
	if r.Spec.CIDR != oldNet.Spec.CIDR {
		err := field.Invalid(field.NewPath("spec").Child("cidr"), r.Spec.CIDR, "field is immutable")
		allErrs = append(allErrs, err)
	}
	if err := ValidateUpdateProjectId(&oldNet.ObjectMeta, &r.ObjectMeta); err != nil {
		allErrs = append(allErrs, err)
	}
	return allErrs
}

func (r *Network) validateSpec() field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")

	if err := r.validateCIDR(fld); err != nil {
		allErrs = append(allErrs, err)
	}

	p, err := r.validateProvider()
	if err != nil {
		allErrs = append(allErrs, err)
		return allErrs
	}

	if err := r.validateRegion(p); err != nil {
		allErrs = append(allErrs, err)
	}

	return allErrs
}

func (r *Network) validateProviderLabel() *field.Error {
	var p Provider
	err := webhookClient.Get(context.TODO(), types.NamespacedName{Name: r.Spec.Provider}, &p)
	if err != nil {
		return badProviderTypeLabel(r.Labels[ProviderTypeLabel], fmt.Sprintf("unknown due to error: %s", err.Error()))
	}
	if r.Labels[ProviderTypeLabel] != p.Labels[ProviderTypeLabel] {
		return badProviderTypeLabel(r.Labels[ProviderTypeLabel], p.Labels[ProviderTypeLabel])
	}
	return nil
}

func (r *Network) validateProvider() (*Provider, *field.Error) {
	p := new(Provider)
	err := webhookClient.Get(context.TODO(), types.NamespacedName{Name: r.Spec.Provider}, p)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil, field.Invalid(field.NewPath("spec", "provider"), r.Spec.Provider, "unknown provider")
		}
		return nil, field.InternalError(nil, err)
	}

	return p, nil
}

func (r *Network) validateRegion(p *Provider) *field.Error {
	match := false
	for _, region := range p.Spec.Regions {
		if r.Spec.Region == region.Name {
			match = true
			break
		}
	}

	if !match {
		var names []string
		for _, region := range p.Spec.Regions {
			names = append(names, region.Name)
		}
		detail := fmt.Sprintf("unsupported region, support regions: %v", names)
		return field.Invalid(field.NewPath("spec", "region"), r.Spec.Region, detail)
	}

	return nil
}

func (r *Network) validateCIDR(fld *field.Path) *field.Error {
	_, ipNet, err := net.ParseCIDR(r.Spec.CIDR)
	if err != nil {
		return field.Invalid(fld, r.Spec.CIDR, err.Error())
	}

	ones, _ := ipNet.Mask.Size()
	// hard limit 4 bits for aws, but will get only 1 IP for each LB, and 2 IP for worker.
	// we limit at least 10 bits here.
	minHostBits := 10
	if 32-ones < 4 {
		return field.Invalid(fld, r.Spec.CIDR, fmt.Sprintf("host bits is: %d, need at least %d", 32-ones, minHostBits))
	}

	return nil
}
