package v1

import (
	"crypto/md5"
	"fmt"
	"sort"
	"strings"
)

const (
	DefaultGKEComputeClassForTiKV = "tikv-priority"
	DefaultGKEComputeClassForTiDB = "tidb-priority"
)

// DefaultGKEComputeClassNameForTiKV keeps the legacy static name when there is no
// cluster identity yet, but switches to a stable hashed name once the cluster ID exists.
func DefaultGKEComputeClassNameForTiKV(clusterID string) string {
	return defaultGKEComputeClassName(clusterID, ComponentTiKV)
}

func DefaultGKEComputeClassNameForTiDB(clusterID string) string {
	return defaultGKEComputeClassName(clusterID, ComponentTiDB)
}

func DefaultGKEComputeClassNameForTiDBGroup(clusterID, groupID string) string {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" {
		return DefaultGKEComputeClassNameForTiDB(clusterID)
	}
	return newGKEComputeClassName(strings.TrimSpace(clusterID)+"-"+groupID, string(ComponentTiDB), "priority", gkeResourceNameLimit)
}

func GKEComputeClassNameForBindings(bindings []GKEComputeClassNodePoolBinding) (string, error) {
	hash, err := GKEComputeClassBindingsHash(bindings)
	if err != nil {
		return "", err
	}
	return strings.Join(nonEmptyStrings("compute-class", hash, "priority"), "-"), nil
}

func defaultGKEComputeClassName(clusterID string, component ComponentType) string {
	clusterID = strings.TrimSpace(clusterID)
	if clusterID == "" {
		switch component {
		case ComponentTiDB:
			return DefaultGKEComputeClassForTiDB
		default:
			return DefaultGKEComputeClassForTiKV
		}
	}
	return newGKEComputeClassName(clusterID, string(component), "priority", gkeResourceNameLimit)
}

// newGKEComputeClassName truncates the identity segment but keeps a short hash so
// generated names remain deterministic and collision-resistant under GKE length limits.
func newGKEComputeClassName(identity, prefix, suffix string, limit int) string {
	return newGKEComputeClassNameWithHashInput(identity, identity, prefix, suffix, limit)
}

// newGKEComputeClassNameWithHashInput truncates the identity segment but keeps a
// short hash so generated names remain deterministic and collision-resistant
// under GKE length limits.
func newGKEComputeClassNameWithHashInput(identity, hashInput, prefix, suffix string, limit int) string {
	const hashSize = 8
	identity = strings.TrimSpace(identity)
	normalizedIdentity := NormalizeGKEResourceNameIdentity(identity)
	if limit <= 0 || limit > gkeResourceNameLimit {
		limit = gkeResourceNameLimit
	}
	length := limit - hashSize - 1
	if prefix != "" {
		length = length - len(prefix) - 1
	}
	if suffix != "" {
		length = length - len(suffix) - 1
	}
	if length > len(normalizedIdentity) {
		length = len(normalizedIdentity)
	}
	if length < 0 {
		length = 0
	}

	hash := fmt.Sprintf("%x", md5.Sum([]byte(hashInput)))[:hashSize]
	return strings.Join(nonEmptyStrings(prefix, normalizedIdentity[:length], hash, suffix), "-")
}

func NormalizeGKEResourceNameIdentity(identity string) string {
	identity = strings.ToLower(strings.TrimSpace(identity))
	var out strings.Builder
	lastHyphen := false
	for _, r := range identity {
		valid := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
		if valid {
			out.WriteRune(r)
			lastHyphen = false
			continue
		}
		if !lastHyphen {
			out.WriteByte('-')
			lastHyphen = true
		}
	}
	return strings.Trim(out.String(), "-")
}

func nonEmptyStrings(items ...string) []string {
	out := make([]string, 0, len(items))
	for _, item := range items {
		if item != "" {
			out = append(out, item)
		}
	}
	return out
}

func (c *Cluster) GKEComputeClassNameForTiKV() string {
	annotation, ok := c.tiKVGKEComputeClassBindingAnnotation()
	if !ok {
		return ""
	}
	return annotation.ComputeClassName
}

func (c *Cluster) GKEComputeClassNameForTiDB() string {
	return c.GKEComputeClassNameForTiDBGroup("")
}

