package util

import (
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"

	"github.com/Masterminds/semver/v3"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/util/namer"
)

// 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
}

// CalcHashNum calculates an int64 hash from the given string.
// It uses the first 8 characters of the MD5 hash of the string,
// interprets them as a hexadecimal number, and returns the int64 value.
func CalcHashNum(s string) int64 {
	hash := md5.Sum([]byte(s))
	hashStr := hex.EncodeToString(hash[:])
	hashStr = hashStr[:8]
	hashInt, _ := strconv.ParseInt(hashStr, 16, 64)
	return hashInt
}

// CalcVmagentMaxDiskUsagePerURLFromPVSize calculates the optimal value of -remoteWrite.maxDiskUsagePerURL
// based on PV size and the number of remoteWriteUrls.
func CalcVmagentMaxDiskUsagePerURLFromPVSize(pvSize string, urlCount int, reserve string) (string, error) {
	pvSizeQty, err := resource.ParseQuantity(pvSize)
	if err != nil {
		return "", err
	}
	return CalcVmagentMaxDiskUsagePerURLFromPVSizeTyped(&pvSizeQty, urlCount, reserve)
}

// CalcVmagentMaxDiskUsagePerURLFromPVSizeTyped calculates the optimal value of -remoteWrite.maxDiskUsagePerURL
// based on PV size and the number of remoteWriteUrls.
func CalcVmagentMaxDiskUsagePerURLFromPVSizeTyped(pvSizeQty *resource.Quantity, urlCount int, reserve string) (string, error) {
	reserveQty, err := resource.ParseQuantity(reserve) // Leave `reserve` free to avoid disk fullness.
	if err != nil {
		return "", err
	}
	pvSizeQty.Sub(reserveQty)
	if urlCount > 0 {
		pvSizeQty.SetMilli(pvSizeQty.MilliValue() / int64(urlCount))
	}
	r := pvSizeQty.String()
	if strings.HasSuffix(r, "i") {
		// To be compatible with vmagent's configuration requirements.
		r += "B"
	}
	return r, nil
}

// ShouldUpdateO11yVersion returns true if the annotation's stored o11y version is smaller
// than the desired config version, or if the annotation is missing/invalid.
func ShouldUpdateO11yVersion(annotation map[string]string, configVersion int) bool {
	currentVersion := annotation[consts.AnnotationKeyO11yVersion]
	vNum, err := strconv.Atoi(currentVersion)
	if err != nil {
		return true
	}
	return vNum < configVersion
}

// DomainNamespace reduces a FQDN to its first label (e.g. "foo.bar.baz" -> "foo")
// so that names remain compact and within provider limits.
func DomainNamespace(domainName string) string {
	i := strings.Index(domainName, ".")
	if i > 0 {
		domainName = domainName[:i]
	}
	return domainName
}

// InterfaceEndpointName generates a deterministic, length-safe name for the
// Interface Endpoint by combining the first label of the service domain with a
// well-known prefix and the object identity (via `namer.UniqueIdentifier`).
func InterfaceEndpointName(meta metav1.ObjectMeta, domainName, prefix string) string {
	domainName = DomainNamespace(domainName)
	return namer.UniqueIdentifier(meta, namer.WithPrefix(domainName), namer.WithPrefix(prefix))
}

// CompareVersions returns true if the constraint <version1> compared by <operator> to <version2>
// returns true, and false otherwise.
// The comparison is based on semantic versions, i.e. <version1> and <version2> will be converted
// if needed.
func CompareVersions(version1, operator, version2 string) (bool, error) {
	var (
		v1 = normalizeVersion(version1)
		v2 = normalizeVersion(version2)
	)

	return CheckVersionMeetsConstraint(v1, fmt.Sprintf("%s %s", operator, v2))
}

func normalizeVersion(version string) string {
	v := strings.ReplaceAll(version, "v", "")
	idx := strings.IndexAny(v, "-+")
	if idx != -1 {
		v = v[:idx]
	}
	return v
}

// CheckVersionMeetsConstraint returns true if the <version> meets the <constraint>.
func CheckVersionMeetsConstraint(version, constraint string) (bool, error) {
	c, err := semver.NewConstraint(constraint)
	if err != nil {
		return false, err
	}

	v, err := semver.NewVersion(version)
	if err != nil {
		return false, err
	}

	return c.Check(v), nil
}

// getComponentSpec retrieves the specification for a given component from annotations.
//
// The spec annotation uses the format "{vmagent_spec}/{vector_spec}" to support
// independent configuration of vmagent and vector components.
//
// Format specification:
//   - "{vmagent_spec}/{vector_spec}": both components specified, e.g., "default/default", "large/small"
//   - "{vmagent_spec}/": only vmagent specified, vector part empty, e.g., "large/"
//   - "/{vector_spec}": only vector specified, vmagent part empty, e.g., "/small"
//   - "{single_spec}": single value without separator, applies to both components (legacy format)
//
// If the specification is not found or is invalid (e.g., malformed format like "a/b/c"),
// it returns the provided default value.
func getComponentSpec(annotations map[string]string, key, defaultVal, component string) string {
	spec := annotations[key]
	if spec == "" {
		return defaultVal
	}

	if !strings.Contains(spec, "/") {
		return spec
	}

	parts := strings.Split(spec, "/")
	if len(parts) != 2 {
		return defaultVal
	}

	// Determine the index based on the component
	// 0: vmagent
	// 1: vector
	index := 0
	if component == consts.ComponentVector {
		index = 1
	}
	if parts[index] == "" {
		return defaultVal
	}
	return parts[index]
}

// GetComponentDesiredSpec retrieves the desired specification for a given component from annotations.
func GetComponentDesiredSpec(annotations map[string]string, defaultVal, component string) string {
	return getComponentSpec(annotations, consts.AnnotationKeyO11yDesiredSpec, defaultVal, component)
}

// GetComponentMinSpec retrieves the minimum specification for a given component from annotations.
func GetComponentMinSpec(annotations map[string]string, defaultVal, component string) string {
	return getComponentSpec(annotations, consts.AnnotationKeyO11yMinSpec, defaultVal, component)
}

// GetComponentMaxSpec retrieves the maximum specification for a given component from annotations.
func GetComponentMaxSpec(annotations map[string]string, defaultVal, component string) string {
	return getComponentSpec(annotations, consts.AnnotationKeyO11yMaxSpec, defaultVal, component)
}
