package appauthority

import (
	"fmt"
	"strings"
)

type semVer struct {
	core       [3]string
	prerelease []string
}

func ValidateSemanticVersion(value string) error {
	_, err := parseSemVer(value)
	return err
}

// CompareSemanticVersions compares two canonical SemVer values. It returns a
// negative number, zero, or a positive number with the usual ordering meaning.
func CompareSemanticVersions(left, right string) (int, error) {
	l, err := parseSemVer(left)
	if err != nil {
		return 0, fmt.Errorf("left semantic version: %w", err)
	}
	r, err := parseSemVer(right)
	if err != nil {
		return 0, fmt.Errorf("right semantic version: %w", err)
	}
	return compareSemVer(l, r), nil
}

func parseSemVer(value string) (semVer, error) {
	if value == "" || strings.HasPrefix(value, "v") || strings.Contains(value, "+") {
		return semVer{}, fmt.Errorf("must be canonical SemVer 2.0.0 without leading v or build metadata")
	}
	coreText, prereleaseText, hasPrerelease := strings.Cut(value, "-")
	coreParts := strings.Split(coreText, ".")
	if len(coreParts) != 3 {
		return semVer{}, fmt.Errorf("must contain major.minor.patch")
	}
	var result semVer
	for i, part := range coreParts {
		if !isCanonicalNumericIdentifier(part) {
			return semVer{}, fmt.Errorf("core identifier %q is not canonical numeric SemVer", part)
		}
		result.core[i] = part
	}
	if !hasPrerelease {
		return result, nil
	}
	if prereleaseText == "" {
		return semVer{}, fmt.Errorf("prerelease cannot be empty")
	}
	for _, identifier := range strings.Split(prereleaseText, ".") {
		if identifier == "" {
			return semVer{}, fmt.Errorf("prerelease identifier cannot be empty")
		}
		numeric := true
		for _, char := range []byte(identifier) {
			if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' {
				return semVer{}, fmt.Errorf("invalid prerelease identifier %q", identifier)
			}
			if char < '0' || char > '9' {
				numeric = false
			}
		}
		if numeric && !isCanonicalNumericIdentifier(identifier) {
			return semVer{}, fmt.Errorf("numeric prerelease identifier %q has leading zero", identifier)
		}
		result.prerelease = append(result.prerelease, identifier)
	}
	return result, nil
}

func isCanonicalNumericIdentifier(value string) bool {
	if value == "" || (len(value) > 1 && value[0] == '0') {
		return false
	}
	for _, char := range []byte(value) {
		if char < '0' || char > '9' {
			return false
		}
	}
	return true
}

func compareSemVer(left, right semVer) int {
	for i := range left.core {
		if cmp := compareNumericText(left.core[i], right.core[i]); cmp != 0 {
			return cmp
		}
	}
	if len(left.prerelease) == 0 && len(right.prerelease) == 0 {
		return 0
	}
	if len(left.prerelease) == 0 {
		return 1
	}
	if len(right.prerelease) == 0 {
		return -1
	}
	limit := len(left.prerelease)
	if len(right.prerelease) < limit {
		limit = len(right.prerelease)
	}
	for i := 0; i < limit; i++ {
		l, r := left.prerelease[i], right.prerelease[i]
		lNumeric, rNumeric := isCanonicalNumericIdentifier(l), isCanonicalNumericIdentifier(r)
		switch {
		case lNumeric && rNumeric:
			if cmp := compareNumericText(l, r); cmp != 0 {
				return cmp
			}
		case lNumeric:
			return -1
		case rNumeric:
			return 1
		case l < r:
			return -1
		case l > r:
			return 1
		}
	}
	switch {
	case len(left.prerelease) < len(right.prerelease):
		return -1
	case len(left.prerelease) > len(right.prerelease):
		return 1
	default:
		return 0
	}
}

func compareNumericText(left, right string) int {
	switch {
	case len(left) < len(right):
		return -1
	case len(left) > len(right):
		return 1
	case left < right:
		return -1
	case left > right:
		return 1
	default:
		return 0
	}
}
