package core

import (
	"context"
	"fmt"
	"net"
	"strconv"
	"time"

	runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
	"github.com/crossplane/crossplane-runtime/pkg/resource"
	openapiruntime "github.com/go-openapi/runtime"
	httptransport "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
	tidbcloud_client "github.com/pingcap/dbaas/central/proto/output/client"
	"github.com/pingcap/dbaas/central/proto/output/client/auth_service"
	"github.com/pingcap/dbaas/central/proto/output/client/cluster_service"
	"github.com/pingcap/dbaas/central/proto/output/models"
	"github.com/pkg/errors"
	"github.com/tidbcloud/tidbcloud-provider/api/v1alpha1"
	"github.com/tidbcloud/tidbcloud-provider/pkg/scheme"
	"github.com/tidbcloud/tidbcloud-provider/pkg/util"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/discovery"
	memory "k8s.io/client-go/discovery/cached"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/restmapper"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	gardenAdminNamespace  = "garden"
	gardenAdminSecretName = "garden-kubeconfig-for-admin"

	machineTypeUnknown = "unknown"
)

type ClusterController struct{}

func ConditionUpgrading() runtimev1alpha1.Condition {
	return runtimev1alpha1.Condition{
		Type:               runtimev1alpha1.TypeReady,
		Status:             corev1.ConditionFalse,
		Reason:             "ClusterUpgrading",
		LastTransitionTime: metav1.Now(),
	}
}

func ConditionScaling() runtimev1alpha1.Condition {
	return runtimev1alpha1.Condition{
		Type:               runtimev1alpha1.TypeReady,
		Status:             corev1.ConditionFalse,
		Reason:             "ClusterScaling",
		LastTransitionTime: metav1.Now(),
	}
}

func (c *ClusterController) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		Named(v1alpha1.ClusterKindAPIVersion).
		For(&v1alpha1.Cluster{}).
		Complete(managed.NewReconciler(mgr,
			resource.ManagedKind(v1alpha1.ClusterGroupVersionKind),
			managed.WithExternalConnecter(&clusterConnector{client: mgr.GetClient()})))
}

type clusterConnector struct {
	client client.Client
}

func (c *clusterConnector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) {
	i, ok := mg.(*v1alpha1.Cluster)
	if !ok {
		return nil, errors.New("managed resource is not a Cluster")
	}

	provider := &v1alpha1.ProviderConfig{}
	if err := c.client.Get(ctx, types.NamespacedName{Name: i.Spec.ProviderConfigReference.Name}, provider); err != nil {
		return nil, errors.Wrap(err, "cannot get ProviderConfig")
	}

	providerSecret := &corev1.Secret{}
	secretKey := provider.Spec.Credentials.SecretRef
	if err := c.client.Get(ctx, types.NamespacedName{Namespace: secretKey.Namespace, Name: secretKey.Name}, providerSecret); err != nil {
		return nil, errors.Wrap(err, "cannot get provider credentials")
	}

	cli := tidbcloud_client.New(httptransport.New(provider.Spec.Endpoint, "", []string{"https"}), strfmt.Default)
	token, ok := providerSecret.Data[secretKey.Key]
	if !ok {
		return nil, errors.New(fmt.Sprintf("cannot find token key %s in provider credentials", secretKey.Key))
	}
	auth := httptransport.BearerToken(string(token))

	gardenSecret := &corev1.Secret{}
	if err := c.client.Get(ctx, types.NamespacedName{Namespace: gardenAdminNamespace, Name: gardenAdminSecretName}, gardenSecret); err != nil {
		return nil, errors.Wrap(err, "cannot get gardener kubeconfig")
	}
	kubeCli, err := util.KubeClientFromSecret(gardenSecret)
	if err != nil {
		return nil, errors.Wrap(err, "cannot get gardener kubernetes client")
	}

	return &clusterExternal{Client: cli, Auth: auth, KubeCli: c.client, Gardener: kubeCli, Project: provider.Spec.Project}, nil
}

type clusterExternal struct {
	Client   *tidbcloud_client.CentralService
	Auth     openapiruntime.ClientAuthInfoWriter
	KubeCli  client.Client
	Gardener kubernetes.Interface
	Project  string
}

