package k8s

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

	"github.com/BurntSushi/toml"
	"github.com/imdario/mergo"
	"github.com/pingcap/errors"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/chart/loader"
	"helm.sh/helm/v3/pkg/kube"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"

	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/core"
	"github.com/pingcap/test-infra/sdk/resource"
	sdkresource "github.com/pingcap/test-infra/sdk/resource"
)

var (
	_ sdkresource.Client = TiDBDrainerClient{}
)

var (
	//go:embed assets/tidb-drainer/Chart.yaml
	chart []byte
	//go:embed assets/tidb-drainer/values.yaml
	values []byte
	//go:embed assets/tidb-drainer/templates/_helpers.tpl
	templatesHelpers []byte
	//go:embed assets/tidb-drainer/templates/drainer-service.yaml
	templatesService []byte
	//go:embed assets/tidb-drainer/templates/drainer-configmap.yaml
	templatesConfigmap []byte
	//go:embed assets/tidb-drainer/templates/drainer-statefulset.yaml
	templatesStatefulset []byte
	//go:embed assets/tidb-drainer/templates/scripts/_start_drainer.sh.tpl
	scriptStartSh []byte
	//go:embed assets/tidb-drainer/templates/NOTES.txt
	templatesNotes []byte

	defaultDrainerValues map[string]interface{}
)

type drainerConfigSnippet struct {
	SyncerCfg struct {
		To struct {
			Host string `toml:"host" json:"host"`
			Port int    `toml:"port" json:"port"`
		} `toml:"to" json:"to"`
	} `toml:"syncer" json:"sycner"`
}

type TiDBDrainerClient struct {
	*KubeClientSet
}

func NewTiDBDrainerClient(k *KubeClientSet) TiDBDrainerClient {
	return TiDBDrainerClient{k}
}

func (t TiDBDrainerClient) Create(spec json.RawMessage) (core.Resource, error) {
	actionConfig := new(action.Configuration)

	if err := actionConfig.Init(kube.GetConfig(t.kubeConfig, "", t.Namespace), t.Namespace, "configmap", sugar.Infof); err != nil {
		sugar.Errorf("init the helm action config error: %v", err)
		return nil, err
	}
	apiRes, err := t.buildTiDBDrainerValueObject(spec)
	if err != nil {
		return nil, err
	}
	chart, err := loader.LoadFiles([]*loader.BufferedFile{
		{Name: "Chart.yaml", Data: chart},
		{Name: "values.yaml", Data: values},
		{Name: "templates/_helpers.tpl", Data: templatesHelpers},
		{Name: "templates/NOTES.txt", Data: templatesNotes},
		{Name: "templates/drainer-service.yaml", Data: templatesService},
		{Name: "templates/drainer-configmap.yaml", Data: templatesConfigmap},
		{Name: "templates/drainer-statefulset.yaml", Data: templatesStatefulset},
		{Name: "templates/scripts/_start_drainer.sh.tpl", Data: scriptStartSh},
	})
	if err != nil {
		return nil, err
	}
	client := action.NewInstall(actionConfig)
	client.Namespace = t.Namespace
	client.ReleaseName = apiRes.Name
	client.Version = "v1.2.4" // v1.2.4 is the latest version of pingcap/tidb-drainer

	// merge default values with override values
	values := runtime.DeepCopyJSON(defaultDrainerValues)
	data, err := json.Marshal(apiRes.Spec)
	if err != nil {
		return nil, err
	}
	overrideValues := make(map[string]interface{})
	if err := json.Unmarshal(data, &overrideValues); err != nil {
		return nil, err
	}
	// patch resources.limits equal witch resources.requests
	overrideValues["resources"].(map[string]interface{})["limits"] = overrideValues["resources"].(map[string]interface{})["requests"]
	if err := mergo.Merge(&values, overrideValues, mergo.WithOverride); err != nil {
		return nil, err
	}
	// set the drainerName(sts name uses it) here, the default is clusterName-ReleaseName-drainer if no set, that's too long
	values["drainerName"] = apiRes.Name

	// set the syncToMySQLAddr hint to avoid drainer crash loop because of the un-ready of sync-to cluster
	var drainerConfigSnippet = drainerConfigSnippet{}
	if err := toml.Unmarshal([]byte(apiRes.Spec.Config), &drainerConfigSnippet); err != nil {
		return nil, err
	}
	if drainerConfigSnippet.SyncerCfg.To.Host != "" && drainerConfigSnippet.SyncerCfg.To.Port != 0 {
		values["syncToMySQLAddr"] = fmt.Sprintf("%s:%d", drainerConfigSnippet.SyncerCfg.To.Host, drainerConfigSnippet.SyncerCfg.To.Port)
	}
	_, err = client.Run(chart, values)
	if err != nil {
		return nil, err
	}
	resource := &TiDBDrainer{KubeClientSet: *t.KubeClientSet, TiDBDrainer: *apiRes}
	resource.Provider = t.FQDN()
	if err := resource.Sync(); err != nil {
		return nil, err
	}
	return resource, nil
}

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

