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"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// 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{}
	}

	// TODO: remove it after all clusters are migrated
	if r.Spec.NetworkName != "" && r.Spec.Kubernetes.Name == "" {
		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() (admission.Warnings, 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 nil, invalidOrNil(allErrs, r)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Cluster) ValidateUpdate(old runtime.Object) (admission.Warnings, 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"))
	}
	allErrs = append(allErrs, validateSQLEndpoints(&oldR.Spec.SqlEndpoints, &r.Spec.SqlEndpoints, "")...)
	// 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"))
		}
	}
	// tikv dedicated raft engine storage is immutable
	curSetted := r.Spec.Cluster.TiKV.DedicatedRaftEngineStorage != nil
	oldSetted := oldR.Spec.Cluster.TiKV.DedicatedRaftEngineStorage != nil
	if curSetted != oldSetted {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("cluster.tikv.dedicatedRaftEngineStorage"),
			r.Spec.Cluster.TiKV.DedicatedRaftEngineStorage, "field is immutable"))
	}

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

	// serverless label LabelEncryptKeyspace is immutable
	if r.Labels[ProviderTypeLabel] == ProviderTypeAWSFreeTier {
		if r.Labels[LabelEncryptKeyspace] != oldR.Labels[LabelEncryptKeyspace] {
			allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("labels").Child(LabelEncryptKeyspace),
				r.Labels[LabelEncryptKeyspace], "field is immutable"))
		}
	}

	for _, oldGroup := range oldR.Spec.ExtraGroups {
		for _, group := range r.Spec.ExtraGroups {
			if oldGroup.Name == group.Name {
				allErrs = append(allErrs, validateSQLEndpoints(&oldGroup.SqlEndpoints, &group.SqlEndpoints, group.Name)...)
			}
		}
	}
	allErrs = append(allErrs, r.validateLBTarget()...)
	return nil, invalidOrNil(allErrs, r)
}

func extraGroupPath(groupName string, root string, childPath ...string) *field.Path {
	if groupName == "" {
		return field.NewPath("spec").Child(root, childPath...)
	}

	path := field.NewPath("spec", "extraGroups")
	path = path.Key(groupName)
	return path.Child(root, childPath...)
}

func validateSQLEndpoints(old, cur *SqlEndpoints, groupName string) field.ErrorList {
	var allErrs field.ErrorList

	// DNS identity modification is only allowed when current DNS is disabled
	// This ensures consistency with the business logic in updateDNSIdentity method
	if old.DNS.Identity != "" && cur.DNS.Identity != old.DNS.Identity {
		// Allow identity change only if old DNS is disabled
		// This matches the business logic: identity can only be changed when DNS is disabled
		if !old.DNS.Disabled {
			allErrs = append(allErrs, field.Invalid(extraGroupPath(groupName, "sqlEndpoints", "dns", "identity"),
				cur.DNS.Identity, "DNS identity can only be modified when current DNS is disabled"))
		}
	}

	if cur.Public.IsEnabled() && old.Public.IsEnabled() {
		if cur.GetPublicSQLEndpointPort() != old.GetPublicSQLEndpointPort() {
			allErrs = append(allErrs, field.Invalid(extraGroupPath(groupName, "sqlEndpoints", "public", "port"),
				cur.Public.Port, "field is immutable"))
		}
	}
	if cur.Private != nil && old.Private != nil {
		if cur.Private.IsEnabled() && old.Private.IsEnabled() {
			if cur.GetPrivateSQLEndpointPort() != old.GetPrivateSQLEndpointPort() {
				allErrs = append(allErrs, field.Invalid(extraGroupPath(groupName, "sqlEndpoints", "private", "port"),
					cur.Private.Port, "field is immutable"))
			}
		}
	}

	return allErrs
}

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

// validateStorageClass validate SCs used by component, and just only log error out rather than raise an error.
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)
		}
	}

	// Since we support use customized SC for component, so the SC used by component may can't be found in Provider.
	if len(allErrs) != 0 {
		clusterlog.Info("some SCs are not found, may customized SCs are used, ignore.", "errs", allErrs)
		allErrs = nil
	}

	return allErrs
}

func (r *Cluster) validateLBTarget() field.ErrorList {
	parent := field.NewPath("spec")
	var allErrs field.ErrorList
	if r.Spec.SqlEndpoints.UseTiProxyAsLBTarget && r.Spec.Cluster.TiProxy.Replicas == 0 {
		allErrs = append(allErrs, field.Invalid(parent.Child("cluster", "tiProxy", "replicas"), r.Spec.Cluster.TiProxy.Replicas, "tiProxy replicas must be greater than 0 when useTiProxyAsLBTarget is true"))
	}
	for _, group := range r.Spec.ExtraGroups {
		if group.Deleted() {
			continue
		}

		if group.SqlEndpoints.UseTiProxyAsLBTarget && group.TiProxy.Replicas == 0 {
			allErrs = append(allErrs, field.Invalid(parent.Child("extraGroups", group.Name, "tiProxy", "replicas"), group.TiProxy.Replicas, "tiProxy replicas must be greater than 0 when useTiProxyAsLBTarget is true"))
		}
	}
	return allErrs
}

func (r *Cluster) validateSpec() field.ErrorList {
	var allErrs field.ErrorList
	fld := field.NewPath("spec")
	// TODO: remove it after all clusters are migrated
	if r.Spec.NetworkName != "" && r.Spec.Kubernetes.Name == "" {
		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 {
	// TODO: remove it after all clusters are migrated
	if r.Spec.NetworkName != "" && r.Spec.Kubernetes.Name == "" {
		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
}
