package utils

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"golang.org/x/mod/semver"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
	reconcileerr "github.com/tidbcloud/infra-provider/pkg/controllers/common/error"
)

const (
	ClusterPrefix             = "db"
	annMigrationSkipReconcile = "tags.tidbcloud.com/migrated-from-central"

	// DryrunAnnotation instruct the controller to dryrun write operations, DryrunAnnotation is
	// not part of the Infra API thus has no behavior guarantee
	DryrunAnnotation = "internal.tidbcloud.com/dryrun"

	KeyDedicated  = "dedicated"
	Key3LayerArch = "3-layer-arch"
	ValTrue       = "true"

	// The minimum TiDB version that support `tidb_token_auth`
	minTokenBasedAuthVersion = "v6.4.0"

	// GCP nodepool's disk type
	diskTypePdStandard        = "pd-standard"
	diskTypeHyperdiskBalanced = "hyperdisk-balanced" // only used for N4 node
)

// backup&restore related
const (
	S3GatewayEndpointEnv = "S3_GATEWAY_ENDPOINT"

	// BRPriorityClassName is the PriorityClass name for backup and restore pods.
	// It's a cluster-scoped resource shared by all backup/restore jobs in the same dataplane.
	BRPriorityClassName = "tidb-cluster-backup-critical"

	// BRPriorityClassValue (900000000) is set above system-critical priorities
	// to ensure backup and restore pods are not evicted by node pressure. This is intentionally
	// higher than default system-critical priorities (usually < 1000000000) to protect
	// data integrity during backup and restore operations. The value aligns with cloud-native arch.
	BRPriorityClassValue = 900000000
)

// GetMemberType returns the v1alpha1.MemberType from the cloudv1.ComponentType
func GetMemberType(t cloudv1.ComponentType) v1alpha1.MemberType {
	switch t {
	case cloudv1.ComponentTiDB, cloudv1.ComponentAdminTiDB:
		return v1alpha1.TiDBMemberType
	case cloudv1.ComponentTiKV:
		return v1alpha1.TiKVMemberType
	case cloudv1.ComponentPD:
		return v1alpha1.PDMemberType
	case cloudv1.ComponentTiFlash:
		return v1alpha1.TiFlashMemberType
	case cloudv1.ComponentTiCDC:
		return v1alpha1.TiCDCMemberType
	case cloudv1.ComponentTiProxy:
		return v1alpha1.TiProxyMemberType
	default:
		return v1alpha1.UnknownMemberType
	}
}

func ParsingLegacyId(n *cloudv1.Cluster, labelKey string) *uint64 {
	parsed, err := strconv.ParseUint(n.Labels[labelKey], 10, 64)
	if err != nil {
		return nil
	} else {
		return &parsed
	}
}

// CopyLabelsForWorkloads copies label for workloads from infra API CRs.
func CopyLabelsForWorkloads(labels map[string]string) map[string]string {
	keys := []string{
		cloudv1.TenantTagKey,
		cloudv1.ProjectTagKey,
		cloudv1.ClusterTagKey,
	}
	ret := make(map[string]string)
	for _, key := range keys {
		if value, ok := labels[key]; ok {
			ret[key] = value
		}
	}
	return ret
}

// TODO(aylei): remove rule-based cluster access, the cluster status should expose these dynamically
// Deprecated
func GetExtraAccessSvcName(tcName string) string {
	return fmt.Sprintf("%s-tidb-extra-service", tcName)
}

// Deprecated
func GetExtraAccessSecretName(tcName string) string {
	return fmt.Sprintf("%s-tidb-extra-secret", tcName)
}

// Deprecated
func GetPrivilegedTiDBAddress(tcName, ns string) string {
	return fmt.Sprintf("%s.%s", GetExtraAccessSvcName(tcName), ns)
}

func GetMigrationAnnotationSkipPredicator() predicate.Funcs {
	return predicate.NewPredicateFuncs(func(o client.Object) bool {
		if _, found := o.GetAnnotations()[annMigrationSkipReconcile]; found {
			return false
		}

		return true
	})
}

func GetPredicateForProviderTypes(providerTypes ...string) predicate.Funcs {
	return predicate.NewPredicateFuncs(func(o client.Object) bool {
		objPT := o.GetLabels()[cloudv1.ProviderTypeLabel]
		for _, t := range providerTypes {
			if objPT == t {
				return true
			}
		}
		return false
	})
}

func HasDryrunAnnotation(obj client.Object) bool {
	_, ok := obj.GetAnnotations()[DryrunAnnotation]
	return ok
}

func Toleration(comp cloudv1.ComponentType) []corev1.Toleration {
	return []corev1.Toleration{
		{
			Key:      KeyDedicated,
			Operator: corev1.TolerationOpEqual,
			Value:    fmt.Sprintf("tidb-%s", string(comp)),
			Effect:   corev1.TaintEffectNoSchedule,
		},
	}
}

func TolerationWithUK(comp cloudv1.ComponentType, uk string) []corev1.Toleration {
	return []corev1.Toleration{
		{
			Key:      KeyDedicated,
			Operator: corev1.TolerationOpEqual,
			Value:    fmt.Sprintf("%s-%s", string(comp), uk),
			Effect:   corev1.TaintEffectNoSchedule,
		},
	}
}

