package tioperator

import (
	"fmt"
	"math"
	"strconv"
	"strings"

	"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func setTemplateVersionFromAnnotations(comp *entity.Component, annotations map[string]string) {
	if comp == nil || annotations == nil {
		return
	}
	if v := annotations[templateversion.TemplateVersionAnnotationKey]; v != "" {
		comp.TemplateVersion = v
	}
}

const (
	localSSD             = "local-ssd"
	cloudAuto            = "cloud-auto"
	namespacePrefix      = "tidb"
	componentGroupPrefix = "db-"
	clusterCRName        = "db"

	// GoMemlimitRate is the fraction (80%) of the TiDB container memory limit used to compute the GOMEMLIMIT environment variable.
	GoMemlimitRate = 0.8

	// Sidecar requests subtracted from the main container Request so the whole Pod
	// plus DaemonSet still fits allocatable. See analyze/08-sidecar-resource-budget.md.
	TiDBSidecarMemoryMiB       uint64 = 261
	TiDBSidecarCPUMilli        uint64 = 40
	TiDBWorkerSidecarMemoryMiB uint64 = 50
	TiDBWorkerSidecarCPUMilli  uint64 = 100
	TiFlashSidecarMemoryMiB    uint64 = 10
	TiFlashSidecarCPUMilli     uint64 = 40

	requestMemoryReserveMinMi   uint64 = 256
	requestMemoryReserveMaxMi   uint64 = 1024
	requestMemoryReservePercent uint64 = 5
	requestCPUReserveMinMilli   uint64 = 100
	requestCPUReserveMaxMilli   uint64 = 200
	requestCPUReservePercent    uint64 = 5
)

func dataplaneNamespace(clusterId string) string {
	return fmt.Sprintf("tidb%s", clusterId)
}

func splitImageRepoTag(image *string) (string, string) {
	if image == nil {
		return "", ""
	}

	ref := strings.TrimSpace(*image)
	if ref == "" {
		return "", ""
	}

	if digestIndex := strings.Index(ref, "@"); digestIndex >= 0 {
		ref = ref[:digestIndex]
	}

	lastSlash := strings.LastIndex(ref, "/")
	lastColon := strings.LastIndex(ref, ":")
	if lastColon > lastSlash {
		return ref[:lastColon], ref[lastColon+1:]
	}

	return ref, ""
}

func isReady(conditions []metav1.Condition) bool {
	return meta.IsStatusConditionTrue(conditions, v1alpha1.CondReady)
}

func isSynced(conditions []metav1.Condition) bool {
	return meta.IsStatusConditionTrue(conditions, v1alpha1.CondSynced)
}

func isUpToDate(generation int64, commonStatus v1alpha1.CommonStatus) bool {
	return commonStatus.CurrentRevision == commonStatus.UpdateRevision && generation == commonStatus.ObservedGeneration
}

// func readyAndSynced(conditions []metav1.Condition) bool {
// 	return isReady(conditions) && isSynced(conditions)
// }

// func readyAndUpToDate(generation int64, commonStatus v1alpha1.CommonStatus) bool {
// 	return isReady(commonStatus.Conditions) && isUpToDate(generation, commonStatus)
// }

func getStorageType(storageClassName string) string {
	// storageClassName
	// fmt.Sprintf("%s-%d-%d", volumeType, iops, throughput)
	// e.g. gp3-3000-400, gp3-6000-625, io2-4000, local-ssd
	if storageClassName == localSSD {
		return storageClassName
	}

	if strings.HasPrefix(storageClassName, cloudAuto) {
		return cloudAuto
	}

	args := strings.Split(storageClassName, "-")
	if len(args) == 0 {
		return ""
	}

	return args[0]
}

func getStorageIops(storageClassName string) uint32 {
	// storageClassName has format fmt.Sprintf("%s-%d-%d", volumeType, iops, throughput)
	var str string
	if strings.HasPrefix(storageClassName, cloudAuto) {
		// e.g. cloud-auto-3000
		args := strings.Split(storageClassName, "-")
		if len(args) < 3 {
			return 0
		}
		str = args[2]
	} else {
		// e.g. gp3-3000-400, gp3-6000-625, io2-4000, local-ssd
		args := strings.Split(storageClassName, "-")
		if len(args) < 2 {
			return 0
		}
		str = args[1]
	}
	iops, err := strconv.ParseUint(str, 10, 32)
	if err != nil {
		return 0
	}
	return uint32(iops)
}

