// Package tagcmp provides image tag comparison operations.
package tagcmp

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

type Tag struct {
	Major        string `json:",omitempty"` // major semver part
	Minor        string `json:",omitempty"` // minor semver part
	MissingMinor bool   // whether or not the minor semver part was left out
	Patch        string `json:",omitempty"` // patch semver part
	MissingPatch bool   // whether or not he patch semver part was left out
	Suffix       string // tag suffix (e.g. "-alpine") [would be release candidate in semver]
	UsesV        bool   // whether or not the tag uses the "v" prefix
	Metadata     string // metadata: what's after + and after the first "-"
}

type TagDelta struct {
	Major int // major semver difference
	Minor int // minor semver difference
	Patch int // patch semver difference
}

// ByTagAsc sorts tags in ascending order where the last element is the latest tag.
type ByTagAsc []Tag

func (t ByTagAsc) Len() int      { return len(t) }
func (t ByTagAsc) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t ByTagAsc) Less(i, j int) bool {
	return t[i].IsLessThan(t[j])
}

// ByTagDesc sorts tags in descending order where the first element is the latest tag.
type ByTagDesc []Tag

func (t ByTagDesc) Len() int      { return len(t) }
func (t ByTagDesc) Swap(i, j int) { t[j], t[i] = t[i], t[j] }
func (t ByTagDesc) Less(i, j int) bool {
	return t[j].IsLessThan(t[i])
}

// IsGreaterThan tests if a tag is greater than another. There are some
// tag-isms to take into account here, shorter is bigger (i.e. 2.1 > 2.1.1 ==
// true, 2 > 2.1 == true).
func (t Tag) IsGreaterThan(tag Tag) bool {
	// shorter is bigger, i.e. 2.1 > 2.1.1
	if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
		return true
	}
	if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
		return false
	}

	// ignore errors since Parse already handled
	mj1, _ := strconv.Atoi(t.Major)
	mj2, _ := strconv.Atoi(tag.Major)
	if mj1 > mj2 {
		return true
	}
	if mj2 > mj1 {
		return false
	}

	mn1, _ := strconv.Atoi(t.Minor)
	mn2, _ := strconv.Atoi(tag.Minor)
	if mn1 > mn2 {
		return true
	}
	if mn2 > mn1 {
		return false
	}

	p1, _ := strconv.Atoi(t.Patch)
	p2, _ := strconv.Atoi(tag.Patch)
	if p1 > p2 {
		return true
	}
	if p2 > p1 {
		return false
	}

	return false
}

// IsLessThan tests if a tag is less than another. There are some tag-isms to
// take into account here, shorter is bigger (i.e. 2.1 < 2.1.1 == false, 2 <
// 2.1 == false).
func (t Tag) IsLessThan(tag Tag) bool {
	return !t.IsGreaterThan(tag)
}

// Equals tests Tag equality
func (t Tag) Equals(tag Tag) bool {
	if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
		return false
	}

	if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
		return false
	}

	if t.Metadata != tag.Metadata {
		return false
	}

	// ignore errors since Parse already handled
	mj1, _ := strconv.Atoi(t.Major)
	mj2, _ := strconv.Atoi(tag.Major)
	if mj1 != mj2 {
		return false
	}

	mn1, _ := strconv.Atoi(t.Minor)
	mn2, _ := strconv.Atoi(tag.Minor)
	if mn1 != mn2 {
		return false
	}

	p1, _ := strconv.Atoi(t.Patch)
	p2, _ := strconv.Atoi(tag.Patch)
	return p1 == p2
}

// String formats a Tag correctly in string representation
func (t Tag) String() string {
	var repr string

	if t.UsesV {
		repr += "v"
	}

	repr += t.Major

	if !t.MissingMinor {
		repr += fmt.Sprintf(".%s", t.Minor)
	}

	if !t.MissingPatch {
		repr += fmt.Sprintf(".%s", t.Patch)
	}

	if t.Suffix != "" {
		repr += fmt.Sprintf("-%s", t.Suffix)
	}

	if t.Metadata != "" {
		repr += fmt.Sprintf("+%s", t.Metadata)
	}

	return repr
}

func (t TagDelta) String() string {
	var repr string
	repr = fmt.Sprintf("%d.%d.%d", t.Major, t.Minor, t.Patch)
	return repr
}