func (c *clusterExternal) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) {
	r, ok := mg.(*v1alpha1.Cluster)
	if !ok {
		return managed.ExternalObservation{}, errors.New("Managed resource is not a Cluster")
	}

	// TODO: move the webhook-based defaulting
	if r.Spec.Parameters.Name == "" {
		// generate the database name
		r.Spec.Parameters.Name = fmt.Sprintf("%s%s", r.Spec.Parameters.GenerateName, util.RandString(6))
		if err := c.KubeCli.Update(ctx, r); err != nil {
			return managed.ExternalObservation{}, errors.Wrap(err, "error generate name for cluster")
		}
		// anyway, reconcile in next loop
		return managed.ExternalObservation{}, errors.New("name generated for cluster, reconciling...")
	}

	var existing *models.CentralCluster
	if r.Status.ID != "" {
		resp, err := c.Client.ClusterService.GetCluster(
			cluster_service.NewGetClusterParams().WithID(r.Status.ID),
			c.Auth)
		if err == nil {
			existing = resp.Payload
		} else if !util.Is404(err) {
			return managed.ExternalObservation{}, errors.Wrap(err, "error calling TiDB Cloud API")
		}
	}

	if existing == nil {
		return managed.ExternalObservation{ResourceExists: false}, nil
	}

	r.Status.Status = string(existing.Status)
	if len(existing.Endpoint.Address) < 1 {
		r.Status.Endpoint = "Pending"
	} else {
		r.Status.Host = existing.Endpoint.Address
		r.Status.Port = existing.Endpoint.Port
		r.Status.Endpoint = fmt.Sprintf("%s:%d", existing.Endpoint.Address, existing.Endpoint.Port)
	}
	if existing.ClusterComponents != nil {
		r.Status.TiDB.CPU = fmt.Sprintf("%.0fc", existing.ClusterComponents.Tidb.NodeProfile.Compute.CPU)
		r.Status.TiDB.CurrentReplicas = existing.ClusterComponents.Tidb.Replicas
		r.Status.TiDB.Summary = fmt.Sprintf("%d * %s", r.Status.TiDB.CurrentReplicas, r.Status.TiDB.CPU)
		r.Status.TiKV.CPU = fmt.Sprintf("%.0fc", existing.ClusterComponents.Tikv.NodeProfile.Compute.CPU)
		r.Status.TiKV.CurrentReplicas = existing.ClusterComponents.Tikv.Replicas
		r.Status.TiKV.Summary = fmt.Sprintf("%d * %s", r.Status.TiKV.CurrentReplicas, r.Status.TiKV.CPU)
	}
	if r.Status.TiDB.MachineType == "" {
		profiles, err := c.Client.ClusterService.Profiles(
			cluster_service.NewProfilesParams().WithProvider(r.Spec.Parameters.Provider), c.Auth)
		if err != nil {
			return managed.ExternalObservation{}, errors.Wrap(err, "error fetching profiles")
		}
		var profile *models.CentralInstance
		for _, v := range profiles.Payload.Instances {
			if v.ProfileName == r.Spec.Parameters.Tier {
				profile = v
				break
			}
		}
		if profile == nil {
			r.Status.TiDB.MachineType = machineTypeUnknown
			r.Status.TiKV.MachineType = machineTypeUnknown
		} else {
			r.Status.TiDB.MachineType = profile.Tidb.InstanceType
			r.Status.TiKV.MachineType = profile.Tikv.InstanceType
		}
	}

	switch r.Status.Status {
	case string(models.CentralClusterStatusNORMAL):
		if r.Status.Initialized {
			r.SetConditions(runtimev1alpha1.Available())
		} else {
			r.SetConditions(ConditionUpgrading())
		}
	case string(models.CentralClusterStatusUPGRADING):
		r.SetConditions(ConditionUpgrading())
	case string(models.CentralClusterStatusSCALING):
		r.SetConditions(ConditionScaling())
	case string(models.CentralClusterStatusCREATING), string(models.CentralClusterStatusRECOVERING):
		r.SetConditions(runtimev1alpha1.Creating())
	case string(models.CentralClusterStatusUNKNOWN), string(models.CentralClusterStatusUNAVAILABLE):
		r.SetConditions(runtimev1alpha1.Unavailable())
	}

	needUpdate := (r.Status.Status == string(models.CentralClusterStatusNORMAL)) && !r.Status.Initialized
	return managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: !needUpdate}, nil
}

func (c *clusterExternal) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) {
	r, ok := mg.(*v1alpha1.Cluster)
	if !ok {
		return managed.ExternalCreation{}, errors.New("managed resource is not a Cluster")
	}

	r.SetConditions(runtimev1alpha1.Creating())

	p := r.Spec.Parameters
	var password string
	if r.Spec.Parameters.InitPassword != nil {
		password = *r.Spec.Parameters.InitPassword
	} else {
		password = util.RandString(8)
	}

	cd := managed.ConnectionDetails{runtimev1alpha1.ResourceCredentialsSecretPasswordKey: []byte(password)}

	// TODO: support tiflash
	created, err := c.Client.ClusterService.CreateCluster(cluster_service.NewCreateClusterParams().
		WithBody(&models.CentralClusterCreateReq{
			Name:         p.Name,
			ProfileName:  p.Tier,
			Provider:     p.Provider,
			Region:       p.Region,
			RootPassword: password,
			TidbCount:    p.TiDB.Replicas,
			TikvCount:    p.TiKV.Replicas,
		}), c.Auth)

	if err != nil {
		return managed.ExternalCreation{}, errors.Wrap(err, "error creating cluster in TiDB Cloud")
	}
	r.Status.ID = created.Payload.ID
	return managed.ExternalCreation{ConnectionDetails: cd}, nil
}