func getStorageThroughput(storageClassName string) uint32 {
	// storageClassName has format fmt.Sprintf("%s-%d-%d", volumeType, iops, throughput)
	var str string
	if strings.HasPrefix(storageClassName, cloudAuto) {
		// e.g. cloud-auto-3000
		return 0
	} else {
		// e.g. gp3-3000-400, gp3-6000-625, io2-4000, local-ssd
		args := strings.Split(storageClassName, "-")
		if len(args) < 3 {
			return 0
		}
		str = args[2]
	}
	throughput, err := strconv.ParseUint(str, 10, 32)
	if err != nil {
		return 0
	}
	return uint32(throughput)
}

func cvtStorageNameToEn(name string) (apiv1.Component_Storage_Name, error) {
	switch schemav1.StorageName(name) {
	case schemav1.StorageData:
		return apiv1.Component_Storage_DATA, nil
	case schemav1.StorageRaftLog:
		return apiv1.Component_Storage_RAFTLOG, nil
	case schemav1.StorageRaftDB: // deprecated
		return apiv1.Component_Storage_RAFTDB, nil
	case schemav1.StorageSlowLog:
		return apiv1.Component_Storage_SLOWLOG, nil
	default:
		return apiv1.Component_Storage_NAME_UNSPECIFIED, fmt.Errorf("unspecified storage %s", name)
	}
}

func CvtNodeSpecToQuota(labels, annotations map[string]string, replicas uint32, resourceList corev1.ResourceList, nodeSelector map[string]string) (*entity.NodeQuota, error) {
	nodeSpec := labels[schemav1.LabelNodeSpecTypeKey]

	cpu, memory, err := utils.ParseNodeSpec(nodeSpec)
	if err != nil {
		return nil, fmt.Errorf("failed to parse node spec %s: %w", nodeSpec, err)
	}

	withLocalDisk := labels[schemav1.LabelUseWithLocalDisk] == "true"

	localDiskSize := uint32(0)
	if withLocalDisk {
		if v, ok := labels[schemav1.LabelUseLocalDiskSize]; ok {
			size, err := strconv.ParseUint(v, 10, 64)
			if err != nil {
				return nil, fmt.Errorf("failed to local disk size %s: %w", v, err)
			}

			localDiskSize = uint32(size)
		}
	}

	// Extract node version from nodeSelector if present
	nodeVersion := ""
	if nodeSelector != nil {
		nodeVersion = nodeSelector[schemav1.K8sVersionLabelKey]
	}

	quota := &entity.NodeQuota{
		NodeSpec: &entity.NodeSpec{
			Cpu:             uint32(cpu),
			MemoryGi:        uint32(memory),
			WithLocalDisk:   withLocalDisk,
			LocalDiskSizeGi: localDiskSize,
			NodeVersion:     nodeVersion,
		},
		NodeCount: replicas,
	}

	if resourceList != nil {
		if cpuResource, ok := resourceList[corev1.ResourceCPU]; ok {
			quota.NodeSpec.AvailableCpu = uint32(cpuResource.AsApproximateFloat64())
		}
		if memoryResource, ok := resourceList[corev1.ResourceMemory]; ok {
			quota.NodeSpec.AvailableMemoryMi = uint32(memoryResource.AsApproximateFloat64() / 1024 / 1024) // convert to MiB
		}
	}

	minCount := annotations[schemav1.AnnotationsComponentMinCount]
	if minCount != "" {
		minCountInt, err := strconv.Atoi(minCount)
		if err != nil {
			return nil, fmt.Errorf("failed to parse min count %s: %w", minCount, err)
		}
		quota.MinNodeCount = uint32(minCountInt)
	}

	maxCount := annotations[schemav1.AnnotationsComponentMaxCount]
	if maxCount != "" {
		maxCountInt, err := strconv.Atoi(maxCount)
		if err != nil {
			return nil, fmt.Errorf("failed to parse max count %s: %w", maxCount, err)
		}
		quota.MaxNodeCount = uint32(maxCountInt)
	}
	return quota, nil
}

func checkNodeQuota(currNodeQuota, nodeQuota *entity.NodeQuota) error {
	if nodeQuota == nil {
		return nil
	}

	if nodeQuota.GetNodeCount() > currNodeQuota.GetMaxNodeCount() {
		return fmt.Errorf("node count %d is greater than max node count %d", nodeQuota.GetNodeCount(), currNodeQuota.GetMaxNodeCount())
	}

	if nodeQuota.GetNodeCount() < currNodeQuota.GetMinNodeCount() {
		return fmt.Errorf("node count %d is less than min node count %d", nodeQuota.GetNodeCount(), currNodeQuota.GetMinNodeCount())
	}
	return nil
}

