package k8s

import (
	"context"
	_ "embed"
	"encoding/json"
	"net/url"

	"k8s.io/apimachinery/pkg/labels"

	"github.com/imdario/mergo"
	"github.com/pingcap/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

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

var (
	_ resource.Client = TestbedClient{}

	//go:embed assets/podgroup.yaml
	podGroupSpec []byte

	defaultPodGroupSpec map[string]interface{}
)

type TestbedClient struct {
	*KubeClientSet
}

func NewTestbedClient(k *KubeClientSet) TestbedClient {
	return TestbedClient{k}
}

// Create a testbed concept container(not linux container)
// spec: request.CreateTestbedRequest
//
//	_, err = client.Create([]byte(`{
//	"name": "testbed",
//	"type": "TEST_BED",
//  "qos": [{"podGroup": "cluster-1"}],
//	}`))
func (t TestbedClient) Create(spec json.RawMessage) (core.Resource, error) {
	cm, podGroups, testbed, err := t.buildTestbedObj(spec)
	if err != nil {
		return nil, err
	}
	cm, err = t.ClientSet.CoreV1().ConfigMaps(t.Namespace).Create(context.TODO(), cm, metav1.CreateOptions{})
	if err != nil {
		return nil, err
	}
	// add the testbed to podgroup's ownerReference
	owner, err := GetOwnerReferenceFromTestbed(t.KubeClientSet, testbed.Name)
	if err != nil {
		return nil, err
	}
	for idx := range podGroups {
		podGroups[idx].SetOwnerReferences([]metav1.OwnerReference{*owner})
		pg, err := t.DynamicClient.Resource(podGroupGVR).Namespace(t.Namespace).Create(context.TODO(), podGroups[idx], metav1.CreateOptions{})
		if err != nil {
			return nil, errors.Annotatef(err, "create podGroup %s", podGroups[idx].GetName())
		}
		podGroups[idx] = pg
	}

	testbed.Ready = &[]bool{true}[0]
	return &Testbed{
		KubeClientSet: *t.KubeClientSet,
		TestBed:       *testbed,
		cm:            cm,
		podGroups:     podGroups,
	}, nil
}

func (t TestbedClient) Get(handle apiresource.ResourceHandle) (core.Resource, error) {
	cm, err := t.ClientSet.CoreV1().ConfigMaps(t.Namespace).Get(context.TODO(), handle.Name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}
	testbed := new(apiresource.TestBed)
	if raw, ok := cm.Data["raw"]; ok {
		if err := json.Unmarshal([]byte(raw), testbed); err != nil {
			return nil, err
		}
	}
	pgs := make([]*unstructured.Unstructured, 0)

	if testbed != nil && len(testbed.Qos) != 0 {
		pgList, err := t.DynamicClient.Resource(podGroupGVR).Namespace(t.Namespace).List(context.TODO(), metav1.ListOptions{
			LabelSelector: labels.Set{"app.kubernetes.io/managed-by": "test-infra"}.AsSelector().String(),
		})
		if err != nil {
			return nil, err
		}
		for idx := range pgList.Items {
			pgs = append(pgs, &pgList.Items[idx])
		}
	}

	return &Testbed{
		KubeClientSet: *t.KubeClientSet,
		TestBed: apiresource.TestBed{
			ResourceHandle: apiresource.ResourceHandle{
				Name:  handle.Name,
				Type:  apiresource.ResourceTypeTestbed,
				Ready: &[]bool{true}[0],
			},
		},
		cm:        cm,
		podGroups: pgs,
	}, nil
}

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

// TODO(@mahjonp) pvcs of TiDBCluster should be also deleted when deleting a Testbed, we haven't set their ownerReference
func (t TestbedClient) Delete(handle apiresource.ResourceHandle) error {
	return t.ClientSet.CoreV1().ConfigMaps(t.Namespace).Delete(context.TODO(), handle.Name, metav1.DeleteOptions{
		PropagationPolicy: &[]metav1.DeletionPropagation{metav1.DeletePropagationForeground}[0],
	})
}

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

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

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

func (t TestbedClient) buildTestbedObj(params json.RawMessage) (*corev1.ConfigMap, []*unstructured.Unstructured, *apiresource.TestBed, error) {
	testbed := new(request.CreateTestbedRequest)
	if err := json.Unmarshal(params, testbed); err != nil {
		return nil, nil, nil, err
	}
	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testbed.Name,
			Namespace: t.Namespace,
			Labels:    map[string]string{"app.kubernetes.io/managed-by": "test-infra"},
		},
		Data: map[string]string{
			// write the raw object into testbed
			"raw": string(params),
		},
	}
	qosClass := make([]*unstructured.Unstructured, 0, len(testbed.Qos))

	for _, qosItem := range testbed.Qos {
		qos := new(unstructured.Unstructured)

		if err := mergo.Merge(&qos.Object, runtime.DeepCopyJSON(defaultPodGroupSpec)); err != nil {
			return nil, nil, nil, errors.Trace(err)
		}
		qos.SetName(qosItem.PodGroup)
		qos.SetNamespace(t.Namespace)
		if qosItem.Exclusive != nil {
			_ = unstructured.SetNestedField(qos.Object, *qosItem.Exclusive, "spec", "exclusive")
		}
		qosClass = append(qosClass, qos)
	}

	return cm, qosClass, &apiresource.TestBed{
		ResourceHandle: apiresource.ResourceHandle{
			Name: testbed.Name,
			Type: apiresource.ResourceTypeTestbed,
		},
		Qos: testbed.Qos,
	}, nil
}

func init() {
	mustUnmarshalYaml(podGroupSpec, &defaultPodGroupSpec)
}
