package br

import (
	"context"
	"fmt"
	"sort"

	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"k8s.io/apimachinery/pkg/api/resource"
)

type TiBRNodePlacement struct {
	NodeSpec    string
	NodeVersion string
}

func getDataplaneTenantAndKubernetesVersion(
	ctx context.Context,
	dpServiceCli dplanemgrv1.DataPlaneServiceClient,
	dataplaneID string,
) (string, string, error) {
	dp, err := dpServiceCli.GetDataPlane(ctx, &dplanemgrv1.GetDataPlaneRequest{
		DataPlaneId: dataplaneID,
	})
	if err != nil {
		return "", "", fmt.Errorf("failed to get dataplane: %w", err)
	}

	tags := dp.GetTags()
	if tags == nil {
		return "", "", fmt.Errorf("tags is nil for dataplane: %s", dataplaneID)
	}
	tenant, ok := tags[schemav1.LabelUserTenantKey]
	if !ok {
		return "", "", fmt.Errorf("tenant tag is not found for dataplane: %s", dataplaneID)
	}

	nodeVersion := dp.GetKubernetes().GetVersion()
	if nodeVersion == "" {
		return "", "", fmt.Errorf("kubernetes version is empty for dataplane: %s", dataplaneID)
	}

	return tenant, nodeVersion, nil
}

func resolveTiBRNodePlacement(
	ctx context.Context,
	dpServiceCli dplanemgrv1.DataPlaneServiceClient,
	dataplaneID, nodeVersion, cpu, memory string,
) (TiBRNodePlacement, error) {
	cpuQuantity, err := resource.ParseQuantity(cpu)
	if err != nil {
		return TiBRNodePlacement{}, fmt.Errorf("failed to parse TiBR cpu %q: %w", cpu, err)
	}
	memoryQuantity, err := resource.ParseQuantity(memory)
	if err != nil {
		return TiBRNodePlacement{}, fmt.Errorf("failed to parse TiBR memory %q: %w", memory, err)
	}

	requestedCPUMilli := cpuQuantity.MilliValue()
	const bytesPerMi = int64(1024 * 1024)
	requestedMemoryMi := (memoryQuantity.Value() + bytesPerMi - 1) / bytesPerMi
	if requestedCPUMilli <= 0 || requestedMemoryMi <= 0 {
		return TiBRNodePlacement{}, fmt.Errorf("TiBR resources must be positive, got cpu=%q memory=%q", cpu, memory)
	}

	resp, err := dpServiceCli.ListNodePools(ctx, &dplanemgrv1.ListNodePoolsRequest{
		DataPlaneId: dataplaneID,
	})
	if err != nil {
		return TiBRNodePlacement{}, fmt.Errorf("failed to list node pools for dataplane %s: %w", dataplaneID, err)
	}

	candidates := make([]*dplanemgrv1.NodePool, 0, len(resp.GetNodePools()))
	for _, pool := range resp.GetNodePools() {
		if pool.GetStatus() != dplanemgrv1.NodePoolStatus_NODEPOOL_ACTIVE ||
			pool.GetNodeType() != dplanemgrv1.NodeType_Dedicated ||
			pool.GetWithLocalDisk() ||
			pool.GetCpuCore() == 0 ||
			pool.GetMemoryGi() == 0 ||
			pool.GetK8SVersion() != nodeVersion ||
			int64(pool.GetAvailableCpuMilliCore()) < requestedCPUMilli ||
			int64(pool.GetAvailableMemoryMi()) < requestedMemoryMi {
			continue
		}
		candidates = append(candidates, pool)
	}

	if len(candidates) == 0 {
		return TiBRNodePlacement{}, fmt.Errorf(
			"no active dedicated node pool fits TiBR cpu=%q memory=%q on kubernetes version %s for dataplane %s",
			cpu, memory, nodeVersion, dataplaneID,
		)
	}

	sort.Slice(candidates, func(i, j int) bool {
		if candidates[i].GetCpuCore() != candidates[j].GetCpuCore() {
			return candidates[i].GetCpuCore() < candidates[j].GetCpuCore()
		}
		if candidates[i].GetMemoryGi() != candidates[j].GetMemoryGi() {
			return candidates[i].GetMemoryGi() < candidates[j].GetMemoryGi()
		}
		return candidates[i].GetName() < candidates[j].GetName()
	})

	selected := candidates[0]
	return TiBRNodePlacement{
		NodeSpec:    fmt.Sprintf("%dC%dG", selected.GetCpuCore(), selected.GetMemoryGi()),
		NodeVersion: nodeVersion,
	}, nil
}

func getTiBRNodePlacement(
	ctx context.Context,
	dpServiceCli dplanemgrv1.DataPlaneServiceClient,
	dataplaneID, cpu, memory string,
) (tenant string, placement TiBRNodePlacement, err error) {
	tenant, nodeVersion, err := getDataplaneTenantAndKubernetesVersion(ctx, dpServiceCli, dataplaneID)
	if err != nil {
		return "", TiBRNodePlacement{}, err
	}
	placement, err = resolveTiBRNodePlacement(ctx, dpServiceCli, dataplaneID, nodeVersion, cpu, memory)
	if err != nil {
		return "", TiBRNodePlacement{}, err
	}
	return tenant, placement, nil
}

func resolveTiBRAutoBackupPlacement(
	ctx context.Context,
	dpServiceCli dplanemgrv1.DataPlaneServiceClient,
	bizConfig *config.BizConfig,
	sharedPool *entity.SharedPool,
) (TiBRNodePlacement, error) {
	cpu, memory, err := getResourcesForTiBRAutoBackup(bizConfig)
	if err != nil {
		return TiBRNodePlacement{}, err
	}
	_, placement, err := getTiBRNodePlacement(ctx, dpServiceCli, sharedPool.GetDataplaneName(), cpu, memory)
	return placement, err
}
