package k8s

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"net/url"
	"os"

	httptransport "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
	"github.com/imdario/mergo"
	"github.com/pingcap/errors"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/client-go/kubernetes"
	"k8s.io/utils/pointer"

	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/core"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client/cluster_service"
	"github.com/pingcap/test-infra/sdk/dbaas_central/client/test_token_service"
	"github.com/pingcap/test-infra/sdk/dbaas_central/models"
	"github.com/pingcap/test-infra/sdk/pkg/tidbcloud"
	"github.com/pingcap/test-infra/sdk/pkg/util"
	"github.com/pingcap/test-infra/sdk/resource"
)

var (
	_ resource.Client = TiDBCloudClusterClient{}
)

type tidbCloudConfiguration struct {
	tidbCloudURL      string
	auth0ClientID     string
	auth0ClientSecret string
	auth0Domain       string
	// readonly fields
	cloudProvider string
	region        string
	// monitor
	ThanosQueryURL string
}

type TiDBCloudClusterClient struct {
	*KubeClientSet
	tidbCloudConfiguration *tidbCloudConfiguration
	tidbCloudClient        *client.DbaasCentral
	tidbCloudKubeClientSet TiDBCloudKubeClientSet
}

func (t TiDBCloudClusterClient) Create(spec json.RawMessage) (core.Resource, error) {
	baseKubeClient, err := t.tidbCloudKubeClientSet.getBaseKubeClient()
	if err != nil {
		return nil, err
	}

	resHandle, apiReq, token, err := t.buildTiDBCloudObjects(spec)
	if err != nil {
		return nil, errors.Trace(err)
	}

	resp, err := t.tidbCloudClient.ClusterService.CreateClusterV2(apiReq, httptransport.BearerToken(token))
	if err != nil {
		return nil, errors.Trace(err)
	}
	if resp.GetPayload().BaseResp.ErrCode != "0" {
		return nil, fmt.Errorf("create tidbcloud cluster error: %+v", resp.GetPayload().BaseResp)
	}
	resHandle.Status.ClusterID = *resp.GetPayload().ID

	clusterConfig, err := t.buildTiDBCloudClusterConfigObject(resHandle)
	if err != nil {
		return nil, errors.Trace(err)
	}
	_, err = baseKubeClient.DynamicClient.
		Resource(clusterConfigGVR).Create(context.TODO(), clusterConfig, metav1.CreateOptions{})
	if err != nil {
		return nil, errors.Annotate(err, "create cluster-config")
	}

	clusterConfigSpec, _ := json.Marshal(resHandle.Spec.ClusterConfig)
	bClusterConfig := new(bytes.Buffer)
	if err := unstructured.UnstructuredJSONScheme.Encode(clusterConfig, bClusterConfig); err != nil {
		return nil, errors.Trace(err)
	}
	shootKubeConfig, err := t.tidbCloudKubeClientSet.GetShootKubeConfig(context.TODO(), resHandle.Spec.ProjectID, t.tidbCloudConfiguration.cloudProvider, t.tidbCloudConfiguration.region)
	if err != nil {
		return nil, err
	}
	err = createBookSecret(
		t.ClientSet,
		t.Namespace,
		resHandle.Name,
		shootKubeConfig,
		t.tidbCloudConfiguration.tidbCloudURL,
		resHandle.Spec.AccountEmail,
		resHandle.Spec.OrgID,
		resHandle.Spec.ProjectID,
		t.tidbCloudConfiguration.region,
		token,
		resHandle.Status.ClusterID,
		resHandle.Spec.Password,
		resHandle.Spec.Connect,
		t.tidbCloudConfiguration.ThanosQueryURL,
		clusterConfigSpec,
		bClusterConfig.Bytes(),
	)
	if err != nil {
		return nil, err
	}

	cluster := &TiDBCloudCluster{
		KubeClientSet:    *t.KubeClientSet,
		TiDBCloudCluster: *resHandle,
		tidbCloudClient:  t.tidbCloudClient,
	}
	cluster.Provider = t.FQDN()

	if err := cluster.Sync(); err != nil {
		return nil, err
	}
	return cluster, nil
}

