package util

import (
	"errors"
	"fmt"
	"hash"
	"hash/fnv"
	"reflect"
	"runtime"
	"sort"
	"strings"

	"github.com/aws/aws-sdk-go/aws/arn"
	"github.com/davecgh/go-spew/spew"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	k8sruntime "k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

func WrapError(ctx string, err error) error {
	if err == nil {
		return errors.New(ctx)
	}
	return fmt.Errorf("%s: %v", ctx, err)
}

// Pointer return the pointer to the givne argument.
func Pointer[T any](x T) *T {
	return &x
}

func ToCrossplaneTaintEffect(effect corev1.TaintEffect) string {
	// Crossplane decided to be "smart" and only use the values from
	// NO_SCHEDULE;NO_EXECUTE;PREFER_NO_SCHEDULE for the node taints.
	// These values are different from what Kubernetes have been used.
	switch effect {
	case corev1.TaintEffectNoSchedule:
		return "NO_SCHEDULE"
	case corev1.TaintEffectNoExecute:
		return "NO_EXECUTE"
	case corev1.TaintEffectPreferNoSchedule:
		return "PREFER_NO_SCHEDULE"
	}
	return ""
}

func GetAnnotation(obj client.Object, key string) string {
	annotations := obj.GetAnnotations()
	if annotations == nil {
		return ""
	}
	return annotations[key]
}

func SetAnnotation(obj client.Object, key, value string) {
	annotations := obj.GetAnnotations()
	if annotations == nil {
		annotations = make(map[string]string)
	}
	annotations[key] = value
	obj.SetAnnotations(annotations)
}

func GetLabel(obj client.Object, key string) string {
	labels := obj.GetLabels()
	if labels == nil {
		return ""
	}
	return labels[key]
}

func SetLabel(obj client.Object, key, value string) {
	labels := obj.GetLabels()
	if labels == nil {
		labels = make(map[string]string)
	}
	labels[key] = value
	obj.SetLabels(labels)
}

func ParseKeyARN(identifier string) (region, id string, err error) {
	key, err := arn.Parse(identifier)
	if err != nil {
		return "", "", err
	}
	id = strings.TrimPrefix(key.Resource, "key/")
	return key.Region, id, nil
}

func ParseAWSPCAARN(identifier string) (region string, err error) {
	pca, err := arn.Parse(identifier)
	if err != nil {
		return "", err
	}
	if pca.Service != "acm-pca" {
		return "", fmt.Errorf("unexpected ARN service %q", pca.Service)
	}
	if pca.Region == "" {
		return "", errors.New("missing ARN region")
	}
	if !strings.HasPrefix(pca.Resource, "certificate-authority/") {
		return "", fmt.Errorf("unexpected ARN resource %q", pca.Resource)
	}
	return pca.Region, nil
}

func PopulateRawExtension(e *k8sruntime.RawExtension) {
	if e.Object == nil && (len(e.Raw) == 0 || string(e.Raw) == `{}`) {
		*e = k8sruntime.RawExtension{
			Object: &k8sruntime.Unknown{
				TypeMeta: k8sruntime.TypeMeta{
					Kind:       "Unknown",
					APIVersion: cloudv1.GroupVersion.String(),
				},
				Raw: []byte(`{
					"apiVersion": "cloud.pingcap.com/v1",
					"kind": "Unknown"
				}`),
			},
		}
	}
}

func MaxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

type SliceType interface {
	~string | ~int | ~float64 // add more *comparable* types as needed
}

func RemoveDuplicatesInSlice[T SliceType](s []T) []T {
	if len(s) < 1 {
		return s
	}

	// sort
	sort.SliceStable(s, func(i, j int) bool {
		return s[i] < s[j]
	})

	prev := 1
	for curr := 1; curr < len(s); curr++ {
		if s[curr-1] != s[curr] {
			s[prev] = s[curr]
			prev++
		}
	}

	return s[:prev]
}

func FnvHash(raw string) string {
	hash := fnv.New32()
	// fnv.New32().Write() never return error
	_, _ = hash.Write([]byte(raw))
	return fmt.Sprintf("%x", hash.Sum32())
}

// DeepHashObject is copied from "k8s.io/kubernetes/pkg/util/hash"
// it writes specified object to hash using the spew library
// which follows pointers and prints actual values of the nested objects
// ensuring the hash does not change when a pointer changes.
func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) {
	hasher.Reset()
	printer := spew.ConfigState{
		Indent:         " ",
		SortKeys:       true,
		DisableMethods: true,
		SpewKeys:       true,
	}
	// hash.Hash.Write never returns an error, safe to ignore
	_, _ = printer.Fprintf(hasher, "%#v", objectToWrite)
}

func GetFunctionName(f interface{}) (name string) {
	v := reflect.ValueOf(f)
	function := runtime.FuncForPC(v.Pointer())
	if function == nil {
		return ""
	}
	strs := strings.Split(function.Name(), ".")
	if len(strs) < 1 {
		return ""
	}
	return strs[len(strs)-1]
}

// tiproxy utils

const (
	TiProxyStatusTargetPort = 3080
	TiProxyStatusHealthPath = "/api/debug/health"
)

func IsTiProxyEnabled(tc *v1alpha1.TidbCluster) bool {
	return tc != nil && tc.TiProxyStsDesiredReplicas() != 0
}
