package facade_imp

import (
	"context"
	"fmt"
	"strconv"

	chaosv1alpha1 "github.com/chaos-mesh/chaos-mesh/api/v1alpha1"
	"github.com/tidbcloud/chaos-svc/idl/pbgen/proto/chaos"
	"github.com/tidbcloud/chaos-svc/pkg/domain/constant"
	"github.com/tidbcloud/chaos-svc/pkg/domain/entity"
	"github.com/tidbcloud/chaos-svc/pkg/domain/facade"
	"github.com/tidbcloud/chaos-svc/pkg/infra/config"
	cloudv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	labelKeyComponentType = "app.kubernetes.io/component"
)

type infraImp struct {
	projectName string
	client      client.Client // we can mock this client for unit tests.
}

func NewInfra(conf *config.ChaosConfig) facade.Infra {
	ctx := context.TODO()
	// using the gardenerProjectName as the identifier of different central deployment
	api, err := NewAPI(ctx, conf.ServerConfig.GardenerConfig.GardenerProjectName, conf)
	if err != nil {
		panic(fmt.Sprintf("initialize the infra API failed, err=%s", err.Error()))
	}
	return api
}

// NewAPI returns a new API instance.
func NewAPI(ctx context.Context, projectName string, conf *config.ChaosConfig) (*infraImp, bizerr.BizError) {
	scheme := runtime.NewScheme()
	err := cloudv1alpha1.AddToScheme(scheme)
	if err != nil {
		return nil, bizerr.WrapError(err, "register infra API cloudv1alpha1: %s", err)
	}
	err = cloudv1.AddToScheme(scheme)
	if err != nil {
		return nil, bizerr.WrapError(err, "register infra API cloudv1: %s", err)
	}
	err = corev1.AddToScheme(scheme)
	if err != nil {
		return nil, bizerr.WrapError(err, "register infra API corev1: %s", err)
	}

	cfg := conf.ServerConfig.InfraAPIConfig
	if cfg.KubeConfigPath == "" {
		return nil, bizerr.New(bizerr.ErrCodeSvcInfraAPI, "kubeConfigPath is not set")
	}
	restConfig, err := clientcmd.BuildConfigFromFlags("", cfg.KubeConfigPath)
	if err != nil {
		return nil, bizerr.WrapError(err, "retrieve infra API server kubeconfig failed: %s", err)
	}
	cli, err := client.New(restConfig, client.Options{
		Scheme: scheme,
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "retrieve infra API server kubeconfig failed: %s", err)
	}

	return &infraImp{
		projectName: projectName,
		client:      cli,
	}, nil
}

func (imp *infraImp) Namespace(projectID uint64) string {
	return imp.projectName + "-" + strconv.FormatUint(projectID, 10)
}

func (imp *infraImp) ChaosServerName(projectID uint64, networkName string) string {
	return networkName
}

func (imp *infraImp) WorkflowName(workflowID uint64) string {
	return strconv.FormatUint(workflowID, 10)
}

func (imp *infraImp) GetChaosServer(ctx context.Context, projectID uint64, networkName string) (*cloudv1alpha1.ChaosServer, bizerr.BizError) {
	cr := &cloudv1alpha1.ChaosServer{}
	ns := imp.Namespace(projectID)
	name := imp.ChaosServerName(projectID, networkName)
	if err := imp.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, cr); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "can't find chaos server, ns=%s,name=%s", ns, name)
		}
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "get chaos server from infra")
	}
	return cr, nil
}

func (imp *infraImp) CreateChaosServerIgnoreExist(ctx context.Context, cr *cloudv1alpha1.ChaosServer) bizerr.BizError {
	logger := log.WithContextS(ctx).With(zap.String("chaosServerName", cr.Name))
	logger.Infof("try to create chaos server cr, cr=%+v", cr)
	err := imp.client.Create(ctx, cr)
	if err != nil {
		if apierrors.IsAlreadyExists(err) {
			return nil
		}
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "call infra to create chaos server")
	}
	return nil
}

func (imp *infraImp) DeleteChaosServerIgnoreNotFound(ctx context.Context, projectID uint64, networkName string) bizerr.BizError {
	ns := imp.Namespace(projectID)
	name := imp.ChaosServerName(projectID, networkName)
	cr := &cloudv1alpha1.ChaosServer{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
	}
	err := imp.client.Delete(ctx, cr)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "delete chaos server cr from infra. ns=%s,name=%s", ns, name)
	}
	return nil
}

func (imp *infraImp) GetWorkflow(ctx context.Context, projectID uint64, workflowID uint64) (*cloudv1alpha1.ChaosWorkflow, bizerr.BizError) {
	cr := &cloudv1alpha1.ChaosWorkflow{}
	ns := imp.Namespace(projectID)
	name := imp.WorkflowName(workflowID)
	if err := imp.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, cr); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "can't find chaos workflow, ns=%s,name=%s", ns, name)
		}
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "get chaos workflow from infra")
	}
	return cr, nil
}