func (t TiDBCloudClusterClient) authToken(accountEmail string) (string, error) {
	resp, err := t.tidbCloudClient.TestTokenService.TestTokenServiceGenerateTestToken(&test_token_service.TestTokenServiceGenerateTestTokenParams{
		Body: &models.CentralGenerateTestTokenReq{
			Auth0ClientID:     t.tidbCloudConfiguration.auth0ClientID,
			Auth0ClientSecret: t.tidbCloudConfiguration.auth0ClientSecret,
			Auth0Domain:       t.tidbCloudConfiguration.auth0Domain,
			Email:             accountEmail,
		},
		Context: context.TODO(),
	}, nil)
	if err != nil {
		return "", err
	}
	return resp.GetPayload().Token, nil
}

func createBookSecret(clientSet kubernetes.Interface,
	namespace string,
	name string,
	shootKubeConfig []byte,
	tidbCloudURL string,
	accountEmail string,
	orgID string,
	projectID string,
	region string,
	authToken string,
	clusterID string,
	password string,
	connect apiresource.TiDBCloudClusterConnect,
	prometheusURL string,
	clusterConfigSpec []byte,
	clusterConfigOverlaid []byte,
) error {
	bookSecret := corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: fmt.Sprintf("tidbcloud-%s", name),
			Labels: map[string]string{
				"app.kubernetes.io/managed-by": "test-infra",
				"app.kubernetes.io/name":       "tidbcloud_cluster",
			},
			Finalizers: []string{tidbCloudClusterFinalizer},
		},
		Data: map[string][]byte{
			"shoot_kubeconfig":      shootKubeConfig,
			"tidb_cloud_url":        []byte(tidbCloudURL),
			"account_email":         []byte(accountEmail),
			"org_id":                []byte(orgID),
			"project_id":            []byte(projectID),
			"region":                []byte(region),
			"auth_token":            []byte(authToken),
			"cluster_id":            []byte(clusterID),
			"cluster_root_password": []byte(password),
			"cluster_connect":       []byte(connect),
			"prometheus_url":        []byte(prometheusURL),
		},
	}
	if len(clusterConfigSpec) != 0 {
		bookSecret.Data["cluster_config_spec"] = clusterConfigSpec
	}
	if len(clusterConfigOverlaid) != 0 {
		bookSecret.Data["cluster_config"] = clusterConfigOverlaid
	}
	_, err := clientSet.CoreV1().Secrets(namespace).Create(context.TODO(), &bookSecret, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("create book secret error: %v", err)
	}
	return nil
}

func (t TiDBCloudClusterClient) buildTiDBCloudObjects(params json.RawMessage) (*apiresource.TiDBCloudCluster, *cluster_service.CreateClusterV2Params, string, error) {
	resHandle := &apiresource.TiDBCloudCluster{Spec: apiresource.TiDBCloudClusterSpec{}}
	if err := json.Unmarshal(params, resHandle); err != nil {
		return nil, nil, "", err
	}
	if err := resHandle.Validate(); err != nil {
		return nil, nil, "", err
	}
	authToken, err := t.authToken(resHandle.Spec.AccountEmail)
	if err != nil {
		return nil, nil, "", err
	}
	dbNodeProfile, err := t.getNodeProfile(models.CentralComponentTypeCOMPONENTTYPETIDB, resHandle.Spec.OrgID, string(resHandle.Spec.Provider), resHandle.Spec.TiDB.NodeProfileName, authToken)
	if err != nil {
		return nil, nil, "", err
	}
	kvNodeProfile, err := t.getNodeProfile(models.CentralComponentTypeCOMPONENTTYPETIKV, resHandle.Spec.OrgID, string(resHandle.Spec.Provider), resHandle.Spec.TiDB.NodeProfileName, authToken)
	if err != nil {
		return nil, nil, "", err
	}
	req := &cluster_service.CreateClusterV2Params{
		Context: context.TODO(),
		Body: &models.CentralCreateClusterReq{
			Components: []*models.CentralClusterComponent{
				{
					ComponentType: models.CentralComponentTypeCOMPONENTTYPETIDB.Pointer(),
					NodeProfileID: pointer.String(dbNodeProfile.ID),
					Replicas:      pointer.Int32(resHandle.Spec.TiDB.Replicas),
					StorageSizeGi: pointer.Int32(0),
				},
				{
					ComponentType: models.CentralComponentTypeCOMPONENTTYPETIKV.Pointer(),
					NodeProfileID: pointer.String(kvNodeProfile.ID),
					Replicas:      pointer.Int32(resHandle.Spec.TiKV.Replicas),
					StorageSizeGi: pointer.Int32(resHandle.Spec.TiKV.StorageSizeGi),
				},
			},
			IsDevTier:    pointer.Bool(false),
			Name:         pointer.String(genClusterFullQualifiedName(t.Namespace, resHandle.Name)),
			OrgID:        pointer.String(resHandle.Spec.OrgID),
			ProjectID:    pointer.String(resHandle.Spec.ProjectID),
			Provider:     pointer.String(string(resHandle.Spec.Provider)),
			Region:       pointer.String(t.tidbCloudConfiguration.region),
			RootPassword: resHandle.Spec.Password,
			// FIXME: make port customize
			Port: pointer.Int32(4000),
		},
		OrgID:     resHandle.Spec.OrgID,
		ProjectID: resHandle.Spec.ProjectID,
	}
	return resHandle, req, authToken, nil
}