func (t TiDBDrainerClient) GetEndpoints(_ apiresource.ResourceHandle) (apiresource.ResourceEndpoints, error) {
	return apiresource.ResourceEndpoints{}, nil
}

func (t TiDBDrainerClient) Delete(handle apiresource.ResourceHandle) error {
	actionConfig := new(action.Configuration)

	if err := actionConfig.Init(kube.GetConfig(t.kubeConfig, "", t.Namespace), t.Namespace, "configmap", sugar.Infof); err != nil {
		sugar.Errorf("init the helm action config error: %v", err)
		return err
	}
	client := action.NewUninstall(actionConfig)
	if _, err := client.Run(handle.Name); err != nil {
		return err
	}

	return t.ClientSet.CoreV1().PersistentVolumeClaims(t.Namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{
		LabelSelector: labels.Set{
			"app.kubernetes.io/component": "drainer",
			"app.kubernetes.io/name":      handle.Name,
		}.AsSelector().String(),
	})
}

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

func (t TiDBDrainerClient) Artifact(handle apiresource.ResourceHandle, action apiresource.GcArtifactAction, archiver sdkresource.ArchiverFunc) (map[string]*url.URL, error) {
	resource, err := t.Get(handle)
	if err != nil {
		return nil, err
	}
	drainer := resource.(*TiDBDrainer)

	pods, err := t.ClientSet.CoreV1().Pods(t.Namespace).List(context.TODO(), metav1.ListOptions{
		LabelSelector: labels.Set{"app.kubernetes.io/name": drainer.Name, "app.kubernetes.io/component": "drainer"}.AsSelector().String(),
	})
	if err != nil {
		return nil, err
	}
	if len(pods.Items) != 1 {
		return nil, fmt.Errorf("expect 1 pod in %s, got %d", drainer.Name, len(pods.Items))
	}
	podName := pods.Items[0].Name
	artifactPath := "/data/drainer.log"
	tarballName := drainer.Name + "/" + strings.ReplaceAll(artifactPath[1:], "/", ".") + ".tar"

	tarStream := t.TarStreamFrom(podName, "drainer", artifactPath)
	defer tarStream.Close()
	u, err := archiver(tarballName, tarStream)
	if err != nil {
		return nil, err
	}
	return map[string]*url.URL{tarballName: u}, nil
}

func (t TiDBDrainerClient) FQDN() string {
	return sdkresource.FQDN(t)
}

func (t TiDBDrainerClient) buildTiDBDrainerValueObject(params json.RawMessage) (*apiresource.TiDBDrainer, error) {
	resHandle := &apiresource.TiDBDrainer{}

	if err := json.Unmarshal(params, resHandle); err != nil {
		return nil, errors.Trace(err)
	}
	if err := resHandle.Spec.Validate(); err != nil {
		return nil, fmt.Errorf("build the tidb drainer %s error: parse the spec error: %w", resHandle.Name, err)
	}
	return resHandle, nil
}

type tidbDrainerGetter struct{ handle apiresource.ResourceHandle }

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

func init() {
	mustUnmarshalYaml(values, &defaultDrainerValues)

	core.RegisterProvider(resource.FQDN(TiDBDrainerClient{}),
		func(handle apiresource.ResourceHandle) core.ResourceGetter { return &tidbDrainerGetter{handle: handle} })
}
