// Package versions contains functions to parse and sort Bazel version identifier.
package versions

import (
	"fmt"
	"log"
	"regexp"
	"sort"
	"strconv"

	"github.com/hashicorp/go-version"
)

const (
	// BazelUpstream contains the name of the official Bazel GitHub organization.
	BazelUpstream = "bazelbuild"
)

var (
	releasePattern       = regexp.MustCompile(`^(\d+)\.\d+\.\d+$`)
	trackPattern         = regexp.MustCompile(`^(\d+)\.(x|\*)$`)
	patchPattern         = regexp.MustCompile(`^(\d+\.\d+\.\d+)-([\w\d]+)$`)
	candidatePattern     = regexp.MustCompile(`^(\d+\.\d+\.\d+)rc(\d+)$`)
	rollingPattern       = regexp.MustCompile(`^\d+\.0\.0-pre\.\d{8}(\.\d+){1,2}$`)
	latestReleasePattern = regexp.MustCompile(`^latest(?:-(?P<offset>\d+))?$`)
	commitPattern        = regexp.MustCompile(`^[a-z0-9]{40}$`)
)

// Info represents a structured Bazel version identifier.
type Info struct {
	MustBeRelease, MustBeCandidate bool
	IsLTS, IsRolling               bool
	IsCommit, IsFork, IsRelative   bool
	Fork, Value                    string
	LatestOffset, TrackRestriction int
}

// Parse extracts and returns structured information about the given Bazel version label.
func Parse(fork, version string) (*Info, error) {
	vi := &Info{Fork: fork, Value: version, IsFork: isFork(fork)}

	if m := releasePattern.FindStringSubmatch(version); m != nil {
		vi.IsLTS = true
		vi.MustBeRelease = true
	} else if m := trackPattern.FindStringSubmatch(version); m != nil {
		track, err := strconv.Atoi(m[1])
		if err != nil {
			return nil, fmt.Errorf("invalid version %q, expected something like '5.x' or '5.*'", version)
		}
		vi.IsLTS = true
		vi.MustBeRelease = (m[2] == "x")
		vi.IsRelative = true
		vi.TrackRestriction = track
	} else if patchPattern.MatchString(version) {
		vi.IsLTS = true
		vi.MustBeRelease = true
	} else if m := latestReleasePattern.FindStringSubmatch(version); m != nil {
		vi.IsLTS = true
		vi.MustBeRelease = true
		vi.IsRelative = true
		if m[1] != "" {
			offset, err := strconv.Atoi(m[1])
			if err != nil {
				return nil, fmt.Errorf("invalid version %q, could not parse offset: %v", version, err)
			}
			vi.LatestOffset = offset
		}
	} else if candidatePattern.MatchString(version) {
		vi.IsLTS = true
		vi.MustBeCandidate = true
	} else if version == "last_rc" {
		vi.IsLTS = true
		vi.MustBeCandidate = true
		vi.IsRelative = true
	} else if commitPattern.MatchString(version) {
		vi.IsCommit = true
	} else if version == "last_green" {
		vi.IsCommit = true
		vi.IsRelative = true
	} else if rollingPattern.MatchString(version) {
		vi.IsRolling = true
	} else if version == "rolling" {
		vi.IsRolling = true
		vi.IsRelative = true
	} else {
		return nil, fmt.Errorf("invalid version '%s'", version)
	}
	return vi, nil
}

func isFork(value string) bool {
	return value != "" && value != BazelUpstream
}

// GetInAscendingOrder returns the given versions sorted in ascending order.
func GetInAscendingOrder(versions []string) []string {
	wrappers := make([]*version.Version, len(versions))
	for i, v := range versions {
		wrapper, err := version.NewVersion(v)
		if err != nil {
			log.Printf("WARN: Could not parse version: %s", v)
		}
		wrappers[i] = wrapper
	}
	sort.Sort(version.Collection(wrappers))

	sorted := make([]string, len(versions))
	for i, w := range wrappers {
		sorted[i] = w.Original()
	}
	return sorted
}

func MatchCommitPattern(version string) bool {
	return commitPattern.MatchString(version)
}

// IsCommit returns whether the given version refers to a commit.
func IsCommit(version string) bool {
	return version == "last_green" || commitPattern.MatchString(version)
}
