package action

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/samber/lo"
	csngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"github.com/tidbcloud/cluster-service-ng/pkg/k8s"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
)

type CreatePlaceholderDeploymentAction struct {
	dataplaneManager    dataplane.Manager
	sharedPoolRepo      repo.SharedPoolRepo
	placeholderReplicas int
}

const CreatePlaceholderDeploymentActionName = "create-placeholder-deployment"

func NewCreatePlaceholderDeploymentAction(
	sharedPoolRepo repo.SharedPoolRepo,
	dataplaneManager dataplane.Manager,
	placeholderReplicas int,
) actions.Action {
	return &CreatePlaceholderDeploymentAction{
		sharedPoolRepo:      sharedPoolRepo,
		dataplaneManager:    dataplaneManager,
		placeholderReplicas: placeholderReplicas,
	}
}

func (a *CreatePlaceholderDeploymentAction) Name() string {
	return CreatePlaceholderDeploymentActionName
}

// Invoke creates a placeholder deployment for each node spec in the shared pool.
func (a *CreatePlaceholderDeploymentAction) Invoke(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "create placeholder deployment action invoked")

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	// load sharedPool
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}

	sharedPoolStr, err := args.GetString(workflow.SharedPoolKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool: %w", err)
	}

	var tempSharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &tempSharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedPool", zap.Error(err))
		return fmt.Errorf("failed to unmarshal sharedPool: %w", err)
	}

	sharedPool.ComponentGroups = tempSharedPool.GetComponentGroups()

	targetNodeSpec := make(map[string]*csngv1beta.NodeSpec)
	for _, comp := range sharedPool.ComponentGroups {
		if comp.GetNodeQuota() == nil {
			continue
		}
		nodeSpec := comp.GetNodeQuota().GetNodeSpec()
		if nodeSpec == nil {
			continue
		}
		key := fmt.Sprintf("%vC%vG", nodeSpec.GetCpu(), nodeSpec.GetMemoryGi())
		targetNodeSpec[key] = nodeSpec
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	for nodeSpecKey, nodeSpec := range targetNodeSpec {
		for _, zone := range sharedPool.GetProvider().GetNodeZones() {
			deploy := buildPlaceholderDeployment(zone, nodeSpecKey, nodeSpec, a.placeholderReplicas)
			if err := k8s.CreateIfNotExist(ctx, remoteCli, deploy); err != nil {
				return fmt.Errorf("failed to create placeholder deployment: %w", err)
			}
		}
	}

	return nil
}

func buildPlaceholderDeployment(
	zone string,
	nodeSpecKey string,
	nodeSpec *csngv1beta.NodeSpec,
	placeholderReplicas int,
) *appsv1.Deployment {
	name := "placeholder-" + strings.ToLower(nodeSpecKey) + "-" + zone
	requestCpu := strconv.FormatUint(uint64(nodeSpec.AvailableCpu), 10)
	requestMem := fmt.Sprintf("%vMi", nodeSpec.AvailableMemoryMi)
	deploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: "default",
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: ptr.To(int32(placeholderReplicas)),
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": "placeholder",
				},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"app": "placeholder",
					},
				},
				Spec: corev1.PodSpec{
					PriorityClassName: priorityClassNamePlaceholder,
					Tolerations: []corev1.Toleration{
						{
							Key:      "dedicated",
							Operator: corev1.TolerationOpEqual,
							Value:    "worker",
							Effect:   corev1.TaintEffectNoSchedule,
						},
					},
					NodeSelector: map[string]string{
						"node-spec":                   nodeSpecKey,
						"topology.kubernetes.io/zone": zone,
					},
					Containers: []corev1.Container{
						{
							Name:  "placeholder",
							Image: "public.ecr.aws/eks-distro/kubernetes/pause:3.9",
							Resources: corev1.ResourceRequirements{
								Requests: corev1.ResourceList{
									corev1.ResourceCPU:    resource.MustParse(requestCpu),
									corev1.ResourceMemory: resource.MustParse(requestMem),
								},
							},
							SecurityContext: &corev1.SecurityContext{
								ReadOnlyRootFilesystem:   ptr.To(true),
								AllowPrivilegeEscalation: ptr.To(false),
								Privileged:               ptr.To(false),
								Capabilities: &corev1.Capabilities{
									Drop: []corev1.Capability{"NET_RAW"},
								},
							},
						},
					},
					SecurityContext: &corev1.PodSecurityContext{
						RunAsUser:    lo.ToPtr(int64(1000)),
						RunAsGroup:   lo.ToPtr(int64(2000)),
						FSGroup:      lo.ToPtr(int64(2000)),
						RunAsNonRoot: lo.ToPtr(true),
					},
					ServiceAccountName: placeholderServiceAccountName,
				},
			},
		},
	}
	return deploy
}
