/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

/*
Package compatibility offers helper functions for comparing and filtering versions.
*/
package compatibility

import (
	"errors"
	"fmt"
	"strings"

	"golang.org/x/mod/semver"
)

var (
	// ErrMajorMismatch signals that the major version of two compared versions don't match.
	ErrMajorMismatch = errors.New("missmatching major version")
	// ErrMinorDrift signals that the minor version of two compared versions are further apart than one.
	ErrMinorDrift = errors.New("version difference larger than one minor version")
	// ErrSemVer signals that a given version does not adhere to the Semver syntax.
	ErrSemVer = errors.New("invalid semantic version")
	// ErrOutdatedCLI signals that the configured version is newer than the CLI. This is not allowed.
	ErrOutdatedCLI = errors.New("target version newer than cli version")
)

// InvalidUpgradeError present an invalid upgrade. It wraps the source and destination version for improved debuggability.
type InvalidUpgradeError struct {
	from     string
	to       string
	innerErr error
}

// NewInvalidUpgradeError returns a new InvalidUpgradeError.
func NewInvalidUpgradeError(from string, to string, innerErr error) *InvalidUpgradeError {
	return &InvalidUpgradeError{from: from, to: to, innerErr: innerErr}
}

// Unwrap returns the inner error, which is nil in this case.
func (e *InvalidUpgradeError) Unwrap() error {
	return e.innerErr
}

// Error returns the String representation of this error.
func (e *InvalidUpgradeError) Error() string {
	return fmt.Sprintf("upgrading from %s to %s is not a valid upgrade: %s", e.from, e.to, e.innerErr)
}

// EnsurePrefixV returns the input string prefixed with the letter "v", if the string doesn't already start with that letter.
func EnsurePrefixV(str string) string {
	if strings.HasPrefix(str, "v") {
		return str
	}
	return "v" + str
}

// IsValidUpgrade checks that a and b adhere to a version drift of 1 and b is greater than a.
func IsValidUpgrade(a, b string) error {
	a = EnsurePrefixV(a)
	b = EnsurePrefixV(b)

	if !semver.IsValid(a) || !semver.IsValid(b) {
		return NewInvalidUpgradeError(a, b, ErrSemVer)
	}

	if semver.Compare(a, b) >= 0 {
		return NewInvalidUpgradeError(a, b, errors.New("current version newer than or equal to new version"))
	}

	aMajor, aMinor, err := parseCanonicalSemver(a)
	if err != nil {
		return NewInvalidUpgradeError(a, b, err)
	}
	bMajor, bMinor, err := parseCanonicalSemver(b)
	if err != nil {
		return NewInvalidUpgradeError(a, b, err)
	}

	if aMajor != bMajor {
		return NewInvalidUpgradeError(a, b, ErrMajorMismatch)
	}

	if bMinor-aMinor > 1 {
		return NewInvalidUpgradeError(a, b, ErrMinorDrift)
	}

	return nil
}

// BinaryWith tests that this binarie's version is greater or equal than some target version, but not further away than one minor version.
func BinaryWith(binaryVersion, target string) error {
	binaryVersion = EnsurePrefixV(binaryVersion)
	target = EnsurePrefixV(target)
	if !semver.IsValid(binaryVersion) || !semver.IsValid(target) {
		return ErrSemVer
	}
	cliMajor, cliMinor, err := parseCanonicalSemver(binaryVersion)
	if err != nil {
		return err
	}
	targetMajor, targetMinor, err := parseCanonicalSemver(target)
	if err != nil {
		return err
	}

	// Major versions always have to match.
	if cliMajor != targetMajor {
		return ErrMajorMismatch
	}
	// For images we allow newer patch versions, therefore this only checks the minor version.
	if semver.Compare(semver.MajorMinor(binaryVersion), semver.MajorMinor(target)) == -1 {
		return ErrOutdatedCLI
	}
	// Abort if minor version drift between CLI and versionA value is greater than 1.
	if cliMinor-targetMinor > 1 {
		return ErrMinorDrift
	}

	return nil
}

// FilterNewerVersion filters the list of versions to only include versions newer than currentVersion.
func FilterNewerVersion(currentVersion string, newVersions []string) []string {
	currentVersion = EnsurePrefixV(currentVersion)
	var result []string

	for _, image := range newVersions {
		image = EnsurePrefixV(image)
		// check if image is newer than current version
		if semver.Compare(image, currentVersion) <= 0 {
			continue
		}
		result = append(result, image)
	}
	return result
}

// NextMinorVersion returns the next minor version for a given canonical semver.
// The returned format is vMAJOR.MINOR.
func NextMinorVersion(version string) (string, error) {
	major, minor, err := parseCanonicalSemver(EnsurePrefixV(version))
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("v%d.%d", major, minor+1), nil
}

func parseCanonicalSemver(version string) (major int, minor int, err error) {
	version = semver.MajorMinor(version) // ensure version is in canonical form (vX.Y.Z)
	if version == "" {
		return 0, 0, fmt.Errorf("invalid semver: '%s'", version)
	}
	_, err = fmt.Sscanf(version, "v%d.%d", &major, &minor)
	if err != nil {
		return 0, 0, fmt.Errorf("parsing version: %w", err)
	}

	return major, minor, nil
}