func (c *Cluster) GKEComputeClassNameForTiDBGroup(groupID string) string {
	// Only active groups should surface a compute-class name; stale group annotations
	// must not look like a live heterogeneous target.
	annotation, ok := c.tiDBGKEComputeClassBindingAnnotation(groupID, "")
	if !ok {
		return ""
	}
	return annotation.ComputeClassName
}

func (c *Cluster) TiDBGKEComputeClassBindingsAnnoKey(groupID string) string {
	return c.tiDBGKEComputeClassAnnoKey(AnnoKeyGKEComputeClassForTiDBBindings, groupID)
}

// The default TiDB group reuses the unsuffixed binding annotation. Extra groups
// add the group suffix so their compute-class intent stays isolated.
func (c *Cluster) tiDBGKEComputeClassAnnoKey(baseKey, groupID string) string {
	groupID = c.tiDBGKEComputeClassGroupID(groupID)
	if groupID == "" || groupID == strings.TrimSpace(c.GetDefaultGroupID()) {
		return baseKey
	}
	return baseKey + "-" + groupID
}

func (c *Cluster) tiDBGKEComputeClassGroupID(groupID string) string {
	groupID = strings.TrimSpace(groupID)
	if groupID != "" {
		return groupID
	}
	return strings.TrimSpace(c.GetDefaultGroupID())
}

func (c *Cluster) TiKVGKEComputeClassIntentConfigured() bool {
	if c == nil || c.GetProviderType() != ProviderTypeGKE {
		return false
	}
	return strings.TrimSpace(c.GetAnnotations()[AnnoKeyGKEComputeClassForTiKVBindings]) != ""
}

func (c *Cluster) GKEComputeClassForTiKVEnabled() bool {
	_, ok := c.tiKVGKEComputeClassBindingAnnotation()
	return ok
}

type memberInstanceTypePolicy struct {
	baseInstanceType     string
	heterogeneousEnabled bool
	allowedInstanceTypes map[string]struct{}
}

func (p memberInstanceTypePolicy) allows(instanceType string) bool {
	return memberInstanceTypeAllowed(instanceType, p.baseInstanceType, p.heterogeneousEnabled, p.allowedInstanceTypes)
}

func memberInstanceTypeAllowed(instanceType, baseInstanceType string, heterogeneousEnabled bool, allowedInstanceTypes map[string]struct{}) bool {
	if instanceType == "" {
		return true
	}
	if baseInstanceType == "" {
		return false
	}
	if instanceType == baseInstanceType {
		return true
	}
	if !heterogeneousEnabled {
		return false
	}
	_, ok := allowedInstanceTypes[instanceType]
	return ok
}

func (c *Cluster) TiKVGKEComputeClassInstanceTypes() map[string]struct{} {
	annotation, ok := c.tiKVGKEComputeClassBindingAnnotation()
	if !ok {
		return map[string]struct{}{}
	}
	return GKEComputeClassBindingAnnotationInstanceTypes(annotation)
}

func (c *Cluster) GKEComputeClassForTiKVNodePool(instanceType string) bool {
	if c == nil || instanceType == "" {
		return false
	}

	_, ok := c.TiKVGKEComputeClassInstanceTypes()[instanceType]
	return ok
}

func (c *Cluster) TiKVMemberInstanceTypeAllowed(instanceType string) bool {
	if c == nil {
		return false
	}
	return c.tiKVMemberInstanceTypePolicy().allows(instanceType)
}

func (c *Cluster) tiKVMemberInstanceTypePolicy() memberInstanceTypePolicy {
	annotation, ok := c.tiKVGKEComputeClassBindingAnnotation()
	allowedInstanceTypes := map[string]struct{}{}
	if ok {
		allowedInstanceTypes = GKEComputeClassBindingAnnotationInstanceTypes(annotation)
	}
	return memberInstanceTypePolicy{
		baseInstanceType:     c.Spec.Cluster.TiKV.InstanceType,
		heterogeneousEnabled: ok,
		allowedInstanceTypes: allowedInstanceTypes,
	}
}

func (c *Cluster) TiDBGKEComputeClassInstanceTypes(groupID, desiredInstanceType string) map[string]struct{} {
	annotation, ok := c.tiDBGKEComputeClassBindingAnnotation(groupID, desiredInstanceType)
	if !ok {
		return map[string]struct{}{}
	}
	return GKEComputeClassBindingAnnotationInstanceTypes(annotation)
}

