package operate

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"

	v1 "github.com/tidbcloud/infra-component/api/v1"
	"golang.org/x/mod/semver"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
	errUnsupportedOperator = fmt.Errorf("unsupported operator")
	errUnmatchedType       = fmt.Errorf("unmatched type")
	errKeyNotPresent       = fmt.Errorf("experssion key not present")
)

// SelectComponent selects the component with the highest version that matches the provided
// description. Intermediate errors are returned along with the selected component spec.
func SelectComponent(componentset v1.ComponentSet, desc v1.Description) (*v1.Component, []error) {
	var errs []error
	sort.Sort(ByVersion(componentset.Spec.Versions))

	var selected *v1.ComponentSpec = nil
	for _, version := range componentset.Spec.Versions {
		matched, err := matchComponentSpec(version, desc)
		if err != nil {
			errs = append(errs, err)
		}
		if err == nil && matched {
			selected = &version
		}
	}
	if selected == nil {
		return nil, errs
	}
	return &v1.Component{
		ObjectMeta: metav1.ObjectMeta{
			Name:      componentset.Name,
			Namespace: componentset.Namespace,
		},
		Spec: *selected,
	}, errs
}

func matchComponentSpec(spec v1.ComponentSpec, desc v1.Description) (bool, error) {
	selector := spec.Selector
	if result, err := evalExpressions(selector.NetworkSelector, desc.Network); err != nil || !result {
		return result, err
	}

	if result, err := evalExpressions(selector.ClusterSelector, desc.Cluster); err != nil || !result {
		return result, err
	}
	return true, nil
}

func evalExpressions(exps []v1.MatchingExpression, desc map[string]interface{}) (bool, error) {
	for _, exp := range exps {
		if result, err := evalExpression(exp, desc); err != nil || !result {
			return result, err
		}
	}
	return true, nil
}

func evalExpression(exp v1.MatchingExpression, desc map[string]interface{}) (bool, error) {
	value, ok := desc[exp.Key]
	if !ok {
		return false, errKeyNotPresent
	}
	switch exp.Operator {
	case v1.Equal:
		return reflect.DeepEqual(value, exp.Value), nil
	case v1.NotEqual:
		return !reflect.DeepEqual(value, exp.Value), nil
	case v1.Greater, v1.GreaterOrEqual, v1.Less, v1.LessOrEqual:
		switch v := value.(type) {
		case int:
			ev, err := strconv.Atoi(exp.Value)
			if err != nil {
				return false, errUnmatchedType
			}
			return compareInt(exp.Operator, v, ev)
		case string:
			return compareStringOrSemver(exp.Operator, v, exp.Value)
		}
	case v1.Include:
		return include(value, exp.Value)
	case v1.NotInclude:
		result, err := include(value, exp.Value)
		return !result, err
	}
	return false, errUnsupportedOperator
}

func compareInt(op string, x, y int) (bool, error) {
	switch op {
	case v1.Greater:
		return x > y, nil
	case v1.GreaterOrEqual:
		return x > y || x == y, nil
	case v1.Less:
		return x < y, nil
	case v1.LessOrEqual:
		return x < y || x == y, nil
	}
	return false, errUnsupportedOperator
}

func include(values interface{}, value string) (bool, error) {
	elements, ok := values.([]string)
	if !ok {
		return false, errUnmatchedType
	}
	for _, element := range elements {
		if element == value {
			return true, nil
		}
	}
	return false, nil
}

func compareStringOrSemver(op string, x, y string) (bool, error) {
	if semver.IsValid(x) || semver.IsValid(y) {
		if !(semver.IsValid(x) && semver.IsValid(y)) {
			return false, errUnmatchedType
		}
		return compareSemver(op, x, y)
	}
	switch op {
	case v1.Greater:
		return x > y, nil
	case v1.GreaterOrEqual:
		return x > y || x == y, nil
	case v1.Less:
		return x < y, nil
	case v1.LessOrEqual:
		return x < y || x == y, nil
	}
	return false, errUnsupportedOperator
}

func compareSemver(op string, x, y string) (bool, error) {
	res := semver.Compare(x, y)
	switch op {
	case v1.Greater:
		return res == 1, nil
	case v1.GreaterOrEqual:
		return res != -1, nil
	case v1.Less:
		return res == -1, nil
	case v1.LessOrEqual:
		return res != 1, nil
	}
	return false, errUnsupportedOperator
}

// ByVersion implements sort.Interface for sorting semantic version strings.
type ByVersion []v1.ComponentSpec

func (vs ByVersion) Len() int {
	return len(vs)
}

func (vs ByVersion) Swap(i, j int) {
	vs[i], vs[j] = vs[j], vs[i]
}

func (vs ByVersion) Less(i, j int) bool {
	cmp := semver.Compare(vs[i].Version, vs[j].Version)
	return cmp < 0
}
