package k8s

import (
	"context"
	"encoding/json"
	"fmt"
	"io"

	"github.com/hashicorp/go-multierror"
	"github.com/pingcap/errors"
	"go.uber.org/zap"
	v1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"

	apiresource "github.com/pingcap/test-infra/common/model/resource"

	"github.com/pingcap/test-infra/sdk/resource"
)

var (
	_ resource.WorkloadNode = &WorkloadNode{}
)

type WorkloadNode struct {
	KubeClientSet
	// api object
	apiresource.WorkloadNode
	// k8s object
	cm                 *corev1.ConfigMap
	sts                *v1.StatefulSet
	service            *corev1.Service
	loadBalanceService *corev1.Service
}

func (t *WorkloadNode) Handle() apiresource.ResourceHandle {
	return t.WorkloadNode.ResourceHandle
}

func (t *WorkloadNode) Exec(opts resource.WorkloadNodeExecOptions) (stdout string, stderr string, exitCode int, err error) {
	stdout, stderr, ec, err := t.execCommandInPodWithFullOutput(fmt.Sprintf("%s-%d", t.WorkloadNode.Name, opts.Peer), "/bin/sh", "-c", opts.Command)
	if err != nil {
		sugar.Error("Exec error", zap.Error(err))
		err = errors.Trace(err)
	}
	if ec != nil {
		exitCode = *ec
	} else {
		sugar.Warnw("cannot get the exit code, return -1")
		exitCode = -1
	}
	return
}

func (t *WorkloadNode) ExecAsync(stdout io.WriteCloser, stderr io.WriteCloser, opts resource.WorkloadNodeExecOptions) (remote resource.RemoteCommander, err error) {
	return t.execCommandInContainerWithAsyncOutput(stdout, stderr, fmt.Sprintf("%s-%d", t.WorkloadNode.Name, opts.Peer), "/bin/sh", "-c", opts.Command)
}

func (t *WorkloadNode) ExecAsyncInterruptible(stdout io.WriteCloser, opts resource.WorkloadNodeExecOptions) (remote resource.RemoteCommanderInterruptible, err error) {
	return t.execCommandInContainerWithAsyncOutputInterruptible(stdout, fmt.Sprintf("%s-%d", t.WorkloadNode.Name, opts.Peer), "/bin/sh", "-c", opts.Command)
}

func (t *WorkloadNode) WriteFile(opts resource.WorkloadNodeWriteFileOptions) error {
	return t.KubeClientSet.WriteFileInContainer(fmt.Sprintf("%s-%d", t.WorkloadNode.Name, opts.Peer), t.sts.Spec.Template.Spec.Containers[0].Name, opts.Content, opts.FilePath)
}

func (t *WorkloadNode) Ready() bool {
	return t.WorkloadNode.Status.Phase == apiresource.WorkloadNodeReady
}