// IsCompatible determines if two tags can be compared together
func (t Tag) IsCompatible(tag Tag) bool {
	if t.UsesV && !tag.UsesV || tag.UsesV && !t.UsesV {
		return false
	}

	if t.Suffix != "" && tag.Suffix == "" || t.Suffix == "" && tag.Suffix != "" {
		return false
	}

	if t.Suffix != "" && tag.Suffix != "" {
		if t.Suffix != tag.Suffix {
			return false
		}
	}

	if t.MissingMinor && !tag.MissingMinor || tag.MissingMinor && !t.MissingMinor {
		return false
	}

	if t.MissingPatch && !tag.MissingPatch || tag.MissingPatch && !t.MissingPatch {
		return false
	}

	return true
}

// IsUpgradeCompatible chekcs if upTag is compatible with a pinned version tag.
// I.e. pinning to 22-fpm should return true if upTag is 22.2.0-fpm but not 22.2.0-alpine or 23.0.0-fpm
func (pin Tag) IsUpgradeCompatible(upTag Tag) bool {
	if pin.Suffix != upTag.Suffix {
		return false
	}
	if pin.Major != upTag.Major {
		return false
	}
	if pin.MissingMinor {
		return true
	}
	if pin.Minor != upTag.Minor {
		return false
	}
	if pin.MissingPatch {
		return true
	}
	if pin.Patch != upTag.Patch {
		return false
	}
	return true
}

// UpgradeDelta returns a TagDelta object which is the difference between an old and new tag
// It can contain negative numbers if comparing with an older tag.
func (curTag Tag) UpgradeDelta(newTag Tag) (TagDelta, error) {
	if !curTag.IsCompatible(newTag) {
		return TagDelta{}, fmt.Errorf("%s and %s are not compatible with each other", curTag.String(), newTag.String())
	}
	diff := TagDelta{
		Major: 0,
		Minor: 0,
		Patch: 0,
	}
	// assuming tags are correctly formatted
	curMajor, _ := strconv.Atoi(curTag.Major)
	newMajor, _ := strconv.Atoi(newTag.Major)
	diff.Major = newMajor - curMajor
	if !curTag.MissingMinor {
		curMinor, _ := strconv.Atoi(curTag.Minor)
		newMinor, _ := strconv.Atoi(newTag.Minor)
		diff.Minor = newMinor - curMinor
	}
	if !curTag.MissingPatch {
		curPatch, _ := strconv.Atoi(curTag.Patch)
		newPatch, _ := strconv.Atoi(newTag.Patch)
		diff.Patch = newPatch - curPatch
	}

	return diff, nil
}

// UpgradeType takes exit from UpgradeElemene and returns a numeric representation of upgrade or downgrade
// 1/-1: patch 2/-2: minor 4/-4: major 0: no change
func (d TagDelta) UpgradeType() int {
	if d.Major > 0 {
		return 4
	}
	if d.Major < 0 {
		return -4
	}
	if d.Minor > 0 {
		return 2
	}
	if d.Minor < 0 {
		return -2
	}
	if d.Patch > 0 {
		return 1
	}
	if d.Patch < 0 {
		return -1
	}
	return 0
}

// CommitHashPattern matches commit-like hash tags
var CommitHashPattern = "^[a-f0-9]{7,40}$"

// DotPattern matches tags which contain multiple versions
var DotPattern = "([0-9]+)\\.([0-9]+)"

// EmptyPattern matches when tags are missing
var EmptyPattern = "^$"

// ParametrizedPattern matches when tags are parametrized
var ParametrizedPattern = "\\${.+}"

// StringPattern matches when tags are only made up of alphabetic characters
var StringPattern = "^[a-zA-Z]+$"

// patternMatches determines if a tag matches unsupported patterns
func patternMatches(tag string) error {
	unsupported := []string{
		CommitHashPattern,
		EmptyPattern,
		ParametrizedPattern,
		StringPattern,
	}

	for _, pattern := range unsupported {
		if match, _ := regexp.Match(pattern, []byte(tag)); match {
			return fmt.Errorf("version %s is not supported (matched: %s)", tag, pattern)
		}
	}

	return nil
}