func (imp *infraImp) CreateWorkflowIgnoreExist(ctx context.Context, workflowEntity *entity.Workflow) bizerr.BizError {
	logger := log.WithContextS(ctx).With("chaosWorkflowID", workflowEntity.ID)

	cr, bizError := imp.entity2CR(ctx, workflowEntity)
	if bizError != nil {
		return bizError
	}
	logger.Infof("try to create chaos workflow cr, cr=%+v", cr)
	err := imp.client.Create(ctx, cr)
	if err != nil {
		if apierrors.IsAlreadyExists(err) {
			return nil
		}
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "call infra to create chaos workflow")
	}
	return nil
}

func (imp *infraImp) DeleteWorkflowIgnoreNotFound(ctx context.Context, projectID uint64, workflowID uint64) bizerr.BizError {
	ns := imp.Namespace(projectID)
	name := imp.WorkflowName(workflowID)
	cr := &cloudv1alpha1.ChaosWorkflow{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
	}
	err := imp.client.Delete(ctx, cr)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "delete chaos workflow cr from infra. ns=%s,name=%s", ns, name)
	}
	return nil
}

func (imp *infraImp) ListWorkflowNodes(ctx context.Context, projectID uint64, workflowID uint64) ([]cloudv1alpha1.ChaosWorkflowNode, bizerr.BizError) {
	ns := imp.Namespace(projectID)
	name := imp.WorkflowName(workflowID)
	nodes := &cloudv1alpha1.ChaosWorkflowNodeList{}
	if err := imp.client.List(ctx, nodes,
		client.InNamespace(ns),
		client.MatchingLabels{chaosv1alpha1.LabelWorkflow: name},
	); err != nil {
		return nil, bizerr.WrapError(err, "list chaos workflow nodes from infra")
	}
	return nodes.Items, nil
}

func (imp *infraImp) GetTiDBCluster(ctx context.Context, projectID uint64, clusterID uint64) (*facade.TiDBCluster, bizerr.BizError) {
	cr, err := imp.GetTiDBClusterCloudObj(ctx, projectID, clusterID)
	if err != nil {
		return nil, err
	}

	return imp.buildTiDBCluster(*cr, projectID)
}

func (imp *infraImp) GetTiDBClusterCloudObj(ctx context.Context, projectID uint64, clusterID uint64) (*cloudv1.Cluster, bizerr.BizError) {
	cr := &cloudv1.Cluster{}
	ns := imp.Namespace(projectID)
	name := strconv.FormatUint(clusterID, 10)
	if err := imp.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, cr); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "can't find tidb cluster, ns=%s,name=%s", ns, name)
		}
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeGeneralBusiness, "get tidb cluster from infra")
	}
	return cr, nil
}

func (imp *infraImp) ListTiDBClusters(ctx context.Context, projectID uint64, filter *facade.TiDBClusterFilter) ([]*facade.TiDBCluster, bizerr.BizError) {
	ns := imp.Namespace(projectID)
	clusterList := &cloudv1.ClusterList{}
	if err := imp.client.List(ctx, clusterList, imp.buildListTiDBClusterOption(ns, filter)...); err != nil {
		return nil, bizerr.WrapError(err, "list tidb clusters from infra")
	}
	clusters := make([]*facade.TiDBCluster, 0, len(clusterList.Items))
	for _, each := range clusterList.Items {
		cluster, err := imp.buildTiDBCluster(each, projectID)
		if err != nil {
			return nil, err
		}
		clusters = append(clusters, cluster)
	}
	return clusters, nil
}

func (imp *infraImp) buildTiDBCluster(cr cloudv1.Cluster, projectID uint64) (*facade.TiDBCluster, bizerr.BizError) {
	networkName := cr.Spec.NetworkName
	providerType := cr.Labels[facade.LabelKeyProviderType]
	region := cr.Labels[facade.LabelKeyRegion]
	clusterID, err := strconv.ParseUint(cr.Name, 10, 64)
	if err != nil {
		return nil, bizerr.WrapError(err, "parse cluster id from infra")
	}
	// TODO: parse this field from TiDBCluster Status
	ns := fmt.Sprintf("tidb%d", clusterID)

	return &facade.TiDBCluster{
		NetworkName:  networkName,
		ProjectID:    projectID,
		ClusterID:    clusterID,
		Region:       region,
		ProviderType: providerType,
		Namespace:    ns,
	}, nil
}

func (imp *infraImp) buildListTiDBClusterOption(namespace string, filter *facade.TiDBClusterFilter) []client.ListOption {
	options := make([]client.ListOption, 0)
	options = append(options, client.InNamespace(namespace))
	if filter != nil {
		if filter.Region != "" {
			options = append(options, client.MatchingLabels{facade.LabelKeyRegion: filter.Region})
		}
	}
	return options
}