// Sync syncs from the k8s object to api object
func (t *WorkloadNode) Sync() error {
	var errs error

	cms, err := t.ClientSet.CoreV1().ConfigMaps(t.Namespace).List(context.TODO(), metav1.ListOptions{
		LabelSelector: labels.Set{TestInfraName: t.Name}.AsSelector().String(),
	})
	if err != nil {
		return multierror.Append(errs, err)
	}
	sts, err := t.ClientSet.AppsV1().StatefulSets(t.Namespace).Get(context.TODO(), t.ResourceHandle.Name, metav1.GetOptions{})
	if err != nil {
		errs = multierror.Append(errs, err)
		sts = nil
	}
	service, err := t.ClientSet.CoreV1().Services(t.Namespace).Get(context.TODO(), t.ResourceHandle.Name+"-peer", metav1.GetOptions{})
	if err != nil {
		errs = multierror.Append(errs, err)
		service = nil
	}
	loadBalanceService, err := t.ClientSet.CoreV1().Services(t.Namespace).Get(context.TODO(), t.ResourceHandle.Name+"-lb", metav1.GetOptions{})
	if err != nil {
		errs = multierror.Append(errs, err)
		loadBalanceService = nil
	}
	pvcs, err := t.ClientSet.CoreV1().PersistentVolumeClaims(t.Namespace).List(context.TODO(), metav1.ListOptions{
		LabelSelector: labels.Set{TestInfraName: t.Name}.AsSelector().String(),
	})
	if err != nil {
		return multierror.Append(errs, err)
	}
	if len(cms.Items) == 0 && len(pvcs.Items) == 0 && sts == nil && service == nil && loadBalanceService == nil {
		return resource.ErrorResourceNotFound{ResourceHandle: t.Handle()}
	}
	if errs != nil {
		return errs
	}

	if len(cms.Items) == 1 {
		t.cm = &cms.Items[0]
	}
	t.sts = sts
	t.service = service
	t.loadBalanceService = loadBalanceService

	t.WorkloadNode.Spec = apiresource.WorkloadNodeSpec{
		Replicas: *sts.Spec.Replicas,
		Container: apiresource.WorkloadUnit{
			Name:  sts.Spec.Template.Spec.Containers[0].Name,
			Image: sts.Spec.Template.Spec.Containers[0].Image,
			Resources: apiresource.ResourceRequirements{
				Requests: make(map[apiresource.ResourceName]apiresource.Quantity),
				Limits:   make(map[apiresource.ResourceName]apiresource.Quantity),
			},
			Command:    sts.Spec.Template.Spec.Containers[0].Command,
			Args:       sts.Spec.Template.Spec.Containers[0].Args,
			WorkingDir: sts.Spec.Template.Spec.Containers[0].WorkingDir,
		},
	}
	for res, quantity := range sts.Spec.Template.Spec.Containers[0].Resources.Requests {
		t.WorkloadNode.Spec.Container.Resources.Requests[apiresource.ResourceName(res)] = apiresource.Quantity(quantity.String())
	}
	for res, quantity := range sts.Spec.Template.Spec.Containers[0].Resources.Limits {
		t.WorkloadNode.Spec.Container.Resources.Limits[apiresource.ResourceName(res)] = apiresource.Quantity(quantity.String())
	}
	if len(sts.Spec.VolumeClaimTemplates) != 0 {
		for _, volume := range sts.Spec.VolumeClaimTemplates {
			if volume.Name == "data" {
				if quantity, ok := volume.Spec.Resources.Requests[corev1.ResourceStorage]; ok {
					t.WorkloadNode.Spec.Container.Resources.Requests[apiresource.Storage] = apiresource.Quantity(quantity.String())
				}
			}
		}
	}
	if sts.Status.ReadyReplicas == *sts.Spec.Replicas {
		t.WorkloadNode.Status.Phase = apiresource.WorkloadNodeReady
		t.WorkloadNode.ResourceHandle.Ready = &[]bool{true}[0]
	} else {
		t.WorkloadNode.Status.Phase = apiresource.WorkloadNodeNotReady
		t.WorkloadNode.ResourceHandle.Ready = &[]bool{false}[0]
	}

	specData, err := t.KubeClientSet.GetResourceSpecFromConfigMap(t.ResourceHandle, t.Testbed)
	if err != nil {
		return multierror.Append(errs, err)
	}
	if len(specData) != 0 {
		var workloadNode apiresource.WorkloadNode
		err = json.Unmarshal(specData, &workloadNode)
		if err != nil {
			return multierror.Append(errs, err)
		}
		t.WorkloadNode.Spec.Artifact = workloadNode.Spec.Artifact
	}

	return nil
}

func (t *WorkloadNode) ChaosClient() resource.ChaosClient {
	return &ChaosClient{t.KubeClientSet}
}

func (t *WorkloadNode) ChaosSelect(selector resource.WorkloadNodeSelectorOpts) ([]resource.ChaosSelector, error) {
	var selectors []resource.ChaosSelector

	if selector.All {
		pods, err := t.KubeClientSet.ClientSet.CoreV1().Pods(t.Namespace).List(context.Background(), metav1.ListOptions{
			LabelSelector: labels.Set{TestInfraName: t.WorkloadNode.Name}.AsSelector().String(),
		})
		if err != nil {
			return nil, err
		}
		for i := range pods.Items {
			selectors = append(selectors, &PodSelector{Pod: &pods.Items[i]})
		}
	} else {
		pod, err := t.KubeClientSet.ClientSet.CoreV1().Pods(t.Namespace).Get(context.Background(), fmt.Sprintf("%s-%d", t.WorkloadNode.Name, selector.Peer), metav1.GetOptions{})
		if err != nil {
			return nil, err
		}
		selectors = append(selectors, &PodSelector{pod})
	}

	return selectors, nil
}
