package v1

import (
	"context"
	"fmt"
	"time"

	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"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"
)

var (
	shouldSetClusterID = map[UpgradeOperationTarget]struct{}{
		TiDBNodes: {},
		CDCNodes:  {},
		DMNodes:   {},
	}

	validUpgradeOperationTargets = map[UpgradeOperationTarget]struct{}{
		ControlPlane:   {},
		AdminNodes:     {},
		TiDBNodes:      {},
		CDCNodes:       {},
		DMNodes:        {},
		EmptyNodePools: {},
		AddOns:         {},
	}
)

// log is for logging in this package.
var upgradeoperationlog = logf.Log.WithName("upgradeoperation")

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

//+kubebuilder:webhook:path=/mutate-cloud-pingcap-com-v1-upgradeoperation,mutating=true,failurePolicy=fail,sideEffects=None,groups=cloud.pingcap.com,resources=upgradeoperations,verbs=create,versions=v1,name=mupgradeoperation.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &UpgradeOperation{}

const (
	UpgradeOperationScheduleLayout = "2006-01-02 15:04:05 -0700 MST"

	UpgradeOperationFinalizer = "upgradeoperation.cloud.pingcap.com/finalizer"
)

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *UpgradeOperation) Default() {
	upgradeoperationlog.Info("default", "name", r.Name, "namespace", r.Namespace)

	if r.Labels == nil {
		r.Labels = make(map[string]string)
	}

	if r.Spec.NetworkName != "" {
		var network Network
		if err := webhookClient.Get(context.Background(), types.NamespacedName{
			Namespace: r.Namespace,
			Name:      r.Spec.NetworkName,
		}, &network); err != nil {
			upgradeoperationlog.Error(err, "failed to get network", "networkName", r.Spec.NetworkName)
			return
		}

		r.Labels[TenantTagKey] = network.Labels[TenantTagKey]
		r.Labels[ProjectTagKey] = network.Labels[ProjectTagKey]
		r.Labels[RegionLabel] = network.Spec.Region
		r.Labels[ProviderTypeLabel] = network.Spec.Provider
	}

	// Only add finalizer when the UpgradeOperation is created.
	if r.DeletionTimestamp.IsZero() {
		controllerutil.AddFinalizer(r, UpgradeOperationFinalizer)
	}
}

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

var _ webhook.Validator = &UpgradeOperation{}

func (r *UpgradeOperation) validateSpec() (errs field.ErrorList) {
	if err := r.validateSchedule(); err != nil {
		errs = append(errs, err...)
	}
	if err := r.validateNetwork(); err != nil {
		errs = append(errs, err...)
	}
	if err := r.validateSteps(); err != nil {
		errs = append(errs, err...)
	}
	return
}

// validateSchedule checks if the format of schedule is correct and if it's after now.
func (r *UpgradeOperation) validateSchedule() (errs field.ErrorList) {
	fld := field.NewPath("spec").Child("schedule")

	schedule, err := time.Parse(UpgradeOperationScheduleLayout, r.Spec.Schedule)
	if err != nil {
		upgradeoperationlog.Error(err, "failed to parse schedule", "schedule", r.Spec.Schedule)
		errs = append(errs, field.Invalid(fld, r.Spec.Schedule, fmt.Sprintf("failed to parse schedule: %s", err.Error())))
		return
	}

	if (r.Status.Phase == UpgradeOperationPhaseNone || r.Status.Phase == UpgradeOperationPhasePending) && schedule.Before(time.Now()) {
		err = fmt.Errorf("schedule %s is before now, should be after now", r.Spec.Schedule)
		upgradeoperationlog.Error(err, "schedule is invalid")
		errs = append(errs, field.Invalid(fld, r.Spec.Schedule, err.Error()))
	}

	return
}

