package crd

import (
	"reflect"
	"time"

	"github.com/ngaut/log"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap.com/v1"

	"fmt"

	"github.com/juju/errors"
	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
	apierrs "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/tools/clientcmd"
)

// EnsureCRDExists creates TidbCluster and TidbSet CRD
func EnsureCRDExists(kubeconfig string) error {
	apiextensionsclientset, err := Client(kubeconfig)
	if err != nil {
		return errors.Trace(err)
	}

	tcCRD := &apiextensionsv1beta1.CustomResourceDefinition{
		ObjectMeta: metav1.ObjectMeta{
			Name: fmt.Sprintf("%s.%s", v1.TidbClusterResourcePlural, v1.SchemeGroupVersion.Group),
		},
		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
			Group:   v1.SchemeGroupVersion.Group,
			Version: v1.SchemeGroupVersion.Version,
			Scope:   apiextensionsv1beta1.NamespaceScoped,
			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
				Plural: v1.TidbClusterResourcePlural,
				Kind:   reflect.TypeOf(v1.TidbCluster{}).Name(),
			},
		},
	}
	err = createCRD(apiextensionsclientset, tcCRD)
	if err != nil {
		return errors.Trace(err)
	}

	tsCRD := &apiextensionsv1beta1.CustomResourceDefinition{
		ObjectMeta: metav1.ObjectMeta{
			Name: fmt.Sprintf("%s.%s", v1.TidbSetResourcePlural, v1.SchemeGroupVersion.Group),
		},
		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
			Group:   v1.SchemeGroupVersion.Group,
			Version: v1.SchemeGroupVersion.Version,
			Scope:   apiextensionsv1beta1.NamespaceScoped,
			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
				Plural: v1.TidbSetResourcePlural,
				Kind:   reflect.TypeOf(v1.TidbSet{}).Name(),
			},
		},
	}

	err = createCRD(apiextensionsclientset, tsCRD)
	if err != nil {
		return errors.Trace(err)
	}

	trCRD := &apiextensionsv1beta1.CustomResourceDefinition{
		ObjectMeta: metav1.ObjectMeta{
			Name: fmt.Sprintf("%s.%s", v1.TidbReviewResourcePlural, v1.SchemeGroupVersion.Group),
		},
		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
			Group:   v1.SchemeGroupVersion.Group,
			Version: v1.SchemeGroupVersion.Version,
			Scope:   apiextensionsv1beta1.ClusterScoped,
			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
				Plural: v1.TidbReviewResourcePlural,
				Kind:   reflect.TypeOf(v1.TidbReview{}).Name(),
			},
		},
	}

	err = createCRD(apiextensionsclientset, trCRD)
	if err != nil {
		return errors.Trace(err)
	}

	tcbCRD := &apiextensionsv1beta1.CustomResourceDefinition{
		ObjectMeta: metav1.ObjectMeta{
			Name: fmt.Sprintf("%s.%s", v1.TidbClusterBackupResourcePlural, v1.SchemeGroupVersion.Group),
		},
		Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
			Group:   v1.SchemeGroupVersion.Group,
			Version: v1.SchemeGroupVersion.Version,
			Scope:   apiextensionsv1beta1.NamespaceScoped,
			Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
				Plural: v1.TidbClusterBackupResourcePlural,
				Kind:   reflect.TypeOf(v1.TidbClusterBackup{}).Name(),
			},
		},
	}

	return createCRD(apiextensionsclientset, tcbCRD)
}

func createCRD(clientset *apiextensionsclient.Clientset, crd *apiextensionsv1beta1.CustomResourceDefinition) error {
	_, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
	if err != nil && !apierrs.IsAlreadyExists(err) {
		return errors.Trace(err)
	}
	log.Infof("CRD[%s] created successfully.", crd.Name)
	return WaitForCRDReady(clientset, crd.Name)
}

// WaitForCRDReady wait for CRD being established
func WaitForCRDReady(clientset *apiextensionsclient.Clientset, crdName string) error {
	return wait.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) {
		crd, err := clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
		if err != nil {
			return false, nil
		}

		for _, cond := range crd.Status.Conditions {
			switch cond.Type {
			case apiextensionsv1beta1.Established:
				if cond.Status == apiextensionsv1beta1.ConditionTrue {
					return true, nil
				}
			case apiextensionsv1beta1.NamesAccepted:
				if cond.Status == apiextensionsv1beta1.ConditionFalse {
					log.Warnf("Name conflict: %v\n", cond.Reason)
					return false, nil
				}
			}
		}

		return false, err
	})
}

// Client returns a apiextensions's client
func Client(kubeconfig string) (*apiextensionsclient.Clientset, error) {
	cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		return nil, errors.Trace(err)
	}

	return apiextensionsclient.NewForConfig(cfg)
}
