package v1

import (
	"context"
	"fmt"
	"regexp"

	"github.com/google/go-cmp/cmp"

	"k8s.io/apimachinery/pkg/api/resource"
	"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"
)

var (
	targetTablePatternRegexp = regexp.MustCompile("^(`.+`)\\.(`.+`)|(.+)\\.(.+)$")
)

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

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

func ParseTargetTable(targetTable string) (string, string, error) {
	var result []string

	for _, i := range targetTablePatternRegexp.FindStringSubmatch(targetTable) {
		if i != "" {
			result = append(result, i)
		}
	}

	if len(result) != 3 {
		return "", "", fmt.Errorf("Target table pattern does not match the requirement, please refer the document.")
	}

	return result[1], result[2], nil
}

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

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

var _ webhook.Defaulter = &Import{}

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

	// skip validate cluster exist for migrated resources
	if isMigratedResource(r.Annotations) {
		return
	}

	var c Cluster
	err := webhookClient.Get(context.TODO(), r.GetClusterRef(), &c)
	if err != nil {
		importlog.Error(err, "error get cluster of import, forward to validation")
		return
	}

	r.Labels[ProviderTypeLabel] = c.GetProviderType()
	r.Labels[RegionLabel] = c.Labels[RegionLabel]

	// TODO(user): fill in your defaulting logic, like for bucket URL?.
}

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

var _ webhook.Validator = &Import{}

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

	var allErrs field.ErrorList
	if errs := r.validateSpec(); errs != nil {
		allErrs = append(allErrs, errs...)
	}
	if err := validateProviderLabelForClusterSubresource(r); 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 *Import) ValidateUpdate(old runtime.Object) error {
	importlog.V(4).Info("validate update", "name", r.Name)

	var allErrs field.ErrorList
	oldI := old.(*Import)

	if !cmp.Equal(r.Spec, oldI.Spec) {
		allErrs = append(allErrs, field.Invalid(field.NewPath("spec"), r.Spec, "spec is immutable"))
	}

	if r.GetLabels()[ProviderTypeLabel] != oldI.GetLabels()[ProviderTypeLabel] {
		allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("labels"), r.GetLabels()[ProviderTypeLabel],
			fmt.Sprintf("%s label is immutable", ProviderTypeLabel)))
	}

	return invalidOrNil(allErrs, r)
}

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

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

	// skip validate cluster exist for migrated resources
	if !isMigratedResource(r.Annotations) {
		err := webhookClient.Get(context.TODO(), r.GetClusterRef(), &c)
		if err != nil {
			allErrs = append(allErrs, field.Invalid(fld.Child("clusterName"), r.Spec.ClusterName,
				fmt.Sprintf("cluster not found, err: %s", err.Error())))
			return allErrs
		}
		err = webhookClient.Get(context.TODO(), types.NamespacedName{Name: c.Spec.NetworkName, Namespace: c.ObjectMeta.Namespace}, &n)
		if err != nil {
			allErrs = append(allErrs, field.Invalid(fld.Child("clusterName"), r.Spec.ClusterName,
				fmt.Sprintf("network not found from the cluster: %s", err)))
			return allErrs
		}
	}

	if r.Spec.StorageRequest != "" {
		_, err := resource.ParseQuantity(r.Spec.StorageRequest)
		if err != nil {
			allErrs = append(allErrs, field.Invalid(fld.Child("storageRequest"), r.Spec.StorageRequest,
				fmt.Sprintf("bad quantity format, err: %s", err.Error())))
			return allErrs
		}
	}
	var provider string
	if r.GetLabels() != nil {
		provider = r.GetLabels()[ProviderTypeLabel]
	}

	if r.Spec.DryRun && r.Spec.AccessConfig.RoleArn == "" &&
		(provider == ProviderTypeAWS || provider == ProviderTypeAWSFreeTier) {
		allErrs = append(allErrs, field.Invalid(fld.Child("accessConfig", "roleArn"), r.Spec.AccessConfig.RoleArn,
			"accessConfig.RoleArn must be set in the dry run mode"))
	}

	allValidFormats := map[DataFormat]struct{}{
		DataFormatSQL:           {},
		DataFormatAuroraParquet: {},
		DataFormatCSV:           {},
		DataFormatRawParquet:    {},
	}
	if _, ok := allValidFormats[r.Spec.Source.Format]; !ok {
		allErrs = append(allErrs, field.Invalid(fld.Child("source.format"), r.Spec.Source.Format,
			"unsupported source data format"))
		return allErrs
	}
	if r.Spec.Source.Format == DataFormatCSV {
		if r.Spec.Source.CsvFormat == nil {
			allErrs = append(allErrs, field.Invalid(fld.Child("source", "csvFormat"), r.Spec.Source.CsvFormat,
				`csvConfig must set with the CSV format import`))
		}
		if len(allErrs) != 0 {
			return allErrs
		}
	}

	var storageTypes []string
	if r.Spec.Source.Storage.S3 != nil {
		storageTypes = append(storageTypes, ProviderTypeAWS)
	}
	if r.Spec.Source.Storage.GCS != nil {
		storageTypes = append(storageTypes, ProviderTypeGCP)
	}

	if len(storageTypes) == 0 {
		allErrs = append(allErrs, field.Invalid(fld.Child("source", "storage"), r.Spec.Source.Storage,
			`one of "s3" and "gcs" must be set`))
		return allErrs
	}

	if len(storageTypes) > 1 {
		allErrs = append(allErrs, field.Invalid(fld.Child("source", "storage"), r.Spec.Source.Storage,
			`only one of "s3" and "gcs" can be set`))
		return allErrs
	}

	storageType := storageTypes[0]
	// Check if the providerType matches the source storage type, both 'aws-free-tier', 'aws-shared-tier' and 'aws' should use s3
	if storageType != ConvtProviderType(n.Spec.Provider) {
		allErrs = append(allErrs, field.Invalid(fld.Child("source", "storage"), r.Spec.Source.Storage,
			`storage type is not supported by the provider`))
		return allErrs
	}

	objectNamePattern := r.Spec.Source.ObjectNamePattern
	targetTablePattern := r.Spec.Source.TargetTablePattern

	if objectNamePattern == "" && targetTablePattern != "" || objectNamePattern != "" && targetTablePattern == "" {
		allErrs = append(allErrs, field.Invalid(fld.Child("source", "objectNamePattern or targetTablePattern"), r.Spec.Source,
			`objectNamePattern and targetTablePattern should be provided at the same time`))
		return allErrs
	}

	if objectNamePattern != "" && targetTablePattern != "" {
		_, _, err := ParseTargetTable(targetTablePattern)
		if err != nil {
			allErrs = append(allErrs, field.Invalid(fld.Child("source", "targetTablePattern"), r.Spec.Source.TargetTablePattern, err.Error()))
			return allErrs
		}
	}

	return allErrs
}