// patternCounts determines if tags match unsupported patterns by counting occurences of matches
func patternCounts(tag string) error {
	v := regexp.MustCompile(DotPattern)
	tag = strings.Split(tag, "+")[0]
	if m := v.FindAllStringIndex(tag, -1); len(m) > 1 {
		return fmt.Errorf("'%s' is not supported (%s)", tag, DotPattern)
	}

	return nil
}

// parseVersionPart converts a semver version part to an integer
func parseVersionPart(part string) (int, error) {
	p, err := strconv.Atoi(part)
	if err != nil {
		return 0, err
	}
	return p, nil
}

// ParseDelta converts a tag difference in the format of X, X.Y or X.Y.Z where
// X, Y, Z are positive or negative integers or 0
func ParseDelta(delta string) (TagDelta, error) {
	tagDelta := TagDelta{
		Major: 0,
		Minor: 0,
		Patch: 0,
	}

	splits := strings.Split(delta, ".")
	if len(splits) > 3 {
		return TagDelta{}, fmt.Errorf("'%s' has too much dots", delta)
	}
	major, err := strconv.Atoi(splits[0])
	if err != nil {
		return TagDelta{}, fmt.Errorf("Major part of '%s' is not an integer", delta)
	}
	tagDelta.Major = major

	if len(splits) > 1 {
		minor, err := strconv.Atoi(splits[1])
		if err != nil {
			return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
		}
		tagDelta.Minor = minor
	}
	if len(splits) > 2 {
		patch, err := strconv.Atoi(splits[2])
		if err != nil {
			return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
		}
		tagDelta.Patch = patch
	}
	return tagDelta, nil
}

// Parse converts an image tag into a structured data format. It aims to to
// support the general case of tags which are "semver-like" and/or stable and
// parseable by heuristics. Image tags follow no formal specification and
// therefore this is a best-effort implementation. Examples of tags this
// function can parse are: "5", "5.2", "v4", "v5.3.6", "4-alpine",
// "v3.2.1-debian".
func Parse(tag string) (Tag, error) {
	if err := patternMatches(tag); err != nil {
		return Tag{}, err
	}

	if err := patternCounts(tag); err != nil {
		return Tag{}, err
	}

	usesV := false
	if string(tag[0]) == "v" {
		tag = strings.TrimPrefix(tag, "v")
		usesV = true
	}

	var metadata string
	splits := strings.Split(tag, "+")
	if len(splits) > 1 {
		tag = splits[0]
		metadata = splits[1]
	}

	var suffix string
	splits = strings.SplitN(tag, "-", 2)
	if len(splits) > 1 {
		tag = splits[0]
		suffix = splits[1]
	}

	var major, minor, patch string
	var missingMinor, missingPatch bool
	parts := strings.Split(tag, ".")
	switch {
	case len(parts) == 1:
		if _, err := parseVersionPart(parts[0]); err != nil {
			return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
		}
		major = parts[0]
		missingMinor = true
		missingPatch = true
	case len(parts) == 2:
		if _, err := parseVersionPart(parts[0]); err != nil {
			return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
		}
		major = parts[0]

		if _, err := parseVersionPart(parts[1]); err != nil {
			return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
		}
		minor = parts[1]
		missingPatch = true
	case len(parts) == 3:
		if _, err := parseVersionPart(parts[0]); err != nil {
			return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
		}
		major = parts[0]

		if _, err := parseVersionPart(parts[1]); err != nil {
			return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
		}
		minor = parts[1]

		if _, err := parseVersionPart(parts[2]); err != nil {
			return Tag{}, fmt.Errorf("couldn't parse patch part of '%s': '%s'", tag, parts[2])
		}
		patch = parts[2]
	default:
		return Tag{}, fmt.Errorf("couldn't parse semver of '%s", tag)
	}

	parsedTag := Tag{
		Major:        major,
		Minor:        minor,
		MissingMinor: missingMinor,
		Patch:        patch,
		MissingPatch: missingPatch,
		UsesV:        usesV,
		Suffix:       suffix,
		Metadata:     metadata,
	}

	return parsedTag, nil
}

// IsParsable determines if a tag is supported by this library
func IsParsable(tag string) bool {
	if _, err := Parse(tag); err != nil {
		return false
	}
	return true
}