func (imp *infraImp) entity2CR(ctx context.Context, workflow *entity.Workflow) (*cloudv1alpha1.ChaosWorkflow, bizerr.BizError) {
	name := imp.WorkflowName(workflow.ID)
	ns := imp.Namespace(workflow.ProjectID)
	cluster, err := imp.GetTiDBCluster(ctx, workflow.ProjectID, workflow.ClusterID)
	if err != nil {
		return nil, err
	}
	templates, err := imp.buildWorkflowTemplates(workflow.Spec.Templates, *cluster)
	if err != nil {
		return nil, err
	}
	cr := &cloudv1alpha1.ChaosWorkflow{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
			Labels: map[string]string{
				facade.LabelKeyRegion:       cluster.Region,
				facade.LabelKeyProviderType: cluster.ProviderType,
			},
		},
		Spec: cloudv1alpha1.ChaosWorkflowSpec{
			NetworkName:     cluster.NetworkName,
			RemoteNamespace: cluster.Namespace,
			Entry:           workflow.Spec.EntryNodeName,
			Templates:       templates,
		},
	}
	return cr, nil
}

func (imp *infraImp) buildWorkflowTemplates(templates map[string]entity.WorkflowNodeTemplate, cluster facade.TiDBCluster) ([]chaosv1alpha1.Template, bizerr.BizError) {
	templateList := make([]chaosv1alpha1.Template, 0, len(templates))
	for _, each := range templates {
		switch each.Type {
		case constant.WorkflowTemplateTypePodShutdown:
			template, err := imp.buildPodShutdownTemplate(each, cluster)
			if err != nil {
				return nil, err
			}
			templateList = append(templateList, template)
		case constant.WorkflowTemplateTypeSerial:
			template, err := imp.buildSerialTemplate(each)
			if err != nil {
				return nil, err
			}
			templateList = append(templateList, template)
		default:
			return nil, bizerr.New(bizerr.ErrCodeGeneralBusiness, "template type not supported")
		}
	}
	return templateList, nil
}

func (imp *infraImp) buildSerialTemplate(template entity.WorkflowNodeTemplate) (chaosv1alpha1.Template, bizerr.BizError) {
	if template.Type != constant.WorkflowTemplateTypeSerial {
		return chaosv1alpha1.Template{}, bizerr.New(bizerr.ErrCodeGeneralBusiness, "template type is not serial")
	}
	return chaosv1alpha1.Template{
		Name:     template.Name,
		Type:     chaosv1alpha1.TypeSerial,
		Children: template.Children,
	}, nil
}

func (imp *infraImp) buildPodShutdownTemplate(template entity.WorkflowNodeTemplate, cluster facade.TiDBCluster) (chaosv1alpha1.Template, bizerr.BizError) {
	if template.Type != constant.WorkflowTemplateTypePodShutdown {
		return chaosv1alpha1.Template{}, bizerr.New(bizerr.ErrCodeGeneralBusiness, "template type is not pod shutdown")
	}
	deadline := "10s"
	return chaosv1alpha1.Template{
		Name:     template.Name,
		Type:     chaosv1alpha1.TypePodChaos,
		Deadline: &deadline,
		EmbedChaos: &chaosv1alpha1.EmbedChaos{
			PodChaos: &chaosv1alpha1.PodChaosSpec{
				Action:            chaosv1alpha1.PodKillAction,
				ContainerSelector: imp.buildContainerSelector(&template.PodShutdown.Target, cluster),
			},
		},
	}, nil
}

func (imp *infraImp) buildContainerSelector(target *chaos.ExperimentTarget, cluster facade.TiDBCluster) chaosv1alpha1.ContainerSelector {
	namespace := cluster.Namespace
	selector := chaosv1alpha1.ContainerSelector{
		PodSelector: chaosv1alpha1.PodSelector{
			Mode: chaosv1alpha1.OneMode,
			Selector: chaosv1alpha1.PodSelectorSpec{
				GenericSelectorSpec: chaosv1alpha1.GenericSelectorSpec{
					Namespaces: []string{namespace},
				},
			},
		},
	}
	switch target.Mode {
	case chaos.TargetMode_TARGET_MODE_RANDOM:
		selector.PodSelector.Selector.GenericSelectorSpec.LabelSelectors = map[string]string{
			labelKeyComponentType: convertToComponentType(target.Component),
		}
	case chaos.TargetMode_TARGET_MODE_SPECIFIED:
		selector.PodSelector.Selector.Pods = map[string][]string{
			namespace: {*target.Name},
		}
	}
	return selector
}

func convertToComponentType(component chaos.TargetComponent) string {
	switch component {
	case chaos.TargetComponent_TARGET_COMPONENT_TIDB:
		return "tidb"
	case chaos.TargetComponent_TARGET_COMPONENT_TIKV:
		return "tikv"
	case chaos.TargetComponent_TARGET_COMPONENT_PD:
		return "pd"
	}
	panic(fmt.Sprintf("unknown component type: %v", component))
}