func (c *clusterExternal) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) {
	r, ok := mg.(*v1alpha1.Cluster)
	if !ok {
		return managed.ExternalUpdate{}, errors.New("managed resource is not a Cluster")
	}

	if r.Status.Status != string(models.CentralClusterStatusNORMAL) {
		return managed.ExternalUpdate{}, errors.New(fmt.Sprintf("cannot update cluster since status is %s", r.Status.Status))
	}

	// if cluster is initialized, simply skip now
	// TODO: support scaling and upgrading
	if r.Status.Initialized {
		return managed.ExternalUpdate{}, nil
	}

	// Expose kubeconfig
	if r.Status.KubeconfigSecret == nil {
		// FIXME: internal use only, should not expose publicly
		if err := c.exposeKubeconfig(ctx, r); err != nil {
			return managed.ExternalUpdate{}, errors.Wrap(err, "error exposing kubeconfig of cluster")
		}
	}

	if err := c.customizeVersionAndConfig(ctx, r); err != nil {
		return managed.ExternalUpdate{}, errors.Wrap(err, "error customize version and config")
	}
	r.Status.Status = string(models.ClusterStatusUPGRADING)

	if err := c.updateTrafficFilter(ctx, r); err != nil {
		return managed.ExternalUpdate{}, errors.Wrap(err, "error update source ranges")
	}

	if _, err := net.DialTimeout("tcp", r.Status.Endpoint, 5*time.Second); err != nil {
		return managed.ExternalUpdate{}, errors.Wrap(err, "cluster endpoint not ready")
	}

	r.Status.Initialized = true

	return managed.ExternalUpdate{}, nil
}

func (c *clusterExternal) Delete(ctx context.Context, mg resource.Managed) error {
	r, ok := mg.(*v1alpha1.Cluster)
	if !ok {
		return errors.New("managed resource is not a Cluster")
	}
	r.SetConditions(runtimev1alpha1.Deleting())

	_, err := c.Client.ClusterService.DeleteCluster(
		cluster_service.NewDeleteClusterParams().WithID(r.Status.ID),
		c.Auth)

	return errors.Wrap(resource.Ignore(util.Is404, err), "cannot delete cluster")
}

func (c *clusterExternal) exposeKubeconfig(ctx context.Context, r *v1alpha1.Cluster) error {
	metaOK, err := c.Client.AuthService.Meta(auth_service.NewMetaParams(), c.Auth)
	if err != nil {
		return errors.Wrap(err, "error get tenant id")
	}
	myMeta := metaOK.Payload
	tenantId, err := strconv.ParseInt(myMeta.TenantID, 10, 64)
	if err != nil {
		return errors.Wrap(err, "error format tenant id")
	}
	shootName := util.ShootName(tenantId, r.Spec.Parameters.Provider, r.Spec.Parameters.Region)
	shootNs := util.ShootNamespace(c.Project)
	shootKubeconfig, err := c.Gardener.CoreV1().Secrets(shootNs).Get(ctx, fmt.Sprintf("%s.kubeconfig", shootName), metav1.GetOptions{})
	if err != nil {
		return errors.Wrap(err, "error get shoot kubeconfig for cluster")
	}

	shootKubeconfigSecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: r.GetNamespace(),
			Name:      fmt.Sprintf("tidbcloud-%s-kubeconfig", r.GetName()),
		},
		Data: shootKubeconfig.Data,
	}
	if err := controllerutil.SetOwnerReference(r, shootKubeconfigSecret, scheme.Scheme); err != nil {
		return errors.Wrap(err, "error set ownerReference of kubeconfig")
	}
	if err := c.KubeCli.Create(ctx, shootKubeconfigSecret); err != nil {
		return errors.Wrap(err, "error create cluster kubeconfig")
	}
	r.Status.KubeconfigSecret = &runtimev1alpha1.SecretReference{
		Name:      shootKubeconfigSecret.Name,
		Namespace: shootKubeconfigSecret.Namespace,
	}
	return nil
}