func genClusterFullQualifiedName(namespace, clusterName string) string {
	return fmt.Sprintf("%s-%s", namespace, clusterName)
}

func (t TiDBCloudClusterClient) Get(handle apiresource.ResourceHandle) (core.Resource, error) {
	cluster := &TiDBCloudCluster{
		KubeClientSet:   *t.KubeClientSet,
		tidbCloudClient: t.tidbCloudClient,
		TiDBCloudCluster: apiresource.TiDBCloudCluster{
			ResourceHandle: handle,
		},
	}
	cluster.Provider = t.FQDN()
	if err := cluster.Sync(); err != nil {
		return nil, err
	}
	return cluster, nil
}

func (t TiDBCloudClusterClient) GetEndpoints(handle apiresource.ResourceHandle) (apiresource.ResourceEndpoints, error) {
	return nil, nil
}

func (t TiDBCloudClusterClient) Delete(handle apiresource.ResourceHandle) error {
	bookSecret, err := t.KubeClientSet.ClientSet.CoreV1().Secrets(t.Namespace).Get(context.TODO(), fmt.Sprintf("tidbcloud-%s", handle.Name), metav1.GetOptions{})
	if err != nil {
		return errors.Trace(err)
	}
	clusterID := string(bookSecret.Data["cluster_id"])
	token := string(bookSecret.Data["auth_token"])
	orgID := string(bookSecret.Data["org_id"])
	projectID := string(bookSecret.Data["project_id"])

	_, err = t.tidbCloudClient.ClusterService.DeleteCluster(&cluster_service.DeleteClusterParams{
		ClusterID: clusterID,
		OrgID:     orgID,
		ProjectID: projectID,
		Context:   context.TODO(),
	}, httptransport.BearerToken(token))
	if err != nil {
		return errors.Trace(err)
	}

	baseKubeClient, err := t.tidbCloudKubeClientSet.getBaseKubeClient()
	if err != nil {
		return errors.Trace(err)
	}
	if err := baseKubeClient.DynamicClient.Resource(clusterConfigGVR).
		Delete(context.TODO(), util.TiDBCloudClusterConfigName(orgID, clusterID), metav1.DeleteOptions{}); err != nil {
		return fmt.Errorf("delete cluster configs error: %v", err)
	}
	return nil
}

func (t TiDBCloudClusterClient) ReserveEnv(handle apiresource.ResourceHandle, action apiresource.GcReserveEnvAction) error {
	return nil
}

func (t TiDBCloudClusterClient) Artifact(handle apiresource.ResourceHandle, action apiresource.GcArtifactAction, archiver resource.ArchiverFunc) (map[string]*url.URL, error) {
	return nil, nil
}

func (t TiDBCloudClusterClient) FQDN() string {
	return resource.FQDN(t)
}

func (t TiDBCloudClusterClient) getNodeProfile(componentName models.CentralComponentType, orgID, provider, nodeProfileName, token string) (*models.CentralClusterNodeProfile, error) {
	resp, err := t.tidbCloudClient.ClusterService.CalcNodeProfiles(&cluster_service.CalcNodeProfilesParams{
		Body: &models.CentralCalcNodeProfilesReq{
			IsDevTier: pointer.Bool(false),
			OrgID:     pointer.String(orgID),
			Provider:  pointer.String(provider),
			Region:    pointer.String(t.tidbCloudConfiguration.region),
		},
		Context: context.TODO(),
	}, httptransport.BearerToken(token))
	if err != nil {
		return nil, err
	}
	if resp.GetPayload().BaseResp.ErrCode != "0" {
		return nil, fmt.Errorf("%s", resp.GetPayload().BaseResp.ErrMsg)
	}
	for _, np := range resp.GetPayload().NodeProfiles {
		if *np.ComponentType != componentName {
			continue
		}
		for _, p := range np.Profiles {
			if p.DisplayName == nodeProfileName {
				return p, nil
			}
		}
		break
	}
	return nil, fmt.Errorf("node profile %s[%s] not found", componentName, nodeProfileName)
}