func setNodeSpec(nodeSpec *entity.NodeSpec, labels, nodeSelector map[string]string, compType apiv1.ComponentType) (corev1.ResourceRequirements, error) {
	if nodeSpec == nil {
		return corev1.ResourceRequirements{}, fmt.Errorf("node spec is nil")
	}

	resourceRequirements := corev1.ResourceRequirements{
		Requests: corev1.ResourceList{},
		Limits:   corev1.ResourceList{},
	}
	sidecarCPUMilli, sidecarMemInMiB := SidecarResourcesFor(compType)
	setMainContainerResources(&resourceRequirements, nodeSpec, sidecarCPUMilli, sidecarMemInMiB)

	// set node spec
	if labels == nil {
		labels = map[string]string{}
	}

	labels[schemav1.LabelNodeSpecTypeKey] = fmt.Sprintf("%dC%dG", nodeSpec.Cpu, nodeSpec.MemoryGi)

	if nodeSelector == nil {
		nodeSelector = map[string]string{}
	}

	nodeSelector["node-spec"] = fmt.Sprintf("%dC%dG", nodeSpec.Cpu, nodeSpec.MemoryGi)

	return resourceRequirements, nil
}

// ref: https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-architecture#node_allocatable
// and looks like Azure and EKS share the same algorithm https://github.com/awslabs/amazon-eks-ami/issues/387#issuecomment-582419188
// https://docs.microsoft.com/en-us/azure/aks/concepts-clusters-workloads#resource-reservations
var memTaxTable = []struct {
	boundary       uint64
	rate           float64
	quickDeduction uint64
}{
	{
		boundary:       1 * 1024, // 1GB
		rate:           0.0,
		quickDeduction: 255,
	},
	{
		boundary:       4 * 1024, // 4GB
		rate:           0.25,
		quickDeduction: 0,
	},
	{
		boundary:       8 * 1024, // 8GB
		rate:           0.2,
		quickDeduction: 205, // 0.05 * 4GB rounded
	},
	{
		boundary:       16 * 1024,
		rate:           0.1,
		quickDeduction: 1, // 0.1 * 4GB + 0.15 * 4GB
	},
	{
		boundary:       128 * 1024, // 128GB
		rate:           0.06,
		quickDeduction: 1680, // 0.19 * 4GB + 0.14 * 4GB + 0.04 * 8GB rounded
	},
	{
		boundary:       math.MaxInt64,
		rate:           0.02,
		quickDeduction: 6922, // (0.25 - 0.02) * 4 + (0.2 - 0.02) * 4 + (0.1 - 0.02) * 8 + (0.06 - 0.02) * 112 rounded
	},
}

const reservedMemoryInMiB uint64 = 1536

// ComputeAvailableMemory computes the actual memory available to pods after reserving memory for system components and daemon pods.
func ComputeAvailableMemory(memoryInMiB uint64) uint64 {
	for _, level := range memTaxTable {
		// ASC order of memTaxTable is guaranteed
		if memoryInMiB <= level.boundary {
			tax := uint64(math.Ceil(float64(memoryInMiB)*level.rate)) + level.quickDeduction
			if memoryInMiB > tax {
				memoryInMiB -= tax
			} else {
				memoryInMiB = 0
			}
			break
		}
	}
	// subtract the reserved memory for daemon pods
	if memoryInMiB > reservedMemoryInMiB {
		memoryInMiB -= reservedMemoryInMiB
	} else {
		memoryInMiB = 0
	}
	return memoryInMiB
}

// ComputeRequestMemory is the scheduling Request for a main container given yaml availableMemoryMi
// (already allocatable minus DaemonSet) and sidecar Request. It leaves a size-dependent reserve
// so a later DaemonSet bump or an overstated untested availableMemoryMi does not fail Admit.
// ComputeAvailableMemory is unchanged and remains the capacity→allocatable estimator.
func ComputeRequestMemory(availMemInMiB, sidecarMemInMiB uint64) uint64 {
	raw := availMemInMiB * requestMemoryReservePercent / 100
	reserve := min(requestMemoryReserveMaxMi, max(requestMemoryReserveMinMi, raw))
	remain := subtractSidecar(availMemInMiB, sidecarMemInMiB)
	if remain < reserve {
		return remain
	}
	return remain - reserve
}

// ComputeRequestCPU is the scheduling CPU Request for a main container given
// availableCpuMilli (allocatable minus DaemonSet) and sidecar millicores. It
// leaves avail×5% clamped to [100m, 200m] so a later DaemonSet CPU bump does
// not fail Admit. If the reserve would punch through, keep avail−sidecar.
func ComputeRequestCPU(availCPUMilli, sidecarCPUMilli uint64) uint64 {
	raw := availCPUMilli * requestCPUReservePercent / 100
	reserve := min(requestCPUReserveMaxMilli, max(requestCPUReserveMinMilli, raw))
	remain := subtractSidecar(availCPUMilli, sidecarCPUMilli)
	if remain < reserve {
		return remain
	}
	return remain - reserve
}

