package v1

import (
	"context"
	"fmt"

	"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 clusterlog = logf.Log.WithName("cluster-resource")

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

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

var _ webhook.Defaulter = &Cluster{}

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

	var nt Network
	err := webhookClient.Get(context.TODO(), r.GetNetworkRef(), &nt)
	if err != nil {
		networklog.Error(err, "error get network of cluster, forward to validation")
		return
	}
	r.Labels[RegionLabel] = nt.Spec.Region
	r.Labels[ProviderTypeLabel] = nt.Labels[ProviderTypeLabel]

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

//+kubebuilder:webhook:path=/validate-cloud-pingcap-com-v1-cluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=clusters,verbs=create;update,versions=v1,name=vcluster.kb.io,admissionReviewVersions={v1,v1beta1}

var _ webhook.Validator = &Cluster{}

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

	var allErrs field.ErrorList
	if errs := r.validateSpec(); errs != nil {
		allErrs = append(allErrs, errs...)
	}
	if err := r.validateProviderLabel(); err != nil {
		allErrs = append(allErrs, err)
	}
	return invalidOrNil(allErrs, r)
}

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

	var allErrs field.ErrorList
	oldR := old.(*Cluster)
	if r.Spec.NetworkName != oldR.Spec.NetworkName {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("network"), r.Spec.NetworkName, "field is immutable"))
	}
	if oldR.Spec.SqlEndpoints.DNS.Identity != "" && r.Spec.SqlEndpoints.DNS.Identity != oldR.Spec.SqlEndpoints.DNS.Identity {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("sqlEndpoints.dns.identity"),
			r.Spec.SqlEndpoints.DNS.Identity, "field is immutable after it is set"))
	}
	if r.Spec.SqlEndpoints.Public.IsEnabled() && oldR.Spec.SqlEndpoints.Public.IsEnabled() {
		if r.GetPublicSQLEndpointPort() != oldR.GetPublicSQLEndpointPort() {
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("sqlEndpoints.public.port"),
				r.Spec.SqlEndpoints.Public.Port, "field is immutable"))
		}
	}
	if r.Spec.SqlEndpoints.Private != nil && oldR.Spec.SqlEndpoints.Private != nil {
		if r.Spec.SqlEndpoints.Private.IsEnabled() && oldR.Spec.SqlEndpoints.Private.IsEnabled() {
			if r.GetPrivateSQLEndpointPort() != oldR.GetPrivateSQLEndpointPort() {
				allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("sqlEndpoints.private.port"),
					r.Spec.SqlEndpoints.Private.Port, "field is immutable"))
			}
		}
	}
	// local storage not support resize
	if r.Spec.Cluster.TiKV.GetUseLocalStorage() {
		if oldR.Spec.Cluster.TiKV.StorageSize.Cmp(r.Spec.Cluster.TiKV.StorageSize) != 0 {
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("cluster.tikv.storageSize"),
				r.Spec.Cluster.TiKV.StorageSize, "size of local storage is immutable"))
		}
	}
	if r.Spec.Cluster.TiFlash.GetUseLocalStorage() {
		if oldR.Spec.Cluster.TiFlash.StorageSize.Cmp(r.Spec.Cluster.TiFlash.StorageSize) != 0 {
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("cluster.tiflash.storageSize"),
				r.Spec.Cluster.TiFlash.StorageSize, "size of local storage is immutable"))
		}
	}
	if err := r.validateProviderLabel(); err != nil {
		allErrs = append(allErrs, err)
	}
	return invalidOrNil(allErrs, r)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Cluster) ValidateDelete() error {
	return nil
}