func (t TiDBCloudClusterClient) buildTiDBCloudClusterConfigObject(resHandle *apiresource.TiDBCloudCluster) (*unstructured.Unstructured, error) {
	var cc = new(unstructured.Unstructured) // the ClusterConfig object

	baseKubeClient, err := t.tidbCloudKubeClientSet.getBaseKubeClient()
	if err != nil {
		return nil, errors.Trace(err)
	}

	baseClusterConfig, err := baseKubeClient.DynamicClient.
		Resource(clusterConfigGVR).Get(context.TODO(), apiresource.TiDBCloudBaseClusterConfig, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	cc.SetAPIVersion(baseClusterConfig.GetAPIVersion())
	cc.SetKind(baseClusterConfig.GetKind())
	cc.SetName(util.TiDBCloudClusterConfigName(resHandle.Spec.OrgID, resHandle.Status.ClusterID))
	ccLabels := baseClusterConfig.GetLabels()
	if err := mergo.Merge(
		&ccLabels,
		map[string]string{
			apiresource.TiDBCloudClusterLabel: resHandle.Status.ClusterID,
			"tidbcloud.com/managed-by":        "test-infra",
			apiresource.TiDBCloudTenentLabel:  resHandle.Spec.OrgID,
		},
		mergo.WithOverride); err != nil {
		return nil, err
	}
	cc.SetLabels(ccLabels)
	spec, found, err := unstructured.NestedFieldCopy(baseClusterConfig.Object, "spec")
	if err != nil {
		return nil, errors.Trace(err)
	}
	if !found {
		return nil, fmt.Errorf("tidbcloud base cluster config without spec field")
	}
	spec.(map[string]interface{})["configMeta"].(map[string]interface{})["gitCommitID"] = "hacked-config-by-test-infra"
	if err := unstructured.SetNestedField(cc.Object, spec, "spec"); err != nil {
		return nil, err
	}

	return applyClusterConfig(resHandle.Spec.ClusterConfig, cc)
}

func NewTiDBCloudClusterClient(k *KubeClientSet, config *tidbcloud.TiDBCloudConfiguration) (*TiDBCloudClusterClient, error) {
	var tidbCloudConfig *tidbCloudConfiguration
	var tidbCloudClient *client.DbaasCentral
	var tidbCloudKubeClient TiDBCloudKubeClientSet

	// init tidbCloudClient here, otherwise init it on time
	if config != nil {
		var err error
		tidbCloudConfig = &tidbCloudConfiguration{
			tidbCloudURL:      config.TiDBCloudURL,
			auth0ClientID:     config.Auth0ClientID,
			auth0ClientSecret: config.Auth0ClientSecret,
			auth0Domain:       config.Auth0Domain,
			cloudProvider:     config.CloudProvider,
			region:            config.Region,
			ThanosQueryURL:    config.ThanosURL,
		}
		tidbCloudClient = client.NewHTTPClientWithConfig(strfmt.Default, client.DefaultTransportConfig().WithHost(config.TiDBCloudURL).WithSchemes([]string{"https"}))
		tidbCloudKubeClient, err = NewTiDBCloudKubeClientSet(config)
		if err != nil {
			return nil, err
		}
	}

	return &TiDBCloudClusterClient{
		KubeClientSet:          k,
		tidbCloudConfiguration: tidbCloudConfig,
		tidbCloudClient:        tidbCloudClient,
		tidbCloudKubeClientSet: tidbCloudKubeClient,
	}, nil
}

type clusterGetter struct{ handle apiresource.ResourceHandle }

func (t *clusterGetter) Get() (core.Resource, error) {
	k, err := NewKubeClientFromKubeConfig(t.handle.Testbed, os.Getenv("KUBECONFIG"))
	if err != nil {
		return nil, err
	}
	client, err := NewTiDBCloudClusterClient(k, nil)
	if err != nil {
		return nil, err
	}
	return client.Get(t.handle)
}

func init() {
	core.RegisterProvider(resource.FQDN(TiDBCloudClusterClient{}), func(handle apiresource.ResourceHandle) core.ResourceGetter {
		return &clusterGetter{handle: handle}
	})
}
