package server

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"

	apiv1 "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/pkg/tioperator"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
)

// TiDB autoscaling intentionally exposes only the production TiDB shapes.
// Other components have different legal node specs.
var tidbAutoscalingNodeSpecs = map[string]struct{}{
	"4C16G":  {},
	"8C16G":  {},
	"16C32G": {},
}

type autoscalerTier struct {
	cpu           uint32
	memoryGi      uint32
	cpuRequest    string
	memoryRequest string
}

func buildTiDBGroupAutoscalerTemplateArgs(
	ctx context.Context,
	sharedPool *entity.SharedPool,
	tidbGroup *entity.TiDBGroup,
	nodeSpecMap map[string][]*entity.NodeSpec,
	ruConfig *entity.RuConfig,
) (string, error) {
	if ruConfig == nil {
		return "", fmt.Errorf("RU config is required")
	}

	var tidbComp *entity.Component
	for _, comp := range sharedPool.GetComponentGroups() {
		if comp.GetComponentType() == apiv1.ComponentType_TIDB && comp.GetId() == tidbGroup.GetId() {
			tidbComp = comp
			break
		}
	}
	if tidbComp == nil || tidbComp.GetNodeQuota() == nil {
		return "", fmt.Errorf("TiDB component %s has no node quota", tidbGroup.GetId())
	}

	minReplicas := tidbComp.GetNodeQuota().GetMinNodeCount()
	maxReplicas := tidbComp.GetNodeQuota().GetMaxNodeCount()
	if maxReplicas == 0 {
		maxReplicas = tidbComp.GetNodeQuota().GetNodeCount()
	}
	if minReplicas == 0 || maxReplicas < minReplicas {
		return "", fmt.Errorf("invalid TiDB replica bounds: min=%d max=%d", minReplicas, maxReplicas)
	}

	componentConfig := ruConfig.GetComponents()[strings.ToLower(apiv1.ComponentType_TIDB.String())]
	if componentConfig == nil {
		return "", fmt.Errorf("TiDB component is missing from RU config")
	}

	tiers := make([]autoscalerTier, 0, len(componentConfig.GetNodeSpecRuRange()))
	seen := map[string]struct{}{}
	for name := range componentConfig.GetNodeSpecRuRange() {
		cpu, memoryGi, err := utils.ParseNodeSpec(name)
		if err != nil {
			return "", fmt.Errorf("invalid TiDB node spec %q: %w", name, err)
		}
		nodeSpec, err := getNodeSpec(ctx, &entity.NodeSpec{Cpu: uint32(cpu), MemoryGi: uint32(memoryGi)}, nodeSpecMap, false)
		if err != nil {
			continue
		}
		canonicalName := entity.GetComponentNodeSpec(nodeSpec)
		if _, ok := seen[canonicalName]; ok {
			continue
		}
		seen[canonicalName] = struct{}{}
		_, ok := tidbAutoscalingNodeSpecs[canonicalName]
		if !ok {
			continue
		}
		cpuReq, memReq, _, _, _ := effectiveTiDBCapacity(nodeSpec)
		tiers = append(tiers, autoscalerTier{
			cpu:           nodeSpec.GetCpu(),
			memoryGi:      nodeSpec.GetMemoryGi(),
			cpuRequest:    cpuReq,
			memoryRequest: memReq,
		})
	}
	if len(tiers) == 0 {
		return "", fmt.Errorf("no legal TiDB vertical tiers are available in the dataplane")
	}
	sort.Slice(tiers, func(i, j int) bool {
		if tiers[i].cpu != tiers[j].cpu {
			return tiers[i].cpu < tiers[j].cpu
		}
		return tiers[i].memoryGi < tiers[j].memoryGi
	})

	cpuVariants := make([]string, 0, len(tiers))
	memoryVariants := make([]string, 0, len(tiers))
	for _, tier := range tiers {
		cpuVariants = append(cpuVariants, tier.cpuRequest)
		memoryVariants = append(memoryVariants, tier.memoryRequest)
	}

	cpuVariantsJSON, err := json.Marshal(cpuVariants)
	if err != nil {
		return "", fmt.Errorf("marshal TiDB CPU variants: %w", err)
	}
	memoryVariantsJSON, err := json.Marshal(memoryVariants)
	if err != nil {
		return "", fmt.Errorf("marshal TiDB memory variants: %w", err)
	}

	templateArgs := map[string]string{
		"SharedPoolId":   sharedPool.GetId(),
		"ComponentGroup": entity.GetComponentGroupName(tidbComp),
		"MinReplicas":    strconv.FormatUint(uint64(minReplicas), 10),
		"MaxReplicas":    strconv.FormatUint(uint64(maxReplicas), 10),
		"CPUVariants":    string(cpuVariantsJSON),
		"MemoryVariants": string(memoryVariantsJSON),
	}

	data, err := json.Marshal(templateArgs)
	if err != nil {
		return "", fmt.Errorf("marshal autoscaler template args: %w", err)
	}
	return string(data), nil
}

func effectiveTiDBCapacity(nodeSpec *entity.NodeSpec) (cpuRequest, memoryRequest, cpuLimit, memoryLimit, goMemLimit string) {
	sidecarCPUMilli, sidecarMemInMiB := tioperator.SidecarResourcesFor(apiv1.ComponentType_TIDB)
	availCPU := tioperator.EffectiveAvailableCPUMilli(nodeSpec)
	availMem := uint64(nodeSpec.GetAvailableMemoryMi())
	cpuReq := tioperator.ComputeRequestCPU(availCPU, sidecarCPUMilli)
	memReq := tioperator.ComputeRequestMemory(availMem, sidecarMemInMiB)
	cpuLim := tioperator.EffectiveMaxCPUMilli(nodeSpec)
	memLim := tioperator.EffectiveMaxMemoryMi(nodeSpec)
	goLim := uint64(float64(memLim) * tioperator.GoMemlimitRate)
	return fmt.Sprintf("%dm", cpuReq),
		fmt.Sprintf("%dMi", memReq),
		fmt.Sprintf("%dm", cpuLim),
		fmt.Sprintf("%dMi", memLim),
		fmt.Sprintf("%dMiB", goLim)
}