func (r *Cluster) validateStorageClass(p *Provider) field.ErrorList {
	var allErrs field.ErrorList
	var name string

	fld := field.NewPath("spec", "cluster")
	name = r.Spec.Cluster.PD.StorageClassName
	if name != "" {
		_, err := p.FindStorageClass(name)
		if err != nil {
			ferr := field.Invalid(fld.Child("pd", "storageClassName"), name, err.Error())
			allErrs = append(allErrs, ferr)
		}
	}

	name = r.Spec.Cluster.TiKV.StorageClassName
	if name != "" {
		_, err := p.FindStorageClass(name)
		if err != nil {
			ferr := field.Invalid(fld.Child("tikv", "storageClassName"), name, err.Error())
			allErrs = append(allErrs, ferr)
		}
	}

	name = r.Spec.Cluster.TiFlash.StorageClassName
	if name != "" {
		_, err := p.FindStorageClass(name)
		if err != nil {
			ferr := field.Invalid(fld.Child("tiflash", "storageClassName"), name, err.Error())
			allErrs = append(allErrs, ferr)
		}
	}

	return allErrs
}

func (r *Cluster) validateSpec() field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")
	var nt Network
	err := webhookClient.Get(context.TODO(), r.GetNetworkRef(), &nt)
	if err != nil {
		allErrs = append(allErrs, field.Invalid(fld.Child("networkName"), r.Spec.NetworkName,
			fmt.Sprintf("network not found, err: %s", err.Error())))
	}
	p := &Provider{}
	err = webhookClient.Get(context.TODO(), types.NamespacedName{Name: nt.Spec.Provider}, p)
	if err != nil {
		allErrs = append(allErrs, field.Invalid(fld.Child("networkName"), r.Spec.NetworkName,
			fmt.Sprintf("network does not have valid provider, err: %s", err.Error())))
	} else {
		errList := r.validateTopology(fld.Child("cluster"), r.Spec.Cluster, p)
		if errList != nil {
			allErrs = append(allErrs, errList...)
		}

		errList = r.validateStorageClass(p)
		if errList != nil {
			allErrs = append(allErrs, errList...)
		}

		errList = r.validateSqlEndpoints(fld.Child("sqlEndpoints"), r.Spec.SqlEndpoints)
		if errList != nil {
			allErrs = append(allErrs, errList...)
		}
	}

	return allErrs
}

func (r *Cluster) validateTopology(parent *field.Path, topo ClusterTopology, p *Provider) field.ErrorList {
	var allErrs field.ErrorList
	if _, err := p.FindInstanceType(topo.PD.InstanceType); err != nil {
		allErrs = append(allErrs, field.Invalid(parent.Child("pd").Child("instanceType"), topo.PD.InstanceType, "invalid instance type"))
	}
	if _, err := p.FindInstanceType(topo.TiKV.InstanceType); err != nil {
		allErrs = append(allErrs, field.Invalid(parent.Child("tikv").Child("instanceType"), topo.TiKV.InstanceType, "invalid instance type"))
	}
	if _, err := p.FindInstanceType(topo.TiDB.InstanceType); err != nil {
		allErrs = append(allErrs, field.Invalid(parent.Child("tidb").Child("instanceType"), topo.TiDB.InstanceType, "invalid instance type"))
	}
	if topo.TiFlash.Replicas > 0 {
		if _, err := p.FindInstanceType(topo.TiFlash.InstanceType); err != nil {
			allErrs = append(allErrs, field.Invalid(parent.Child("tiflash").Child("instanceType"), topo.TiFlash.InstanceType, "invalid instance type"))
		}
	}
	return allErrs
}

func (r *Cluster) validateSqlEndpoints(parent *field.Path, endpoints SqlEndpoints) field.ErrorList {
	var allErrs field.ErrorList
	if endpoints.Public.Port != nil && *endpoints.Public.Port == 10080 {
		allErrs = append(allErrs, field.Invalid(parent.Child("public.port"), *endpoints.Public.Port, "port is reserved"))
	}
	if endpoints.Private != nil && endpoints.Private.Port != nil && *endpoints.Private.Port == 10080 {
		allErrs = append(allErrs, field.Invalid(parent.Child("private.port"), *endpoints.Private.Port, "port is reserved"))
	}
	return allErrs
}

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