func (c *Cluster) GKEComputeClassForTiDBNodePool(instanceType, groupID, desiredInstanceType string) bool {
	if c == nil || instanceType == "" {
		return false
	}
	_, ok := c.TiDBGKEComputeClassInstanceTypes(groupID, desiredInstanceType)[instanceType]
	return ok
}

func (c *Cluster) TiDBMemberInstanceTypeAllowed(instanceType string) bool {
	if c == nil {
		return false
	}
	return c.TiDBMemberInstanceTypeAllowedForGroup(instanceType, "", c.Spec.Cluster.TiDB.InstanceType)
}

// TiDB heterogeneous rules are group-scoped. The default group and extra groups
// can be enabled independently, so callers must pass both the observed member
// instance type and the group's desired/base instance type.
func (c *Cluster) TiDBMemberInstanceTypeAllowedForGroup(instanceType, groupID, desiredInstanceType string) bool {
	if c == nil {
		return false
	}
	return c.tiDBMemberInstanceTypePolicy(groupID, desiredInstanceType).allows(instanceType)
}

func (c *Cluster) tiDBMemberInstanceTypePolicy(groupID, desiredInstanceType string) memberInstanceTypePolicy {
	allowedInstanceTypes := c.TiDBGKEComputeClassInstanceTypes(groupID, desiredInstanceType)
	return memberInstanceTypePolicy{
		baseInstanceType:     desiredInstanceType,
		heterogeneousEnabled: len(allowedInstanceTypes) > 0,
		allowedInstanceTypes: allowedInstanceTypes,
	}
}

func (c *Cluster) GKEComputeClassForTiDBDefaultGroupEnabled() bool {
	if c == nil {
		return false
	}
	return c.GKEComputeClassForTiDBGroupEnabled(c.GetDefaultGroupID())
}

func (c *Cluster) GKEComputeClassForTiDBExtraGroupEnabled(groupID string) bool {
	if c == nil {
		return false
	}
	groupID = strings.TrimSpace(groupID)
	if groupID == "" || groupID == strings.TrimSpace(c.GetDefaultGroupID()) {
		return false
	}
	return c.GKEComputeClassForTiDBGroupEnabled(groupID)
}

func (c *Cluster) GKEComputeClassForTiDBGroupEnabled(groupID string) bool {
	_, ok := c.tiDBGKEComputeClassBindingAnnotation(groupID, "")
	return ok
}

func (c *Cluster) TiDBGKEComputeClassIntentConfigured(groupID string) bool {
	if c == nil || c.GetProviderType() != ProviderTypeGKE {
		return false
	}
	groupID = c.tiDBGKEComputeClassGroupID(groupID)
	if !c.tiDBGKEComputeClassGroupExists(groupID) {
		return false
	}
	return strings.TrimSpace(c.GetAnnotations()[c.TiDBGKEComputeClassBindingsAnnoKey(groupID)]) != ""
}

func (c *Cluster) tiDBGKEComputeClassGroupExists(groupID string) bool {
	groupID = strings.TrimSpace(groupID)
	if groupID == "" || groupID == strings.TrimSpace(c.GetDefaultGroupID()) {
		return true
	}
	_, ok := c.GetExtraGroupSpec(groupID)
	return ok
}

// TiDBComputeClassAnnotationRef identifies one configured TiDB compute-class binding annotation.
type TiDBComputeClassAnnotationRef struct {
	TiDBGroupID   string
	AnnotationKey string
}

