package git

import (
	"bytes"
	"context"
	"fmt"
	"strconv"
	"strings"
)

var (
	// minimumVersion is the minimum required Git version.
	minimumVersion = version{2, 29, 0, false}
)

type version struct {
	major, minor, patch uint32
	rc                  bool
}

// Version returns the used git version.
func Version(ctx context.Context) (string, error) {
	var buf bytes.Buffer
	cmd, err := NewCommandFactory().unsafeBareCmd(ctx, cmdStream{Out: &buf}, nil, "version")
	if err != nil {
		return "", err
	}

	if err = cmd.Wait(); err != nil {
		return "", err
	}

	out := strings.Trim(buf.String(), " \n")
	ver := strings.SplitN(out, " ", 3)
	if len(ver) != 3 {
		return "", fmt.Errorf("invalid version format: %q", buf.String())
	}

	return ver[2], nil
}

// VersionLessThan returns true if the parsed version value of v1Str is less
// than the parsed version value of v2Str. An error can be returned if the
// strings cannot be parsed.
// Note: this is an extremely simplified semver comparison algorithm
func VersionLessThan(v1Str, v2Str string) (bool, error) {
	var (
		v1, v2 version
		err    error
	)

	for _, v := range []struct {
		string
		*version
	}{
		{v1Str, &v1},
		{v2Str, &v2},
	} {
		*v.version, err = parseVersion(v.string)
		if err != nil {
			return false, err
		}
	}

	return versionLessThan(v1, v2), nil
}

func versionLessThan(v1, v2 version) bool {
	switch {
	case v1.major < v2.major:
		return true
	case v1.major > v2.major:
		return false

	case v1.minor < v2.minor:
		return true
	case v1.minor > v2.minor:
		return false

	case v1.patch < v2.patch:
		return true
	case v1.patch > v2.patch:
		return false

	case v1.rc && !v2.rc:
		return true
	case !v1.rc && v2.rc:
		return false

	default:
		// this should only be reachable when versions are equal
		return false
	}
}

func parseVersion(versionStr string) (version, error) {
	versionSplit := strings.SplitN(versionStr, ".", 4)
	if len(versionSplit) < 3 {
		return version{}, fmt.Errorf("expected major.minor.patch in %q", versionStr)
	}

	var ver version

	for i, v := range []*uint32{&ver.major, &ver.minor, &ver.patch} {
		var n64 uint64

		if versionSplit[i] == "GIT" {
			// Git falls back to vx.x.GIT if it's unable to describe the current version
			// or if there's a version file. We should just treat this as "0", even
			// though it may have additional commits on top.
			n64 = 0
		} else {
			rcSplit := strings.SplitN(versionSplit[i], "-", 2)

			var err error
			n64, err = strconv.ParseUint(rcSplit[0], 10, 32)
			if err != nil {
				return version{}, err
			}

			if len(rcSplit) == 2 && strings.HasPrefix(rcSplit[1], "rc") {
				ver.rc = true
			}
		}

		*v = uint32(n64)
	}

	if len(versionSplit) == 4 {
		if strings.HasPrefix(versionSplit[3], "rc") {
			ver.rc = true
		}
	}

	return ver, nil
}

// SupportedVersion checks if a version string corresponds to a Git version
// supported by Gitaly.
func SupportedVersion(versionStr string) (bool, error) {
	v, err := parseVersion(versionStr)
	if err != nil {
		return false, err
	}

	return !versionLessThan(v, minimumVersion), nil
}