func TolerationWith3LayerArch() *corev1.Toleration {
	return &corev1.Toleration{
		Key:      Key3LayerArch,
		Operator: corev1.TolerationOpEqual,
		Value:    ValTrue,
		Effect:   corev1.TaintEffectNoSchedule,
	}
}

func TolerationWithArch(arch string) *corev1.Toleration {
	return &corev1.Toleration{
		Key:      consts.ArchLabelKey,
		Operator: corev1.TolerationOpEqual,
		Value:    arch,
		Effect:   corev1.TaintEffectNoSchedule,
	}
}

func TolerationWithComponentShared() []corev1.Toleration {
	return []corev1.Toleration{
		{
			Key:      KeyDedicated,
			Operator: corev1.TolerationOpEqual,
			Value:    fmt.Sprintf("tidb-%s", common.ComponentShared),
			Effect:   corev1.TaintEffectNoSchedule,
		},
	}
}

func IsMatchedToleration(tolerations []corev1.Toleration, comp cloudv1.ComponentType) bool {
	for _, toleration := range tolerations {
		if toleration.Key == KeyDedicated &&
			toleration.Operator == corev1.TolerationOpEqual &&
			toleration.Effect == corev1.TaintEffectNoSchedule &&
			toleration.Value == fmt.Sprintf("tidb-%s", string(comp)) {
			return true
		}
	}
	return false
}

func Taint(comp cloudv1.ComponentType) []corev1.Taint {
	return []corev1.Taint{
		{
			Key:    KeyDedicated,
			Value:  fmt.Sprintf("tidb-%s", string(comp)),
			Effect: corev1.TaintEffectNoSchedule,
		},
	}
}

func TaintWithUk(comp cloudv1.ComponentType, uk string) []corev1.Taint {
	return []corev1.Taint{
		{
			Key:    KeyDedicated,
			Value:  fmt.Sprintf("%s-%s", string(comp), uk),
			Effect: corev1.TaintEffectNoSchedule,
		},
	}
}

func TaintWith3LayerArch() *corev1.Taint {
	return &corev1.Taint{
		Key:    Key3LayerArch,
		Value:  ValTrue,
		Effect: corev1.TaintEffectNoSchedule,
	}
}

// TaintWithArch returns the architecture taint used by GKE ARM node pools.
func TaintWithArch(arch string) *corev1.Taint {
	return &corev1.Taint{
		Key:    consts.ArchLabelKey,
		Value:  arch,
		Effect: corev1.TaintEffectNoSchedule,
	}
}

func TaintWithComponentShared() []corev1.Taint {
	return []corev1.Taint{
		{
			Key:    KeyDedicated,
			Value:  fmt.Sprintf("tidb-%s", common.ComponentShared),
			Effect: corev1.TaintEffectNoSchedule,
		},
	}
}

func Affinity(comp cloudv1.ComponentType) *corev1.Affinity {
	return &corev1.Affinity{
		PodAntiAffinity: &corev1.PodAntiAffinity{
			RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{
				{
					LabelSelector: &metav1.LabelSelector{
						MatchLabels: map[string]string{
							consts.ComponentLabelKey: string(comp),
						},
					},
					TopologyKey: "kubernetes.io/hostname",
				},
			},
		},
	}
}

func NodeAffinityWithUK(matchKey, matchValue string) *corev1.Affinity {
	nodeAFF := &corev1.NodeAffinity{RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
		NodeSelectorTerms: []corev1.NodeSelectorTerm{
			{MatchExpressions: []corev1.NodeSelectorRequirement{
				{
					Key:      matchKey,
					Operator: corev1.NodeSelectorOpIn,
					Values:   []string{matchValue},
				},
			}},
		},
	}}
	return &corev1.Affinity{NodeAffinity: nodeAFF}
}

// IsRealError checks if the given error is a real error.
// If err is the type of `RequeueAfterError` but its Caused is nil,
// it means that the given error is not an error, actually.
func IsRealError(err error) bool {
	if err == nil {
		return false
	}

	if requeueErr, ok := err.(*reconcileerr.RequeueAfterError); ok && requeueErr.Caused == nil {
		return false
	}

	return true
}

// IsStatusConditionTrue returns true when the conditionType is present and set to `true`
func IsStatusConditionTrue(conditions []appsv1.DeploymentCondition, conditionType appsv1.DeploymentConditionType) bool {
	return IsStatusConditionPresentAndEqual(conditions, conditionType, corev1.ConditionTrue)
}

// IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status.
func IsStatusConditionPresentAndEqual(conditions []appsv1.DeploymentCondition, conditionType appsv1.DeploymentConditionType, status corev1.ConditionStatus) bool {
	for _, condition := range conditions {
		if condition.Type == conditionType {
			return condition.Status == status
		}
	}
	return false
}

func InIntegrationTest() bool {
	v := os.Getenv("KUBEBUILDER_ASSETS")
	return v != ""
}