// validateNetwork checks if the network CR exists.
func (r *UpgradeOperation) validateNetwork() (errs field.ErrorList) {
	fld := field.NewPath("spec").Child("networkName")

	if r.Status.Phase == UpgradeOperationPhaseCompleted || r.Status.Phase == UpgradeOperationPhaseFailed {
		return
	}

	if r.Spec.NetworkName != "" {
		var network Network
		if err := webhookClient.Get(context.Background(), types.NamespacedName{
			Namespace: r.Namespace,
			Name:      r.Spec.NetworkName,
		}, &network); err != nil {
			upgradeoperationlog.Error(err, "failed to get network", "networkName", r.Spec.NetworkName)
			errs = append(errs, field.Invalid(fld, r.Spec.NetworkName,
				fmt.Sprintf("Network %s/%s is not found, err: %s", r.Namespace, r.Spec.NetworkName, err.Error())))
		}
	}

	return
}

// validateSteps checks if the steps are valid.
func (r *UpgradeOperation) validateSteps() (errs field.ErrorList) {
	fld := field.NewPath("spec").Child("steps")

	for _, step := range r.Spec.Steps {
		if _, ok := validUpgradeOperationTargets[step.Target]; !ok {
			errs = append(errs, field.Invalid(fld, r.Spec.Steps, fmt.Sprintf("target %s is invalid", step.Target)))
			continue
		}

		if _, ok := shouldSetClusterID[step.Target]; ok {
			if step.Cluster == "" {
				errs = append(errs, field.Invalid(fld, r.Spec.Steps, fmt.Sprintf("the `cluster` field is required for target %s", step.Target)))
				continue
			}

			// If r.Spec.NetworkName == "", it means in 3-layer arch.
			if r.Spec.NetworkName != "" && r.Status.Phase != UpgradeOperationPhaseCompleted && r.Status.Phase != UpgradeOperationPhaseFailed {
				var cluster Cluster
				if err := webhookClient.Get(context.TODO(), types.NamespacedName{
					Namespace: r.Namespace,
					Name:      step.Cluster,
				}, &cluster); err == nil && cluster.Spec.NetworkName != r.Spec.NetworkName {
					errs = append(errs, field.Invalid(fld, r.Spec.Steps, fmt.Sprintf("cluster %s/%s is not in network %s", r.Namespace, step.Cluster, r.Spec.NetworkName)))
					continue
				}
			}
		}
	}

	return errs
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *UpgradeOperation) ValidateCreate() (admission.Warnings, error) {
	upgradeoperationlog.Info("validate create", "name", r.Name, "namespace", r.Namespace)
	return nil, invalidOrNil(r.validateSpec(), r)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *UpgradeOperation) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
	upgradeoperationlog.Info("validate update", "name", r.Name, "namespace", r.Namespace)
	oldUO, _ := old.(*UpgradeOperation)
	var allErrs field.ErrorList

	switch r.Status.Phase {
	// Allow to update spec when it does not begin to run.
	case UpgradeOperationPhaseNone, UpgradeOperationPhasePending:
		if err := r.validateSpec(); err != nil {
			allErrs = append(allErrs, err...)
		}

	// Once it begins to run, or it's finished, some fields are immutable.
	case UpgradeOperationPhaseRunning, UpgradeOperationPhaseCompleted, UpgradeOperationPhaseFailed:
		if r.Spec.NetworkName != oldUO.Spec.NetworkName {
			err := fmt.Errorf("networkName is immutable: %s", r.Spec.NetworkName)
			upgradeoperationlog.Error(err, "try to update networkName", "old", oldUO.Spec.NetworkName, "new", r.Spec.NetworkName)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("networkName"), r.Spec.NetworkName, err.Error()))
		}

		if r.Spec.Schedule != oldUO.Spec.Schedule {
			err := fmt.Errorf("schedule is immutable: %s", r.Spec.Schedule)
			upgradeoperationlog.Info("try to update schedule", "old", oldUO.Spec.Schedule, "new", r.Spec.Schedule)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("schedule"), r.Spec.Schedule, err.Error()))
		}

		if !r.Spec.Steps.Equals(oldUO.Spec.Steps) {
			err := fmt.Errorf("steps are immutable: %v", r.Spec.Steps)
			upgradeoperationlog.Info("try to update steps", "old", oldUO.Spec.Steps, "new", r.Spec.Steps)
			allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("steps"), r.Spec.Steps, err.Error()))
		}
	}

	return nil, invalidOrNil(allErrs, r)
}

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