package v1

import (
	"context"
	"fmt"
	"math/rand"
	"time"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/validation/field"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const charset = "abcdefghijklmnopqrstuvwxyz"

var webhookClient readThroughReader

// +kubebuilder:object:root=false
// +kubebuilder:object:generate=false
type PeeringCreator interface {
	Create(peer *NetworkPeer, sn *Network) error
}

// avoid use client.Object to make guys still need some old version controller-runtime
// can import infra-api
type object interface {
	metav1.Object
	runtime.Object
}

type readThroughReader struct {
	direct client.Reader
	cached client.Reader
}

func (c *readThroughReader) Get(ctx context.Context, key client.ObjectKey, obj object) error {
	if err := c.cached.Get(ctx, key, obj); err != nil {
		if apierrors.IsNotFound(err) {
			return c.direct.Get(ctx, key, obj)
		}
		return err
	}
	return nil
}

func RegisterWebhooks(mgr ctrl.Manager, creator PeeringCreator) error {
	var err error
	webhookClient = readThroughReader{
		cached: mgr.GetClient(),
		direct: mgr.GetAPIReader(),
	}

	if err = (&Account{}).SetupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&ProviderType{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&Provider{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&NetworkPeer{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&Import{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&Network{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&Cluster{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&Backup{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&Restore{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	if err = (&TiCDC{}).setupWebhookWithManager(mgr); err != nil {
		return err
	}
	return nil
}

func invalidOrNil(allErrs field.ErrorList, r object) error {
	if len(allErrs) == 0 {
		return nil
	}
	return apierrors.NewInvalid(r.GetObjectKind().GroupVersionKind().GroupKind(), r.GetName(), allErrs)
}

func badProviderTypeLabel(v string, real string) *field.Error {
	return field.Invalid(
		field.NewPath("metadata").Child("labels").Child(ProviderTypeLabel),
		v,
		fmt.Sprintf("provider type label does not match the real type [%s]", real))
}

func randomStringWithCharset(length int, charset string) string {
	var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
	b := make([]byte, length)
	for i := range b {
		b[i] = charset[seededRand.Intn(len(charset))]
	}
	return string(b)
}

// RandomString generates a random string from a subset of characters
func RandomString(length int) string {
	return randomStringWithCharset(length, charset)
}

type clusterSubresource interface {
	object
	GetClusterRef() types.NamespacedName
}

func validateProviderLabelForClusterSubresource(r clusterSubresource) *field.Error {
	if isMigratedResource(r.GetAnnotations()) {
		return nil
	}

	var c Cluster
	err := webhookClient.Get(context.TODO(), r.GetClusterRef(), &c)
	if err != nil {
		return badProviderTypeLabel(r.GetLabels()[ProviderTypeLabel], fmt.Sprintf("unknown due to error: %s", err.Error()))
	}
	if r.GetLabels()[ProviderTypeLabel] != c.Labels[ProviderTypeLabel] {
		return badProviderTypeLabel(r.GetLabels()[ProviderTypeLabel], c.Labels[ProviderTypeLabel])
	}
	return nil
}

func isMigratedResource(annotations map[string]string) bool {
	if annotations == nil {
		return false
	}

	return annotations[TagLabelPrefix+"migrated-from-central"] == "true"
}

func populateRawExtension(e *runtime.RawExtension) {
	// Even with the field specified as nullable, the API server validates the field and check
	// the existence of the apiVersion and kind as the CRD itself is declared as an embedded resource. Here
	// we perform a defaulting to make sure the two subfields are set when ForProvider is null so it can
	// pass the API server validation.{
	if e.Object == nil && (len(e.Raw) == 0 || string(e.Raw) == `{}`) {
		*e = runtime.RawExtension{
			Object: &runtime.Unknown{
				TypeMeta: runtime.TypeMeta{
					Kind:       "Unknown",
					APIVersion: GroupVersion.String(),
				},
				Raw: []byte(`{
					"apiVersion": "cloud.pingcap.com/v1",
					"kind": "Unknown"
				}`),
			},
		}
	}
}