// ConfiguredTiDBComputeClassAnnotationRefs returns all non-empty TiDB compute-class binding annotations.
func (c *Cluster) ConfiguredTiDBComputeClassAnnotationRefs() []TiDBComputeClassAnnotationRef {
	if c == nil {
		return nil
	}
	annotations := c.GetAnnotations()
	out := make([]TiDBComputeClassAnnotationRef, 0, len(c.Spec.ExtraGroups)+1)
	if strings.TrimSpace(annotations[AnnoKeyGKEComputeClassForTiDBBindings]) != "" {
		// The unsuffixed TiDB annotation represents the default group. Keep an
		// empty defaultGroupID as the existing default-group sentinel.
		out = append(out, TiDBComputeClassAnnotationRef{
			TiDBGroupID:   c.GetDefaultGroupID(),
			AnnotationKey: AnnoKeyGKEComputeClassForTiDBBindings,
		})
	}
	prefix := AnnoKeyGKEComputeClassForTiDBBindings + "-"
	suffixedAnnotationKeys := make([]string, 0)
	for annoKey, value := range annotations {
		groupID := strings.TrimSpace(strings.TrimPrefix(annoKey, prefix))
		if strings.TrimSpace(value) == "" || !strings.HasPrefix(annoKey, prefix) || groupID == "" {
			continue
		}
		suffixedAnnotationKeys = append(suffixedAnnotationKeys, annoKey)
	}
	sort.Strings(suffixedAnnotationKeys)
	for _, annoKey := range suffixedAnnotationKeys {
		out = append(out, TiDBComputeClassAnnotationRef{
			TiDBGroupID:   strings.TrimSpace(strings.TrimPrefix(annoKey, prefix)),
			AnnotationKey: annoKey,
		})
	}
	return out
}

// ActiveTiDBGKEComputeClassGroupIDs is the controller-facing view of which TiDB
// groups still own heterogeneous intent and should keep their nodepool resources.
// It is intentionally driven by explicit compute-class binding annotations, not
// by current TiDB replicas: replicas=0 stops runtime nodepool generation, but it
// does not delete the heterogeneous binding.
func (c *Cluster) ActiveTiDBGKEComputeClassGroupIDs() []string {
	if c == nil {
		return nil
	}
	out := make([]string, 0, len(c.Spec.ExtraGroups)+1)
	defaultGroupID := strings.TrimSpace(c.GetDefaultGroupID())
	if c.GKEComputeClassForTiDBGroupEnabled(defaultGroupID) {
		out = append(out, defaultGroupID)
	}
	for _, extraGroup := range c.GetExtraGroups() {
		if c.GKEComputeClassForTiDBGroupEnabled(extraGroup.Name) {
			out = append(out, extraGroup.Name)
		}
	}
	return out
}

func (c *Cluster) TiDBInstanceTypeForGroup(groupID string) string {
	if c == nil {
		return ""
	}
	component, err := c.GetComponentSpecInGroup(c.tiDBGKEComputeClassGroupID(groupID), ComponentTiDB)
	if err != nil || component == nil {
		return ""
	}
	return strings.TrimSpace(component.GetInstanceType())
}

func (c *Cluster) tiKVGKEComputeClassBindingAnnotation() (GKEComputeClassBindingAnnotation, bool) {
	if !c.TiKVGKEComputeClassIntentConfigured() {
		return GKEComputeClassBindingAnnotation{}, false
	}
	return c.gkeComputeClassBindingAnnotation(
		AnnoKeyGKEComputeClassForTiKVBindings,
		strings.TrimSpace(c.Spec.Cluster.TiKV.InstanceType),
	)
}

func (c *Cluster) tiDBGKEComputeClassBindingAnnotation(groupID, baseInstanceType string) (GKEComputeClassBindingAnnotation, bool) {
	if c == nil || c.GetProviderType() != ProviderTypeGKE {
		return GKEComputeClassBindingAnnotation{}, false
	}
	groupID = c.tiDBGKEComputeClassGroupID(groupID)
	if !c.TiDBGKEComputeClassIntentConfigured(groupID) {
		return GKEComputeClassBindingAnnotation{}, false
	}
	baseInstanceType = strings.TrimSpace(baseInstanceType)
	if baseInstanceType == "" {
		baseInstanceType = c.TiDBInstanceTypeForGroup(groupID)
	}
	return c.gkeComputeClassBindingAnnotation(c.TiDBGKEComputeClassBindingsAnnoKey(groupID), baseInstanceType)
}

func (c *Cluster) gkeComputeClassBindingAnnotation(annotationKey, baseInstanceType string) (GKEComputeClassBindingAnnotation, bool) {
	annotation, explicit, err := ClusterGKEComputeClassBindingAnnotation(
		c,
		annotationKey,
		baseInstanceType,
		"",
	)
	if err != nil || !explicit {
		return GKEComputeClassBindingAnnotation{}, false
	}
	return annotation, true
}
