package constraint

import (
	"fmt"
	"net"
	"regexp"
	"strings"

	"github.com/moby/swarmkit/v2/api"
)

const (
	eq = iota
	noteq

	// NodeLabelPrefix is the constraint key prefix for node labels.
	NodeLabelPrefix = "node.labels."
	// EngineLabelPrefix is the constraint key prefix for engine labels.
	EngineLabelPrefix = "engine.labels."
)

var (
	alphaNumeric = regexp.MustCompile(`^(?i)[a-z_][a-z0-9\-_.]+$`)
	// value can be alphanumeric and some special characters. it shouldn't container
	// current or future operators like '>, <, ~', etc.
	valuePattern = regexp.MustCompile(`^(?i)[a-z0-9:\-_\s\.\*\(\)\?\+\[\]\\\^\$\|\/]+$`)

	// operators defines list of accepted operators
	operators = []string{"==", "!="}
)

// Constraint defines a constraint.
type Constraint struct {
	key      string
	operator int
	exp      string
}

// Parse parses list of constraints.
func Parse(env []string) ([]Constraint, error) {
	exprs := []Constraint{}
	for _, e := range env {
		found := false
		// each expr is in the form of "key op value"
		for i, op := range operators {
			if !strings.Contains(e, op) {
				continue
			}
			// split with the op
			parts := strings.SplitN(e, op, 2)

			if len(parts) < 2 {
				return nil, fmt.Errorf("invalid expr: %s", e)
			}

			part0 := strings.TrimSpace(parts[0])
			// validate key
			matched := alphaNumeric.MatchString(part0)
			if !matched {
				return nil, fmt.Errorf("key '%s' is invalid", part0)
			}

			part1 := strings.TrimSpace(parts[1])

			// validate Value
			matched = valuePattern.MatchString(part1)
			if !matched {
				return nil, fmt.Errorf("value '%s' is invalid", part1)
			}
			// TODO(dongluochen): revisit requirements to see if globing or regex are useful
			exprs = append(exprs, Constraint{key: part0, operator: i, exp: part1})

			found = true
			break // found an op, move to next entry
		}
		if !found {
			return nil, fmt.Errorf("constraint expected one operator from %s", strings.Join(operators, ", "))
		}
	}
	return exprs, nil
}

// Match checks if the Constraint matches the target strings.
func (c *Constraint) Match(whats ...string) bool {
	var match bool

	// full string match
	for _, what := range whats {
		// case insensitive compare
		if strings.EqualFold(c.exp, what) {
			match = true
			break
		}
	}

	switch c.operator {
	case eq:
		return match
	case noteq:
		return !match
	}

	return false
}

// NodeMatches returns true if the node satisfies the given constraints.
func NodeMatches(constraints []Constraint, n *api.Node) bool {
	for _, constraint := range constraints {
		switch {
		case strings.EqualFold(constraint.key, "node.id"):
			if !constraint.Match(n.ID) {
				return false
			}
		case strings.EqualFold(constraint.key, "node.hostname"):
			// if this node doesn't have hostname
			// it's equivalent to match an empty hostname
			// where '==' would fail, '!=' matches
			if n.Description == nil {
				if !constraint.Match("") {
					return false
				}
				continue
			}
			if !constraint.Match(n.Description.Hostname) {
				return false
			}
		case strings.EqualFold(constraint.key, "node.ip"):
			nodeIP := net.ParseIP(n.Status.Addr)
			// single IP address, node.ip == 2001:db8::2
			if ip := net.ParseIP(constraint.exp); ip != nil {
				ipEq := ip.Equal(nodeIP)
				if (ipEq && constraint.operator != eq) || (!ipEq && constraint.operator == eq) {
					return false
				}
				continue
			}
			// CIDR subnet, node.ip != 210.8.4.0/24
			if _, subnet, err := net.ParseCIDR(constraint.exp); err == nil {
				within := subnet.Contains(nodeIP)
				if (within && constraint.operator != eq) || (!within && constraint.operator == eq) {
					return false
				}
				continue
			}
			// reject constraint with malformed address/network
			return false
		case strings.EqualFold(constraint.key, "node.role"):
			if !constraint.Match(n.Role.String()) {
				return false
			}
		case strings.EqualFold(constraint.key, "node.platform.os"):
			if n.Description == nil || n.Description.Platform == nil {
				if !constraint.Match("") {
					return false
				}
				continue
			}
			if !constraint.Match(n.Description.Platform.OS) {
				return false
			}
		case strings.EqualFold(constraint.key, "node.platform.arch"):
			if n.Description == nil || n.Description.Platform == nil {
				if !constraint.Match("") {
					return false
				}
				continue
			}
			if !constraint.Match(n.Description.Platform.Architecture) {
				return false
			}

		// node labels constraint in form like 'node.labels.key==value'
		case len(constraint.key) > len(NodeLabelPrefix) && strings.EqualFold(constraint.key[:len(NodeLabelPrefix)], NodeLabelPrefix):
			if n.Spec.Annotations.Labels == nil {
				if !constraint.Match("") {
					return false
				}
				continue
			}
			label := constraint.key[len(NodeLabelPrefix):]
			// label itself is case sensitive
			val := n.Spec.Annotations.Labels[label]
			if !constraint.Match(val) {
				return false
			}

		// engine labels constraint in form like 'engine.labels.key!=value'
		case len(constraint.key) > len(EngineLabelPrefix) && strings.EqualFold(constraint.key[:len(EngineLabelPrefix)], EngineLabelPrefix):
			if n.Description == nil || n.Description.Engine == nil || n.Description.Engine.Labels == nil {
				if !constraint.Match("") {
					return false
				}
				continue
			}
			label := constraint.key[len(EngineLabelPrefix):]
			val := n.Description.Engine.Labels[label]
			if !constraint.Match(val) {
				return false
			}
		default:
			// key doesn't match predefined syntax
			return false
		}
	}

	return true
}