func (c *clusterExternal) updateTrafficFilter(ctx context.Context, r *v1alpha1.Cluster) error {
	var cidrs []*models.CentralTrafficFilter
	if len(r.Spec.Parameters.SourceRanges) < 1 {
		cidrs = append(cidrs, &models.CentralTrafficFilter{Cidr: "0.0.0.0/0"})
	}
	for _, sourceRange := range r.Spec.Parameters.SourceRanges {
		cidrs = append(cidrs, &models.CentralTrafficFilter{Cidr: sourceRange})
	}
	params := cluster_service.NewUpdateTrafficFilterParams().WithBody(
		&models.CentralUpdateTrafficFilterReq{
			TrafficFilters: &models.CentralTrafficFilterList{Cidrs: cidrs},
		})
	_, err := c.Client.Transport.Submit(&openapiruntime.ClientOperation{
		ID:                 "UpdateTrafficFilter",
		Method:             "POST",
		PathPattern:        fmt.Sprintf("/api/v1/clusters/%s/traffic_filter", r.Status.ID),
		ProducesMediaTypes: []string{"application/json"},
		ConsumesMediaTypes: []string{"application/json"},
		Schemes:            []string{"https"},
		Params:             params,
		Reader:             &cluster_service.UpdateTrafficFilterReader{},
		AuthInfo:           c.Auth,
		Context:            params.Context,
		Client:             params.HTTPClient,
	})
	return err
}

func (c *clusterExternal) customizeVersionAndConfig(ctx context.Context, r *v1alpha1.Cluster) error {
	if r.Status.KubeconfigSecret == nil {
		return errors.New("Target cluster's kubeconfig is not initialized")
	}
	dbKubeSecret := &corev1.Secret{}
	if err := c.KubeCli.Get(ctx, types.NamespacedName{Namespace: r.Status.KubeconfigSecret.Namespace, Name: r.Status.KubeconfigSecret.Name}, dbKubeSecret); err != nil {
		return errors.Wrap(err, "cannot get kubeconfig secret")
	}
	restConfig, err := util.RestConfigFromSecret(dbKubeSecret)
	if err != nil {
		return errors.Wrap(err, "cannot get rest config from database's kubeconfig")
	}
	disc, err := discovery.NewDiscoveryClientForConfig(restConfig)
	if err != nil {
		return errors.Wrap(err, "build restMapper")
	}
	mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disc))
	dc, err := dynamic.NewForConfig(restConfig)
	if err != nil {
		return errors.Wrap(err, "build dynamic client")
	}
	tcGVR, err := mapper.RESTMapping(schema.GroupKind{
		Group: "pingcap.com",
		Kind:  "TidbCluster",
	}, "v1alpha1")
	if err != nil {
		return errors.Wrap(err, "build TidbCluster group versioned resource")
	}
	tcAPI := dc.Resource(tcGVR.Resource).Namespace(fmt.Sprintf("tidb%s", r.Status.ID))
	tc, err := tcAPI.Get(ctx, "db", metav1.GetOptions{})
	if err != nil {
		return errors.Wrap(err, "get tc")
	}
	spec, err := util.GetChild(tc.Object, "spec")
	if err != nil {
		return errors.Wrap(err, "get .spec from tc")
	}

	spec["version"] = r.Spec.Parameters.Version
	if err := componentOverlay(spec, "tidb", &r.Spec.Parameters.TiDB); err != nil {
		return errors.Wrap(err, "override tidb spec")
	}
	if err := componentOverlay(spec, "tikv", &r.Spec.Parameters.TiKV); err != nil {
		return errors.Wrap(err, "override tikv spec")
	}
	if err := componentOverlay(spec, "pd", r.Spec.Parameters.PD); err != nil {
		return errors.Wrap(err, "override pd spec")
	}
	if err := componentOverlay(spec, "tiflash", r.Spec.Parameters.TiFlash); err != nil {
		return errors.Wrap(err, "override tiflash spec")
	}
	_, err = tcAPI.Update(ctx, tc, metav1.UpdateOptions{})
	return err
}

func componentOverlay(spec map[string]interface{}, name string, comp *v1alpha1.ComponentParameters) error {
	if comp == nil {
		return nil
	}
	compSpec, err := util.GetChild(spec, name)
	if err != nil {
		return errors.Wrap(err, fmt.Sprintf("get %s from .spec", name))
	}
	if comp.VersionOverride != nil {
		compSpec["version"] = *comp.VersionOverride
	}
	if comp.BaseImage != nil && len(*comp.BaseImage) > 0 {
		compSpec["baseImage"] = *comp.BaseImage
	}
	if comp.RawConfig != nil && len(*comp.RawConfig) > 0 {
		if compSpec["config"] == nil {
			compSpec["config"] = map[string]interface{}{}
		}
		if err := util.OverlayToml(compSpec, *comp.RawConfig, "config"); err != nil {
			return errors.Wrap(err, "overlay config")
		}
	}
	return nil
}