// SidecarResourcesFor returns the sidecar CPU millicores and memory MiB to subtract from the
// main container Request so the whole Pod still fits allocatable.
func SidecarResourcesFor(compType apiv1.ComponentType) (cpuMilli, memMiB uint64) {
	switch compType {
	case apiv1.ComponentType_TIDB:
		return TiDBSidecarCPUMilli, TiDBSidecarMemoryMiB
	case apiv1.ComponentType_TIDB_WORKER:
		return TiDBWorkerSidecarCPUMilli, TiDBWorkerSidecarMemoryMiB
	case apiv1.ComponentType_TIFLASH_WRITE,
		apiv1.ComponentType_TIFLASH_COMPUTE,
		apiv1.ComponentType_TIFLASH_LEARNER,
		apiv1.ComponentType_TIFLASH_COMPUTE_LEARNER:
		return TiFlashSidecarCPUMilli, TiFlashSidecarMemoryMiB
	default:
		return 0, 0
	}
}

// EffectiveAvailableCPUMilli is availableCpuMilli, else availableCpu*1000, else (cpu-1)*1000.
func EffectiveAvailableCPUMilli(nodeSpec *entity.NodeSpec) uint64 {
	if nodeSpec == nil {
		return 0
	}
	if nodeSpec.GetAvailableCpuMilli() > 0 {
		return uint64(nodeSpec.GetAvailableCpuMilli())
	}
	if nodeSpec.GetAvailableCpu() > 0 {
		return uint64(nodeSpec.GetAvailableCpu()) * 1000
	}
	if nodeSpec.GetCpu() > 1 {
		return uint64(nodeSpec.GetCpu()-1) * 1000
	}
	return 0
}

// EffectiveMaxCPUMilli is maxCpuMilli, else EffectiveAvailableCPUMilli.
func EffectiveMaxCPUMilli(nodeSpec *entity.NodeSpec) uint64 {
	if nodeSpec != nil && nodeSpec.GetMaxCpuMilli() > 0 {
		return uint64(nodeSpec.GetMaxCpuMilli())
	}
	return EffectiveAvailableCPUMilli(nodeSpec)
}

// EffectiveMaxMemoryMi is maxMemoryMi, else availableMemoryMi. Does not fall back to memoryGi.
func EffectiveMaxMemoryMi(nodeSpec *entity.NodeSpec) uint64 {
	if nodeSpec == nil {
		return 0
	}
	if nodeSpec.GetMaxMemoryMi() > 0 {
		return uint64(nodeSpec.GetMaxMemoryMi())
	}
	return uint64(nodeSpec.GetAvailableMemoryMi())
}

func subtractSidecar(avail, sidecar uint64) uint64 {
	if avail > sidecar {
		return avail - sidecar
	}
	return 0
}

// setMainContainerResources writes CPU and memory Request/Limit for a main
// container. Limit uses Max (falling back to Avail); Request uses
// ComputeRequestCPU / ComputeRequestMemory. Request is not clamped to Limit:
// both formulas are already ≤ avail−sidecar, and Max ≥ Avail is the catalog invariant.
func setMainContainerResources(resourceRequirements *corev1.ResourceRequirements, nodeSpec *entity.NodeSpec, sidecarCPUMilli, sidecarMemInMiB uint64) {
	if resourceRequirements == nil || nodeSpec == nil {
		return
	}
	if resourceRequirements.Requests == nil {
		resourceRequirements.Requests = corev1.ResourceList{}
	}
	if resourceRequirements.Limits == nil {
		resourceRequirements.Limits = corev1.ResourceList{}
	}

	availMem := uint64(nodeSpec.GetAvailableMemoryMi())
	limitMem := EffectiveMaxMemoryMi(nodeSpec)
	if availMem > 0 {
		requestMem := ComputeRequestMemory(availMem, sidecarMemInMiB)
		resourceRequirements.Requests[corev1.ResourceMemory] = *resource.NewQuantity(int64(requestMem)*1024*1024, resource.BinarySI)
	}
	if limitMem > 0 {
		resourceRequirements.Limits[corev1.ResourceMemory] = *resource.NewQuantity(int64(limitMem)*1024*1024, resource.BinarySI)
	}

	availCPU := EffectiveAvailableCPUMilli(nodeSpec)
	limitCPU := EffectiveMaxCPUMilli(nodeSpec)
	if availCPU > 0 {
		resourceRequirements.Requests[corev1.ResourceCPU] = *resource.NewMilliQuantity(int64(ComputeRequestCPU(availCPU, sidecarCPUMilli)), resource.DecimalSI)
	}
	if limitCPU > 0 {
		resourceRequirements.Limits[corev1.ResourceCPU] = *resource.NewMilliQuantity(int64(limitCPU), resource.DecimalSI)
	}
}