// MatchRegion is a glob checker against "provider-region-shootid" string.
// One could use pattern like "*-shootid" to match all shootid, or "aws-us-west-2-*" to match all shootid in aws us-west-2 region.
func MatchRegion(provider, region, shootName string, patterns []string) bool {
	for _, s := range patterns {
		target := fmt.Sprintf("%s-%s-%s", provider, region, shootName)
		if matched, err := filepath.Match(s, target); err == nil && matched {
			// Region is in the whitelist, do not skip.
			return true
		}
	}
	return false
}

// TiCDCNodeGroupNamePrefix returns the ticdc node group name prefix
// deployedInDownstream identify if the ticdc is deployed in downstream
func TiCDCNodeGroupNamePrefix(deployedInDownstream bool,
	upstreamClusterName string,
	destCluster *cloudv1.Cluster,
) string {
	var prefix string
	if destCluster != nil {
		prefix = destCluster.PrefixedId()
	}
	if deployedInDownstream {
		if prefix != "" {
			prefix = fmt.Sprintf("%s-%s", prefix, "cdc")
		} else {
			// no downstream cluster, use the UpstreamClusterName
			prefix = fmt.Sprintf("%s-%s", cloudv1.ClusterPrefixId(upstreamClusterName), "cdc")
		}
	}
	return prefix
}

func TokenBasedAuthEnabled(cluster *cloudv1.Cluster, version string) bool {
	providerType := cluster.Labels[cloudv1.ProviderTypeLabel]
	switch providerType {
	case consts.ProviderTypeAWS, consts.ProviderTypeGCP:
		return false
	}

	if cluster.Labels[cloudv1.ClusterLabelWantTokenBasedAuth] != "true" {
		return false
	}

	if !tokenBasedAuthSupported(version) {
		return false
	}
	return true
}

func tokenBasedAuthSupported(version string) bool {
	return semver.Compare(version, minTokenBasedAuthVersion) >= 0
}

func GetLableValue(obj client.Object, label string) (string, bool) {
	value, ok := obj.GetLabels()[label]
	return value, ok
}

func GetClusterIDFromLabel(obj client.Object) (string, error) {
	value, ok := GetLableValue(obj, consts.ClusterLabelKey)
	if !ok || value == "" {
		return "", errors.New("cluster label not exists")
	}
	return value, nil
}

func GetProjectIDFromLabel(obj client.Object) (string, error) {
	value, ok := GetLableValue(obj, consts.ProjectLabelKey)
	if !ok || value == "" {
		return "", errors.New("project label not exists")
	}
	return value, nil
}

func GetTenantIDFromLabel(obj client.Object) (string, error) {
	value, ok := GetLableValue(obj, consts.TenantLabelKey)
	if !ok || value == "" {
		return "", errors.New("tenant label not exists")
	}
	return value, nil
}

// GenerateCommonNodePoolLabels return the labels apply to gce instances
func GenerateCommonNodePoolLabels(cluster *cloudv1.Cluster) map[string]string {
	l := map[string]string{}
	// Append billing labels
	l[cloudv1.TagLabelPrefix+common.UsedBy] = common.CustomerTiDB
	l[cloudv1.TagLabelPrefix+common.Cluster] = common.ClustersShared
	l[cloudv1.TagLabelPrefix+common.ServiceType] = common.Dedicated
	l[cloudv1.TagLabelPrefix+common.Project] = cluster.GetProjectID()
	l[cloudv1.TagLabelPrefix+common.Tenant] = cluster.GetTenantID()
	return l
}

// BuildResourceLabelsAnnotationValue return the key-value string of labels
func BuildResourceLabelsAnnotationValue(ls map[string]string) string {
	if len(ls) == 0 {
		return ""
	}

	var kvs []string
	for k, v := range ls {
		kvs = append(kvs, k+":"+v)
	}
	sort.Strings(kvs)
	return strings.Join(kvs, ",")
}

// GetGcpNodepoolDiskType returns the recommended disk type for a given GCP machine type.
func GetGcpNodepoolDiskType(machineType string) string {
	var gcpDiskTypeMap = map[string]string{
		// For N4 machine types, use Hyperdisk Balanced as required by GCP. Such as "n4-standard-1", "n4-custom-32-65536"
		"n4-":  diskTypeHyperdiskBalanced,
		"c4a-": diskTypeHyperdiskBalanced,
	}
	for prefix, diskType := range gcpDiskTypeMap {
		if strings.HasPrefix(machineType, prefix) {
			return diskType
		}
	}
	// For all other machine types, use the standard persistent disk.
	return diskTypePdStandard
}

type StatefulPod struct {
	Name    string
	Ordinal int
}

func (s *StatefulPod) GetOrdinal() int {
	return s.Ordinal
}

func (s *StatefulPod) GetName() string {
	return s.Name
}

func NewStatefulPodFromName(name string) (*StatefulPod, error) {
	splits := strings.Split(name, "-")
	// last part should be ordinal for statefulset pod
	ordinalStr := splits[len(splits)-1]
	ordinal, err := strconv.Atoi(ordinalStr)
	if err != nil {
		return nil, err
	}
	return &StatefulPod{
		Name:    name,
		Ordinal: ordinal,
	}, nil
}
